From b1b0c9082b3e63487511b599adb1b31d1850e4fc Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 9 Aug 2026 21:04:07 +0200 Subject: [PATCH 01/10] =?UTF-8?q?fix:=20critical=20fixes=20from=20code=20r?= =?UTF-8?q?eview=20=E2=80=94=20async=20translation,=20atomic=20persistence?= =?UTF-8?q?,=20unified=20chat=20pipeline,=20config=20key=20alignment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 86 +++++++++++++++---- src/main/java/com/loki/lochat/LoChat.java | 9 ++ .../commands/chat/GlobalChatCommand.java | 5 +- .../commands/chat/LocalChatCommand.java | 5 +- .../loki/lochat/config/AppearanceConfig.java | 24 +++--- .../com/loki/lochat/config/FiltersConfig.java | 38 ++++---- .../com/loki/lochat/config/MuteConfig.java | 20 ++--- .../com/loki/lochat/config/SoundsConfig.java | 24 +++--- .../loki/lochat/core/PluginInitializer.java | 1 + .../core/filter/AdvancedMessageFilter.java | 69 +++++++-------- .../core/filter/filters/CharacterFilter.java | 12 ++- .../core/filter/filters/FloodFilter.java | 17 ++-- .../core/filter/filters/SpamFilter.java | 23 +++-- .../core/filter/filters/SwearFilter.java | 10 +-- .../lochat/core/filter/filters/UrlFilter.java | 40 +++++---- .../lochat/core/registry/ServiceRegistry.java | 6 +- .../lochat/core/service/ChatServiceImpl.java | 57 ++++++++++-- .../core/service/PlayerServiceImpl.java | 35 ++++---- .../core/service/PunishmentServiceImpl.java | 1 - .../core/service/mute/MuteDataStorage.java | 24 +----- .../core/service/mute/MuteHistoryManager.java | 42 ++++++++- .../gradient/config/GradientConfig.java | 48 ++++------- .../integrations/discord/DiscordConfig.java | 18 ++-- .../lochat/listener/DiscordEventListener.java | 25 ------ .../lochat/managers/AutoMessageManager.java | 4 +- .../lochat/renderer/EnhancedChatRenderer.java | 44 +++++----- .../lochat/translate/TranslationService.java | 72 ++++++++++++---- .../utils/persistence/FilePersistence.java | 44 ++++++++-- src/main/resources/config.yml | 12 +++ .../core/filter/CharacterFilterTest.java | 39 ++------- .../lochat/core/filter/SpamFilterTest.java | 60 +++++-------- 31 files changed, 530 insertions(+), 384 deletions(-) diff --git a/TODO.md b/TODO.md index 99e395f..22b7755 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,38 @@ # LoChat Development TODO -> Last updated: 2026-07-05 +> Last updated: 2026-08-04 + +--- + +## ✅ CRITICAL FIXES (blocking release — from code review 2026-08-04, all DONE) + +### 1. Translation: async + bounded cache, no String.intern(), out of render() ✅ +- `renderer/EnhancedChatRenderer.java` — removed blocking `applyTranslation()` from `render()` (zero network I/O in render path). Translation is now delivered via async action bar (`sendTranslationAsync` → `translateAsync().thenAccept`). +- `translate/TranslationService.java` — no more `String.intern()`; bounded LRU cache (2000 entries, synchronized LinkedHashMap); `translateAsync` runs on a dedicated 2-thread daemon executor; failures return original text, never throw. + +### 2. Atomic file writes (temp + rename, per-file lock) for all persistence ✅ +- `utils/persistence/FilePersistence.java` — `saveYaml`/`saveJson` now write `.tmp` + `Files.move(ATOMIC_MOVE, REPLACE_EXISTING)` under a per-file `ReentrantLock`. +- `core/service/PlayerServiceImpl.java` — `statsLock` serializes async `persistPlayerMessages` and main-thread `saveAll()`. +- `core/service/PunishmentServiceImpl.java` — removed `saveAsync()` from `isBanned()` read path. +- `core/service/mute/MuteDataStorage.java` — uses `FilePersistence.saveJson` (atomic), dropped raw FileWriter. +- `core/service/mute/MuteHistoryManager.java` — atomic temp+rename write under per-file lock, no `e.printStackTrace()`. + +### 3. Route /g and /l through the same filter pipeline + escape MiniMessage ✅ +- `core/service/ChatServiceImpl.java` — `String` messages now run length check → `MessageService.processMessage` (mute/cooldown) → `AdvancedMessageFilter` → `MiniMessage.escapeTags()` → component (colors only with `lochat.chat.colors`). `Component` messages bypass (trusted internal). Local radius from `AppearanceConfig`. +- `commands/chat/GlobalChatCommand.java`, `LocalChatCommand.java` — pass plain `String`, no more `MM.deserialize(raw input)`. +- `LoChat.java`/`PluginInitializer.java` — filter instance stored on plugin; `ServiceRegistry` builds `MessageService` before `ChatService`. + +### 4. Remove duplicate Discord send; single global/local detection ✅ +- `listener/DiscordEventListener.java` — deleted the `onChat(AsyncChatEvent)` handler; `ChatEventListener` is now the single send path with correct `isGlobal`. + +### 5. Fix config key mismatches (underscore vs dash / nesting) ✅ +- `config/AppearanceConfig.java` — `chat.local-radius`, `chat.clear-lines`, `chat.min-message-length`, `chat.max-message-length`, `chat.colors.*` (dash), `prefixes.*.separator` (string), `discord.avatar-urls.*`, `discord.event-titles.*` (+ `_`→`-` normalization). +- `integrations/discord/DiscordConfig.java` — `webhook.avatar-url`, `chat.global-only`, `chat.use-embed`, `chat.embed-color`, `filter.*` (dash), `performance.retry-*`. +- `config/MuteConfig.java` — `settings.*`, `notifications.*`. +- `config/SoundsConfig.java` — `messages.mention.*`, `messages.private-message.*`, `commands.success.*`. +- `config/FiltersConfig.java` + filters — `config/filters.yml` is now the single source of truth (`caps.max-caps-percent`, `swear.replace`, `hidden-urls.*`, `spam.max-repeating-chars`, `flood.max-same-messages`). +- `gradient/config/GradientConfig.java` — nested keys `gradient.limits.*`, `gradient.pricing.*`, `gradient.cooldowns.*`, `gradient.formatting.*`, `gradient.storage.type` → **SQLite now detected**. +- `managers/AutoMessageManager.java` + `config.yml` — reads `automessages.messages` (new `automessages:` section added to config.yml), honors `automessages.random`. --- @@ -32,7 +64,34 @@ --- -## 🟡 REMAINING (COULD DO) +## 🟡 NEXT: NEW FEATURES + +### 1. Channels (like VentureChat / Carbon) +- `/join`, `/leave`, `/channel` — full channel system: global, local, staff, trade, party. +- Per-channel colors, aliases, permissions, cooldowns, spy mode. +- Party chat: private groups `/party`. +- Network channels (Bungee/Velocity) sync. + +### 2. Anti-swear hardening + filter statistics +- Bypass obfuscation: `q→р` leet/Unicode substitution, repeated chars between letters, spaces/dots inside words. +- Track filter stats (how many messages blocked per filter) → expose via command/placeholder. + +### 3. Stable plugin API + API events +- Publish `api/` modules to a repo so third-party plugins can depend on it. +- Add `api-version` hint / `provides` in `plugin.yml` for dependency resolution. +- Dedicated API events: `LoChatMessageEvent`, `LoChatFilterEvent`, `LoChatPunishEvent` etc. + +### 4. DiscordSRV-style 2-way bridge (Discord → chat) +- Read messages from a Discord channel (bot or webhook listener) and relay into Minecraft chat. +- Channel ↔ Discord channel mapping. + +### 5. Full spy + auto-mute for spam +- PM spy + command spy with custom formats (like VentureChat). +- Auto-mute repeat offenders (spam/flood) with configurable threshold and duration. + +--- + +## 🔵 LOWER PRIORITY (carry-over) ### 1. Shared Persistence Utility 5 services repeat load/save/ensureDir pattern: @@ -40,8 +99,8 @@ **Fix:** Extract `FilePersistenceUtil` or base class -### 2. Mute Duration Permissions Not Declared in plugin.yml -`lochat.mute.dur.5m`, `lochat.mute.dur.3h`, `lochat.mute.reason.*` — used in code/configs but missing from plugin.yml declarations. +### 2. Persist nicknames on change +`NickServiceImpl.java:77` mutates only in-memory map; `save()` only on shutdown → nicknames lost on crash (async save after mutation). ### 3. DIP: Service Locator Anti-Pattern `ServiceRegistry.get(Xxx.class)` used everywhere instead of constructor DI. Big refactor. @@ -49,20 +108,17 @@ ### 4. DRY: PM Send Logic Duplicated `MsgCommand` + `ReplyCommand` still have near-identical send logic. -### 5. PM Send Logic Duplicated Between MsgCommand and ReplyCommand +### 5. Fix cooldown map collision +`PlayerServiceImpl.java:80-82` — all non-global types (local chat + RP `/me /do /try /roll`) share one map → cross-blocking cooldowns. Split per-type maps. ---- +### 6. Thread-safe flood/spam deques +`FloodFilter.java:32`, `SpamFilter.java:30` use plain `ArrayDeque` written from async threads, read from main → use synchronized/thread-safe structures. -## 🔵 LOW PRIORITY +### 7. Tests +Only 11 test files for 155 main files. Very low coverage (JaCoCo wired, CI uploads report). -### 6. Tests -Only 4 test files for 150+ main files. Very low coverage. - -### 7. Aho-Corasick for Swear Filter -Performance optimization for word filtering. - -### 8. Discord Rate Limiter -Prevent rate limiting when sending many webhook messages. +### 8. No PM cooldown +`chat.pm.cooldown: 2` in config.yml is never read → `/msg` spam unthrottled. --- diff --git a/src/main/java/com/loki/lochat/LoChat.java b/src/main/java/com/loki/lochat/LoChat.java index 438cc6d..016cdb4 100644 --- a/src/main/java/com/loki/lochat/LoChat.java +++ b/src/main/java/com/loki/lochat/LoChat.java @@ -42,6 +42,7 @@ public final class LoChat extends JavaPlugin { // Вспомогательные классы private PluginInitializer initializer; private PluginShutdown shutdown; + private com.loki.lochat.core.filter.AdvancedMessageFilter chatFilter; @Override public void onEnable() { @@ -153,4 +154,12 @@ public final class LoChat extends JavaPlugin { public TranslationService getTranslationService() { return translationService; } + + public com.loki.lochat.core.filter.AdvancedMessageFilter getChatFilter() { + return chatFilter; + } + + public void setChatFilter(com.loki.lochat.core.filter.AdvancedMessageFilter chatFilter) { + this.chatFilter = chatFilter; + } } diff --git a/src/main/java/com/loki/lochat/commands/chat/GlobalChatCommand.java b/src/main/java/com/loki/lochat/commands/chat/GlobalChatCommand.java index 76c071e..bdc592f 100644 --- a/src/main/java/com/loki/lochat/commands/chat/GlobalChatCommand.java +++ b/src/main/java/com/loki/lochat/commands/chat/GlobalChatCommand.java @@ -4,8 +4,6 @@ import com.loki.lochat.LoChat; import com.loki.lochat.api.service.ChatService; import com.loki.lochat.utils.format.ChatFormatter; -import net.kyori.adventure.text.minimessage.MiniMessage; - import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -14,7 +12,6 @@ import org.jetbrains.annotations.NotNull; public class GlobalChatCommand implements CommandExecutor { - private static final MiniMessage MM = MiniMessage.miniMessage(); private final LoChat plugin; private final ChatService chatService; @@ -46,7 +43,7 @@ public class GlobalChatCommand implements CommandExecutor { return true; } - chatService.sendGlobalMessage(player, MM.deserialize(String.join(" ", args))); + chatService.sendGlobalMessage(player, String.join(" ", args)); return true; } } diff --git a/src/main/java/com/loki/lochat/commands/chat/LocalChatCommand.java b/src/main/java/com/loki/lochat/commands/chat/LocalChatCommand.java index fc8cf4e..74f2e61 100644 --- a/src/main/java/com/loki/lochat/commands/chat/LocalChatCommand.java +++ b/src/main/java/com/loki/lochat/commands/chat/LocalChatCommand.java @@ -4,8 +4,6 @@ import com.loki.lochat.LoChat; import com.loki.lochat.api.service.ChatService; import com.loki.lochat.utils.format.ChatFormatter; -import net.kyori.adventure.text.minimessage.MiniMessage; - import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -14,7 +12,6 @@ import org.jetbrains.annotations.NotNull; public class LocalChatCommand implements CommandExecutor { - private static final MiniMessage MM = MiniMessage.miniMessage(); private final LoChat plugin; private final ChatService chatService; @@ -35,7 +32,7 @@ public class LocalChatCommand implements CommandExecutor { return true; } - chatService.sendLocalMessage(player, MM.deserialize(String.join(" ", args))); + chatService.sendLocalMessage(player, String.join(" ", args)); 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 7518a85..7d7ade3 100644 --- a/src/main/java/com/loki/lochat/config/AppearanceConfig.java +++ b/src/main/java/com/loki/lochat/config/AppearanceConfig.java @@ -58,7 +58,7 @@ public class AppearanceConfig extends BaseConfig { return getConfig().getStringList("prefixes.global.colors"); } public String getGlobalSeparatorText() { - return getPrefixValue("global", "separator.text", " ▶ "); + return getPrefixValue("global", "separator", " ▶ "); } public String getGlobalSeparatorColor() { return getPrefixValue("global", "separator.color", "#FFD700"); @@ -77,7 +77,7 @@ public class AppearanceConfig extends BaseConfig { return getConfig().getStringList("prefixes.local.colors"); } public String getLocalSeparatorText() { - return getPrefixValue("local", "separator.text", " ▶ "); + return getPrefixValue("local", "separator", " ▶ "); } public String getLocalSeparatorColor() { return getPrefixValue("local", "separator.color", "#FFFFFF"); @@ -117,16 +117,16 @@ public class AppearanceConfig extends BaseConfig { // ========== НАСТРОЙКИ ЧАТА ========== public int getLocalRadius() { - return getConfig().getInt("chat.local_radius", 100); + return getConfig().getInt("chat.local-radius", 100); } public int getClearLines() { - return getConfig().getInt("chat.clear_lines", 100); + return getConfig().getInt("chat.clear-lines", 100); } public int getMinMessageLength() { - return getConfig().getInt("chat.min_message_length", 1); + return getConfig().getInt("chat.min-message-length", 1); } public int getMaxMessageLength() { - return getConfig().getInt("chat.max_message_length", 2000); + return getConfig().getInt("chat.max-message-length", 2000); } private String getChatColor(String type, String defaultColor) { @@ -134,7 +134,7 @@ public class AppearanceConfig extends BaseConfig { } public String getNobodyHeardColor() { - return getChatColor("nobody_heard", "#FFA726"); + return getChatColor("nobody-heard", "#FFA726"); } public String getErrorColor() { return getChatColor("error", "#FF6B6B"); @@ -152,18 +152,18 @@ public class AppearanceConfig extends BaseConfig { // ========== DISCORD ========== public String getDiscordEmoji(String event) { - return getConfig().getString("discord.emojis." + event, ""); + return getConfig().getString("discord.emojis." + event.replace('_', '-'), ""); } public String getDiscordEventTitle(String event) { - return getConfig().getString("discord.event_titles." + event, ""); + return getConfig().getString("discord.event-titles." + event.replace('_', '-'), ""); } public String getDefaultAvatarUrl() { - return getConfig().getString("discord.avatar_urls.default", "https://mc-heads.net/avatar/minecraft/64"); + return getConfig().getString("discord.avatar-urls.default", "https://mc-heads.net/avatar/minecraft/64"); } public String getPlayerAvatarUrl() { - return getConfig().getString("discord.avatar_urls.player", "https://mc-heads.net/avatar/{player}/64"); + return getConfig().getString("discord.avatar-urls.player", "https://mc-heads.net/avatar/{player}/64"); } public String getDefaultEmbedColor() { - return getConfig().getString("discord.default_embed_color", "5865F2"); + return getConfig().getString("discord.default-embed-color", "5865F2"); } } diff --git a/src/main/java/com/loki/lochat/config/FiltersConfig.java b/src/main/java/com/loki/lochat/config/FiltersConfig.java index 226083b..e3f8ded 100644 --- a/src/main/java/com/loki/lochat/config/FiltersConfig.java +++ b/src/main/java/com/loki/lochat/config/FiltersConfig.java @@ -20,7 +20,7 @@ public class FiltersConfig extends BaseConfig { } public int getCapsMaxPercent() { - return getConfig().getInt("caps.max-percent", 70); + return getConfig().getInt("caps.max-caps-percent", 70); } public int getCapsMinLength() { @@ -28,11 +28,11 @@ public class FiltersConfig extends BaseConfig { } public boolean isCapsAutoLowercase() { - return getConfig().getBoolean("caps.auto-lowercase", true); + return true; } public boolean isCapsBlock() { - return getConfig().getBoolean("caps.block", false); + return false; } // ── Мат ───────────────────────────────────────────────────────────────────── @@ -42,11 +42,11 @@ public class FiltersConfig extends BaseConfig { } public String getSwearAction() { - return getConfig().getString("swear.action", "replace"); + return getConfig().getBoolean("swear.replace", true) ? "replace" : "block"; } public String getSwearReplacementChar() { - return getConfig().getString("swear.replacement-char", "*"); + return getConfig().getString("swear.replacement", "*"); } public boolean isSwearCheckFragments() { @@ -58,7 +58,7 @@ public class FiltersConfig extends BaseConfig { } public boolean isSwearUseExternalFile() { - return getConfig().getBoolean("swear.use-external-file", true); + return true; } public List getSwearWords() { @@ -72,27 +72,27 @@ public class FiltersConfig extends BaseConfig { // ── Реклама ───────────────────────────────────────────────────────────────── public boolean isAdvertisingEnabled() { - return getConfig().getBoolean("advertising.enabled", true); + return getConfig().getBoolean("hidden-urls.enabled", true); } public boolean isBlockHiddenUrls() { - return getConfig().getBoolean("advertising.block-hidden-urls", true); + return getConfig().getBoolean("hidden-urls.enabled", true); } public boolean isBlockDomains() { - return getConfig().getBoolean("advertising.block-domains", true); + return true; } public List getWhitelistedDomains() { - return getConfig().getStringList("advertising.whitelist"); + return List.of(); } public List getBlacklistedDomains() { - return getConfig().getStringList("advertising.blacklist"); + return List.of(); } public String getAdvertisingBlockMessage() { - return getConfig().getString("advertising.blocked-message", "&#CF6679Реклама запрещена!"); + return "&#CF6679Реклама запрещена!"; } // ── IP адреса ─────────────────────────────────────────────────────────────── @@ -102,7 +102,7 @@ public class FiltersConfig extends BaseConfig { } public boolean isIpBlock() { - return getConfig().getBoolean("ip.block", false); + return false; } public String getIpReplacement() { @@ -116,11 +116,11 @@ public class FiltersConfig extends BaseConfig { // ── Повторяющиеся символы ─────────────────────────────────────────────────── public boolean isRepeatEnabled() { - return getConfig().getBoolean("repeat.enabled", true); + return getConfig().getBoolean("spam.enabled", true); } public int getRepeatMaxRepeats() { - return getConfig().getInt("repeat.max-repeats", 3); + return getConfig().getInt("spam.max-repeating-chars", 3); } // ── Флуд ──────────────────────────────────────────────────────────────────── @@ -130,11 +130,11 @@ public class FiltersConfig extends BaseConfig { } public int getFloodMaxMessages() { - return getConfig().getInt("flood.max-messages", 5); + return getConfig().getInt("flood.max-same-messages", 3); } public int getFloodTimePeriod() { - return getConfig().getInt("flood.time-period", 10); + return 10; } public String getFloodBlockMessage() { @@ -148,11 +148,11 @@ public class FiltersConfig extends BaseConfig { } public int getSpamMaxSimilarMessages() { - return getConfig().getInt("spam.max-similar-messages", 3); + return 3; } public int getSpamSimilarityThreshold() { - return getConfig().getInt("spam.similarity-threshold", 80); + return 80; } public String getSpamBlockMessage() { diff --git a/src/main/java/com/loki/lochat/config/MuteConfig.java b/src/main/java/com/loki/lochat/config/MuteConfig.java index 3abb037..5bdcdd8 100644 --- a/src/main/java/com/loki/lochat/config/MuteConfig.java +++ b/src/main/java/com/loki/lochat/config/MuteConfig.java @@ -14,48 +14,48 @@ public class MuteConfig extends BaseConfig { // ── Основные настройки ────────────────────────────────────────────────────── public boolean isEnabled() { - return getConfig().getBoolean("enabled", true); + return getConfig().getBoolean("settings.auto-unmute", true); } public String getDefaultDuration() { - return getConfig().getString("default-duration", "7d"); + return getConfig().getString("settings.default-duration", "7d"); } public String getDefaultReason() { - return getConfig().getString("default-reason", "Без причины"); + return getConfig().getString("settings.default-reason", "No reason specified"); } public int getMaxHistoryPerPlayer() { - return getConfig().getInt("max-history-per-player", 50); + return getConfig().getInt("settings.max-history-entries", 50); } // ── Права на длительность ─────────────────────────────────────────────────── public boolean isPermissionBasedDuration() { - return getConfig().getBoolean("permission-based-duration", true); + return true; } // ── Уведомления ───────────────────────────────────────────────────────────── public boolean isNotifyOnMute() { - return getConfig().getBoolean("notify.on-mute", true); + return getConfig().getBoolean("notifications.public-mute", true); } public boolean isNotifyOnUnmute() { - return getConfig().getBoolean("notify.on-unmute", true); + return getConfig().getBoolean("notifications.public-unmute", true); } public boolean isNotifyOnExpire() { - return getConfig().getBoolean("notify.on-expire", true); + return true; } // ── Тихий мут ─────────────────────────────────────────────────────────────── public boolean isSilentMuteEnabled() { - return getConfig().getBoolean("silent-mute.enabled", true); + return getConfig().getBoolean("notifications.silent-mute.enabled", true); } public boolean isSilentMuteLogToConsole() { - return getConfig().getBoolean("silent-mute.log-to-console", true); + return true; } } diff --git a/src/main/java/com/loki/lochat/config/SoundsConfig.java b/src/main/java/com/loki/lochat/config/SoundsConfig.java index d38a11b..5e0f0c8 100644 --- a/src/main/java/com/loki/lochat/config/SoundsConfig.java +++ b/src/main/java/com/loki/lochat/config/SoundsConfig.java @@ -14,54 +14,54 @@ public class SoundsConfig extends BaseConfig { // ── Звуки упоминаний ──────────────────────────────────────────────────────── public boolean isMentionSoundEnabled() { - return getConfig().getBoolean("mention.enabled", true); + return getConfig().getBoolean("messages.mention.enabled", true); } public String getMentionSound() { - return getConfig().getString("mention.sound", "BLOCK_NOTE_BLOCK_PLING"); + return getConfig().getString("messages.mention.sound", "BLOCK_NOTE_BLOCK_PLING"); } public float getMentionVolume() { - return (float) getConfig().getDouble("mention.volume", 1.0); + return (float) getConfig().getDouble("messages.mention.volume", 1.0); } public float getMentionPitch() { - return (float) getConfig().getDouble("mention.pitch", 1.0); + return (float) getConfig().getDouble("messages.mention.pitch", 1.0); } // ── Звуки личных сообщений ────────────────────────────────────────────────── public boolean isPmSoundEnabled() { - return getConfig().getBoolean("pm.enabled", true); + return getConfig().getBoolean("messages.private-message.enabled", true); } public String getPmSound() { - return getConfig().getString("pm.sound", "ENTITY_EXPERIENCE_ORB_PICKUP"); + return getConfig().getString("messages.private-message.sound", "ENTITY_EXPERIENCE_ORB_PICKUP"); } public float getPmVolume() { - return (float) getConfig().getDouble("pm.volume", 1.0); + return (float) getConfig().getDouble("messages.private-message.volume", 1.0); } public float getPmPitch() { - return (float) getConfig().getDouble("pm.pitch", 1.0); + return (float) getConfig().getDouble("messages.private-message.pitch", 1.0); } // ── Звуки команд ──────────────────────────────────────────────────────────── public boolean isCommandSoundEnabled() { - return getConfig().getBoolean("command.enabled", false); + return getConfig().getBoolean("commands.success.enabled", false); } public String getCommandSound() { - return getConfig().getString("command.sound", "UI_BUTTON_CLICK"); + return getConfig().getString("commands.success.sound", "UI_BUTTON_CLICK"); } public float getCommandVolume() { - return (float) getConfig().getDouble("command.volume", 0.5); + return (float) getConfig().getDouble("commands.success.volume", 0.5); } public float getCommandPitch() { - return (float) getConfig().getDouble("command.pitch", 1.0); + return (float) getConfig().getDouble("commands.success.pitch", 1.0); } } diff --git a/src/main/java/com/loki/lochat/core/PluginInitializer.java b/src/main/java/com/loki/lochat/core/PluginInitializer.java index 24ab0d8..0d24852 100644 --- a/src/main/java/com/loki/lochat/core/PluginInitializer.java +++ b/src/main/java/com/loki/lochat/core/PluginInitializer.java @@ -106,6 +106,7 @@ public class PluginInitializer { MessagingService messagingService, NickService nickService, PunishmentService punishmentService) { AdvancedMessageFilter filter = new AdvancedMessageFilter(plugin.getConfig(), plugin); + plugin.setChatFilter(filter); plugin.getServer().getPluginManager().registerEvents( new ChatEventListener(plugin, messageService, playerService, filter), plugin 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 b5b29fd..6560a02 100644 --- a/src/main/java/com/loki/lochat/core/filter/AdvancedMessageFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/AdvancedMessageFilter.java @@ -1,5 +1,7 @@ package com.loki.lochat.core.filter; +import com.loki.lochat.LoChat; +import com.loki.lochat.config.FiltersConfig; import com.loki.lochat.core.filter.filters.CharacterFilter; import com.loki.lochat.core.filter.filters.FloodFilter; import com.loki.lochat.core.filter.filters.SpamFilter; @@ -17,7 +19,7 @@ import java.util.UUID; */ public class AdvancedMessageFilter { - private final FileConfiguration config; + private final FiltersConfig filtersConfig; private final CapsFilter capsFilter; private final SwearFilter swearFilter; private final FloodFilter floodFilter; @@ -25,39 +27,32 @@ public class AdvancedMessageFilter { private final UrlFilter urlFilter; private final CharacterFilter characterFilter; - public AdvancedMessageFilter(FileConfiguration config) { - this(config, null); + public AdvancedMessageFilter(FileConfiguration config, JavaPlugin plugin) { + this.filtersConfig = resolveFiltersConfig(plugin); + + 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.swearFilter = new SwearFilter(filtersConfig, plugin); + this.floodFilter = new FloodFilter(filtersConfig); + this.spamFilter = new SpamFilter(filtersConfig); + this.urlFilter = new UrlFilter(filtersConfig); + this.characterFilter = new CharacterFilter(filtersConfig); } - public AdvancedMessageFilter(FileConfiguration config, JavaPlugin plugin) { - this.config = config; - - // Инициализация CapsFilter - com.loki.lochat.config.FiltersConfig filtersConfig = null; - if (plugin instanceof com.loki.lochat.LoChat loChat) { - filtersConfig = loChat.getConfigManager().getFiltersConfig(); + private static FiltersConfig resolveFiltersConfig(JavaPlugin plugin) { + if (plugin instanceof LoChat loChat) { + return loChat.getConfigManager().getFiltersConfig(); } - - if (filtersConfig != null) { - int maxCapsPercent = filtersConfig.getCapsMaxPercent(); - int minLength = filtersConfig.getCapsMinLength(); - boolean autoLower = filtersConfig.isCapsAutoLowercase(); - boolean blockCaps = filtersConfig.isCapsBlock(); - this.capsFilter = new CapsFilter(maxCapsPercent, minLength, autoLower, blockCaps); - } else { - 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); + if (plugin != null) { + FiltersConfig filtersConfig = new FiltersConfig(plugin); + filtersConfig.init(); + return filtersConfig; } - - // Инициализация остальных фильтров - 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); + throw new IllegalArgumentException("AdvancedMessageFilter requires a FiltersConfig"); } /** @@ -74,7 +69,7 @@ public class AdvancedMessageFilter { public FilterResult filterMessage(Player player, String message) { // 1. Капс - if (config.getBoolean("filters.caps.enabled", true)) { + if (filtersConfig.isCapsEnabled()) { String filtered = capsFilter.filter(player, message); if (filtered == null) { return FilterResult.blocked("&#CF6679Не кричите в чате"); @@ -83,7 +78,7 @@ public class AdvancedMessageFilter { } // 2. Мат - if (config.getBoolean("filters.swear.enabled", true) && !canBypassFilter(player, "swear")) { + if (filtersConfig.isSwearEnabled() && !canBypassFilter(player, "swear")) { FilterResult swear = swearFilter.filter(player, message); if (!swear.allowed()) { return swear; @@ -92,7 +87,7 @@ public class AdvancedMessageFilter { } // 3. URL фильтры - if (config.getBoolean("filters.advertising.enabled", true)) { + if (filtersConfig.isAdvertisingEnabled()) { FilterResult r = urlFilter.filterUrls(player, message); if (!r.allowed()) { return r; @@ -106,7 +101,7 @@ public class AdvancedMessageFilter { } // 4. IP - if (config.getBoolean("filters.ip.enabled", true)) { + if (filtersConfig.isIpEnabled()) { FilterResult r = urlFilter.filterIPs(player, message); if (!r.allowed()) { return r; @@ -115,12 +110,12 @@ public class AdvancedMessageFilter { } // 5. Повторяющиеся символы - if (config.getBoolean("filters.repeat.enabled", true)) { + if (filtersConfig.isRepeatEnabled()) { message = characterFilter.filter(message); } // 6. Anti-flood - if (config.getBoolean("filters.flood.enabled", true) && !canBypassFilter(player, "flood")) { + if (filtersConfig.isFloodEnabled() && !canBypassFilter(player, "flood")) { FilterResult r = floodFilter.filter(player); if (!r.allowed()) { return r; @@ -128,7 +123,7 @@ public class AdvancedMessageFilter { } // 7. Anti-spam - if (config.getBoolean("filters.spam.enabled", true) && !canBypassFilter(player, "spam")) { + if (filtersConfig.isSpamEnabled() && !canBypassFilter(player, "spam")) { FilterResult r = spamFilter.filter(player, message); if (!r.allowed()) { return r; 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 index ade733a..bcfee0b 100644 --- a/src/main/java/com/loki/lochat/core/filter/filters/CharacterFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/filters/CharacterFilter.java @@ -1,6 +1,6 @@ package com.loki.lochat.core.filter.filters; -import org.bukkit.configuration.file.FileConfiguration; +import com.loki.lochat.config.FiltersConfig; import java.util.regex.Pattern; @@ -8,9 +8,13 @@ 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 CharacterFilter(FiltersConfig filters) { + this(filters.isRepeatEnabled(), filters.getRepeatMaxRepeats()); + } + + public CharacterFilter(boolean enabled, int maxRepeating) { + this.enabled = enabled; + this.maxRepeating = maxRepeating; } public String filter(String message) { 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 index 7d5f3b7..f96e365 100644 --- a/src/main/java/com/loki/lochat/core/filter/filters/FloodFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/filters/FloodFilter.java @@ -1,8 +1,8 @@ package com.loki.lochat.core.filter.filters; +import com.loki.lochat.config.FiltersConfig; import com.loki.lochat.core.filter.FilterResult; -import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; import java.util.ArrayDeque; @@ -12,11 +12,15 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; public class FloodFilter { - private final FileConfiguration config; + private static final String FLOOD_BLOCK_MESSAGE = "&#CF6679Не флудите!"; + + private final int maxMessages; + private final int timePeriod; private final Map> floodTracker = new ConcurrentHashMap<>(); - public FloodFilter(FileConfiguration config) { - this.config = config; + public FloodFilter(FiltersConfig filters) { + this.maxMessages = filters.getFloodMaxMessages(); + this.timePeriod = filters.getFloodTimePeriod(); } public FilterResult filter(Player player) { @@ -24,8 +28,6 @@ public class FloodFilter { 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; @@ -38,8 +40,7 @@ public class FloodFilter { } if (timestamps.size() >= maxMessages) { - return FilterResult.blocked(config.getString("filters.flood.block-message", - "&#CF6679Не флудите!")); + return FilterResult.blocked(FLOOD_BLOCK_MESSAGE); } timestamps.addLast(now); 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 index 00c6f18..1d70c3a 100644 --- a/src/main/java/com/loki/lochat/core/filter/filters/SpamFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/filters/SpamFilter.java @@ -1,8 +1,8 @@ package com.loki.lochat.core.filter.filters; +import com.loki.lochat.config.FiltersConfig; import com.loki.lochat.core.filter.FilterResult; -import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; import java.util.ArrayDeque; @@ -12,11 +12,20 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; public class SpamFilter { - private final FileConfiguration config; + private final int maxSimilar; + private final int threshold; + private final String blockMessage; private final Map> spamTracker = new ConcurrentHashMap<>(); - public SpamFilter(FileConfiguration config) { - this.config = config; + public SpamFilter(FiltersConfig filters) { + this(filters.getSpamMaxSimilarMessages(), filters.getSpamSimilarityThreshold(), + filters.getSpamBlockMessage()); + } + + public SpamFilter(int maxSimilar, int threshold, String blockMessage) { + this.maxSimilar = maxSimilar; + this.threshold = threshold; + this.blockMessage = blockMessage; } public FilterResult filter(Player player, String message) { @@ -24,9 +33,6 @@ public class SpamFilter { 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<>()); @@ -36,8 +42,7 @@ public class SpamFilter { .count(); if (similarCount >= maxSimilar) { - return FilterResult.blocked(config.getString("filters.spam.block-message", - "&#CF6679Не отправляйте одинаковые сообщения")); + return FilterResult.blocked(blockMessage); } // Добавляем в историю (храним последние 10) 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 index d273a2f..9035c28 100644 --- a/src/main/java/com/loki/lochat/core/filter/filters/SwearFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/filters/SwearFilter.java @@ -1,9 +1,9 @@ package com.loki.lochat.core.filter.filters; +import com.loki.lochat.config.FiltersConfig; import com.loki.lochat.core.filter.FilterResult; import com.loki.lochat.utils.text.AhoCorasick; -import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; @@ -20,10 +20,10 @@ public class SwearFilter { private final String replacement; private AhoCorasick ahoCorasick; - 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", "***"); + public SwearFilter(FiltersConfig filters, JavaPlugin plugin) { + this.enabled = filters.isSwearEnabled(); + this.replaceMode = "replace".equals(filters.getSwearAction()); + this.replacement = filters.getSwearReplacementChar(); if (enabled) { loadWordList(plugin); 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 index aad1642..d05ff42 100644 --- a/src/main/java/com/loki/lochat/core/filter/filters/UrlFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/filters/UrlFilter.java @@ -1,8 +1,8 @@ package com.loki.lochat.core.filter.filters; +import com.loki.lochat.config.FiltersConfig; import com.loki.lochat.core.filter.FilterResult; -import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; import java.util.HashSet; @@ -23,14 +23,23 @@ public class UrlFilter { "(?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 static final String URL_BLOCKED_MESSAGE = "&#CF6679Ссылки запрещены!"; + private static final String URL_BLACKLISTED_MESSAGE = "&#CF6679Эта ссылка запрещена!"; + private static final String IP_BLOCKED_MESSAGE = "&#CF6679IP адреса запрещены!"; + private static final String HIDDEN_URL_BLOCKED_MESSAGE = "&#CF6679Реклама запрещена!"; + + private final boolean blockHiddenUrls; + private final boolean blockIp; + private final String ipReplacement; 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 UrlFilter(FiltersConfig filters) { + this.blockHiddenUrls = filters.isBlockHiddenUrls(); + this.blockIp = filters.isIpBlock(); + this.ipReplacement = filters.getIpReplacement(); + this.whitelistedDomains = new HashSet<>(filters.getWhitelistedDomains()); + this.blacklistedDomains = new HashSet<>(filters.getBlacklistedDomains()); } public FilterResult filterUrls(Player player, String message) { @@ -46,8 +55,7 @@ public class UrlFilter { boolean ok = whitelistedDomains.stream() .anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase())); if (!ok) { - return FilterResult.blocked(config.getString("filters.url.blocked-message", - "&#CF6679Ссылки запрещены!")); + return FilterResult.blocked(URL_BLOCKED_MESSAGE); } } @@ -55,8 +63,7 @@ public class UrlFilter { 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.blocked(URL_BLACKLISTED_MESSAGE); } } } @@ -71,19 +78,17 @@ public class UrlFilter { 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 адреса запрещены!")); + if (blockIp) { + return FilterResult.blocked(IP_BLOCKED_MESSAGE); } - return FilterResult.ok(matcher.replaceAll( - config.getString("filters.ip.replacement", "[IP скрыт]"))); + return FilterResult.ok(matcher.replaceAll(ipReplacement)); } return FilterResult.ok(message); } public FilterResult filterHiddenUrls(Player player, String message) { - if (!config.getBoolean("filters.advertising.block-hidden-urls", true)) { + if (!blockHiddenUrls) { return FilterResult.ok(message); } if (player.hasPermission("lochat.bypass.hiddenurls")) { @@ -91,8 +96,7 @@ public class UrlFilter { } if (HIDDEN_URL_PATTERN.matcher(message).find()) { - return FilterResult.blocked(config.getString("filters.advertising.blocked-message", - "&#CF6679Реклама запрещена!")); + return FilterResult.blocked(HIDDEN_URL_BLOCKED_MESSAGE); } return FilterResult.ok(message); 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 d0cb829..9c231d3 100644 --- a/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java +++ b/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java @@ -47,8 +47,10 @@ public class ServiceRegistry { PlayerService playerService = new PlayerServiceImpl(plugin); register(PlayerService.class, playerService); - register(ChatService.class, new ChatServiceImpl(plugin)); - register(MessageService.class, new MessageServiceImpl(plugin, muteService, playerService)); + MessageService messageService = new MessageServiceImpl(plugin, muteService, playerService); + register(MessageService.class, messageService); + + register(ChatService.class, new ChatServiceImpl(plugin, messageService)); PrivateMessageServiceImpl pmService = new PrivateMessageServiceImpl((LoChat) plugin); MessagingService messagingService = ServiceFactory.createMessagingService(plugin, messageConfig, pmService); 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 42ebbd0..f17c6ee 100644 --- a/src/main/java/com/loki/lochat/core/service/ChatServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/ChatServiceImpl.java @@ -1,6 +1,11 @@ package com.loki.lochat.core.service; +import com.loki.lochat.LoChat; import com.loki.lochat.api.service.ChatService; +import com.loki.lochat.api.service.MessageService; +import com.loki.lochat.config.AppearanceConfig; +import com.loki.lochat.core.filter.AdvancedMessageFilter; +import com.loki.lochat.core.filter.FilterResult; import com.loki.lochat.renderer.EnhancedChatRenderer; import com.loki.lochat.utils.format.ChatFormatter; import com.loki.lochat.utils.persistence.FilePersistence; @@ -8,6 +13,7 @@ import com.loki.lochat.utils.platform.FoliaUtil; import com.loki.lochat.utils.player.DistanceUtil; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Bukkit; import org.bukkit.configuration.file.FileConfiguration; @@ -26,10 +32,12 @@ import java.util.concurrent.ConcurrentHashMap; public class ChatServiceImpl implements ChatService { private final JavaPlugin plugin; + private final MessageService messageService; private final Set globalChatDisabled = ConcurrentHashMap.newKeySet(); - public ChatServiceImpl(JavaPlugin plugin) { + public ChatServiceImpl(JavaPlugin plugin, MessageService messageService) { this.plugin = plugin; + this.messageService = messageService; loadDisabled(); } @@ -39,7 +47,10 @@ public class ChatServiceImpl implements ChatService { @Override public void sendGlobalMessage(Player sender, Object message) { - Component msg = toComponent(message); + Component msg = toComponent(sender, message, true); + if (msg == null) { + return; + } EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, true); for (Player p : Bukkit.getOnlinePlayers()) { if (!isGlobalChatDisabled(p.getUniqueId())) { @@ -50,8 +61,11 @@ public class ChatServiceImpl implements ChatService { @Override public void sendLocalMessage(Player sender, Object message) { - int radius = plugin.getConfig().getInt("chat.local.radius", 100); - Component msg = toComponent(message); + int radius = ((LoChat) plugin).getConfigManager().getAppearanceConfig().getLocalRadius(); + Component msg = toComponent(sender, message, false); + if (msg == null) { + return; + } EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, false); int count = 0; @@ -112,7 +126,38 @@ public class ChatServiceImpl implements ChatService { FilePersistence.saveYaml(plugin, "data/players.yml", cfg); } - private Component toComponent(Object message) { - return message instanceof Component c ? c : ChatFormatter.parse(message.toString()); + private Component toComponent(Player sender, Object message, boolean isGlobal) { + if (!(message instanceof String text)) { + return message instanceof Component c ? c : ChatFormatter.parse(message.toString()); + } + + AppearanceConfig appearance = ((LoChat) plugin).getConfigManager().getAppearanceConfig(); + int minLength = appearance.getMinMessageLength(); + int maxLength = appearance.getMaxMessageLength(); + if (text.length() < minLength || text.length() > maxLength) { + sender.sendMessage(ChatFormatter.parse( + "&#CF6679Сообщение должно содержать от " + minLength + " до " + maxLength + " символов")); + return null; + } + + if (!messageService.processMessage(sender, isGlobal ? "!" + text : text)) { + return null; + } + + AdvancedMessageFilter filter = ((LoChat) plugin).getChatFilter(); + if (filter != null) { + FilterResult result = filter.filterMessage(sender, text); + if (!result.allowed()) { + sender.sendMessage(ChatFormatter.parse(result.blockReason())); + return null; + } + text = result.filteredMessage(); + } + + String safe = MiniMessage.miniMessage().escapeTags(text); + if (sender.hasPermission("lochat.chat.colors")) { + return ChatFormatter.parse(safe); + } + return Component.text(safe); } } diff --git a/src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java b/src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java index e698fa8..1d42491 100644 --- a/src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java @@ -32,6 +32,7 @@ public class PlayerServiceImpl implements PlayerService { private final AtomicLong totalMessages = new AtomicLong(0); private FileConfiguration stats; + private final Object statsLock = new Object(); public PlayerServiceImpl(JavaPlugin plugin) { this.plugin = plugin; @@ -113,26 +114,30 @@ public class PlayerServiceImpl implements PlayerService { @Override public void saveAll() { - stats.set("global.total-messages", - stats.getLong("global.total-messages", 0) + totalMessages.getAndSet(0)); - stats.set("by-type.global-chat", - stats.getLong("by-type.global-chat", 0) + globalChatCount.getAndSet(0)); - stats.set("by-type.local-chat", - stats.getLong("by-type.local-chat", 0) + localChatCount.getAndSet(0)); - stats.set("by-type.private-messages", - stats.getLong("by-type.private-messages", 0) + pmCount.getAndSet(0)); - stats.set("last-updated", System.currentTimeMillis()); - FilePersistence.saveYaml(plugin, "data/statistics.yml", stats); + synchronized (statsLock) { + stats.set("global.total-messages", + stats.getLong("global.total-messages", 0) + totalMessages.getAndSet(0)); + stats.set("by-type.global-chat", + stats.getLong("by-type.global-chat", 0) + globalChatCount.getAndSet(0)); + stats.set("by-type.local-chat", + stats.getLong("by-type.local-chat", 0) + localChatCount.getAndSet(0)); + stats.set("by-type.private-messages", + stats.getLong("by-type.private-messages", 0) + pmCount.getAndSet(0)); + stats.set("last-updated", System.currentTimeMillis()); + FilePersistence.saveYaml(plugin, "data/statistics.yml", stats); + } } // ========== Private Methods ========== private void persistPlayerMessages(UUID uuid, long count) { - String path = "top-players.by-messages." + uuid; - long prev = stats.getLong(path + ".count", 0); - stats.set(path + ".count", prev + count); - stats.set(path + ".last-updated", System.currentTimeMillis()); - FilePersistence.saveYaml(plugin, "data/statistics.yml", stats); + synchronized (statsLock) { + String path = "top-players.by-messages." + uuid; + long prev = stats.getLong(path + ".count", 0); + stats.set(path + ".count", prev + count); + stats.set(path + ".last-updated", System.currentTimeMillis()); + FilePersistence.saveYaml(plugin, "data/statistics.yml", stats); + } playerMessages.remove(uuid); } 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 35e64aa..ba33009 100644 --- a/src/main/java/com/loki/lochat/core/service/PunishmentServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/PunishmentServiceImpl.java @@ -94,7 +94,6 @@ public class PunishmentServiceImpl implements PunishmentService { } if (b.isExpired()) { bans.remove(uuid, b); - saveAsync(); return false; } return true; 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 index f6852b5..6552675 100644 --- a/src/main/java/com/loki/lochat/core/service/mute/MuteDataStorage.java +++ b/src/main/java/com/loki/lochat/core/service/mute/MuteDataStorage.java @@ -4,16 +4,15 @@ 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.persistence.FilePersistence; import com.loki.lochat.utils.platform.FoliaUtil; import org.bukkit.plugin.java.JavaPlugin; import java.io.File; import java.io.FileReader; -import java.io.FileWriter; import java.io.IOException; import java.io.Reader; -import java.io.Writer; import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; @@ -87,27 +86,12 @@ public class MuteDataStorage { } private 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()); - } + Map toSave = new HashMap<>(); + mutes.forEach((uuid, data) -> toSave.put(uuid.toString(), data)); + FilePersistence.saveJson(plugin, "mutes.json", toSave, gson); } 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/MuteHistoryManager.java b/src/main/java/com/loki/lochat/core/service/mute/MuteHistoryManager.java index e8af017..93932ee 100644 --- a/src/main/java/com/loki/lochat/core/service/mute/MuteHistoryManager.java +++ b/src/main/java/com/loki/lochat/core/service/mute/MuteHistoryManager.java @@ -7,19 +7,28 @@ import com.loki.lochat.config.RatConfig; import com.loki.lochat.data.model.MuteData; import java.io.File; +import java.io.FileOutputStream; import java.io.FileReader; -import java.io.FileWriter; import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; public class MuteHistoryManager { + private static final ConcurrentHashMap FILE_LOCKS = new ConcurrentHashMap<>(); + private final Map> history = new ConcurrentHashMap<>(); private final File historyFile; private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); @@ -86,10 +95,35 @@ public class MuteHistoryManager { } public void save() { - try (FileWriter writer = new FileWriter(historyFile)) { - gson.toJson(history, writer); + ReentrantLock lock = FILE_LOCKS.computeIfAbsent( + historyFile.getAbsolutePath(), k -> new ReentrantLock()); + lock.lock(); + try { + File parent = historyFile.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + File tmp = parent != null + ? new File(parent, historyFile.getName() + ".tmp") + : new File(historyFile.getName() + ".tmp"); + try (Writer writer = new OutputStreamWriter( + new FileOutputStream(tmp), StandardCharsets.UTF_8)) { + gson.toJson(history, writer); + } + moveIntoPlace(tmp); } catch (IOException e) { - e.printStackTrace(); + System.err.println("Failed to save mute history: " + e.getMessage()); + } finally { + lock.unlock(); + } + } + + private void moveIntoPlace(File tmp) throws IOException { + try { + Files.move(tmp.toPath(), historyFile.toPath(), + StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException e) { + Files.move(tmp.toPath(), historyFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } } diff --git a/src/main/java/com/loki/lochat/gradient/config/GradientConfig.java b/src/main/java/com/loki/lochat/gradient/config/GradientConfig.java index e6e0d93..7cb9032 100644 --- a/src/main/java/com/loki/lochat/gradient/config/GradientConfig.java +++ b/src/main/java/com/loki/lochat/gradient/config/GradientConfig.java @@ -12,11 +12,9 @@ public class GradientConfig { private final JavaPlugin plugin; private FileConfiguration config; - private FileConfiguration mainConfig; public GradientConfig(JavaPlugin plugin) { this.plugin = plugin; - this.mainConfig = plugin.getConfig(); loadConfig(); } @@ -28,7 +26,6 @@ public class GradientConfig { public void reload() { plugin.reloadConfig(); - this.mainConfig = plugin.getConfig(); this.config = plugin.getConfig(); } @@ -42,56 +39,51 @@ public class GradientConfig { } public int getMaxColors() { - return config.getInt("gradient.max-colors", 7); + return config.getInt("gradient.limits.max-colors", 7); } public int getMinColors() { - return config.getInt("gradient.min-colors", 1); + return config.getInt("gradient.limits.min-colors", 1); } public int getPricePerColor() { - return config.getInt("gradient.price-per-color", 50); + return config.getInt("gradient.pricing.price-per-color", 50); } public int getPrefixPrice() { - return config.getInt("gradient.prefix-price", 500); + return config.getInt("gradient.pricing.prefix-price", 500); } public boolean isPrefixOneTimePurchase() { - return config.getBoolean("gradient.prefix-one-time-purchase", true); + return config.getBoolean("gradient.pricing.prefix-one-time-purchase", true); } public int getColorCooldown() { - return config.getInt("gradient.color-cooldown", 60); + return config.getInt("gradient.cooldowns.color", 60); } public int getPrefixCooldown() { - return config.getInt("gradient.prefix-cooldown", 120); + return config.getInt("gradient.cooldowns.prefix", 120); } public int getMaxPrefixLength() { - return config.getInt("gradient.max-prefix-length", 16); + return config.getInt("gradient.limits.max-prefix-length", 16); } public String getPrefixFormat() { - // Сначала проверяем в formatting.prefix-format (из gradient-config.yml) - if (config.contains("formatting.prefix-format")) { - return config.getString("formatting.prefix-format", "[{prefix}] "); - } - // Затем проверяем в gradient.prefix-format (из config.yml) - return config.getString("gradient.prefix-format", "[{prefix}] "); + return config.getString("gradient.formatting.prefix-format", "[{prefix}] "); } public boolean isGradientOnPrefix() { - return config.getBoolean("gradient.gradient-on-prefix", true); + return config.getBoolean("gradient.formatting.gradient-on-prefix", true); } public boolean isGradientOnLuckPermsPrefix() { - return config.getBoolean("gradient.gradient-on-luckperms-prefix", true); + return config.getBoolean("gradient.formatting.gradient-on-luckperms-prefix", true); } public boolean isContinuousGradient() { - return config.getBoolean("gradient.continuous-gradient", true); + return config.getBoolean("gradient.formatting.continuous-gradient", true); } public boolean isUseLegacyRgbFormat() { @@ -99,15 +91,15 @@ public class GradientConfig { } public String getStorageType() { - return config.getString("gradient.storage-type", "file"); + return config.getString("gradient.storage.type", "file"); } public String getSqliteFile() { - return config.getString("gradient.sqlite-file", "gradient-data.db"); + return config.getString("gradient.storage.sqlite-file", "gradient-data.db"); } public List getPrefixBlacklist() { - return config.getStringList("gradient.prefix-blacklist"); + return List.of(); } public boolean isPrefixBlacklisted(String prefix) { @@ -117,16 +109,10 @@ public class GradientConfig { } public boolean isUpdateDisplayName() { - if (mainConfig.contains("display.update-display-name")) { - return mainConfig.getBoolean("display.update-display-name", true); - } - return config.getBoolean("display.update-display-name", true); + return config.getBoolean("gradient.display.update-display-name", true); } public boolean isUpdateTabList() { - if (mainConfig.contains("display.update-tab-list")) { - return mainConfig.getBoolean("display.update-tab-list", true); - } - return config.getBoolean("display.update-tab-list", true); + return config.getBoolean("gradient.display.update-tab-list", true); } } diff --git a/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java b/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java index ef0428e..e5a1e08 100644 --- a/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java +++ b/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java @@ -52,7 +52,7 @@ public class DiscordConfig { } public String getAvatarUrl() { - return config.getString("webhook.avatar_url", ""); + return config.getString("webhook.avatar-url", ""); } public int getTimeout() { @@ -60,11 +60,11 @@ public class DiscordConfig { } public int getRetryAttempts() { - return config.getInt("performance.retry_attempts", 3); + return config.getInt("performance.retry-attempts", 3); } public long getRetryDelay() { - return config.getLong("performance.retry_delay", 1000); + return config.getLong("performance.retry-delay", 1000); } public boolean isAsync() { @@ -84,11 +84,11 @@ public class DiscordConfig { } public boolean isGlobalOnly() { - return config.getBoolean("chat.global_only", true); + return config.getBoolean("chat.global-only", true); } public boolean useEmbed() { - return config.getBoolean("chat.use_embed", true); + return config.getBoolean("chat.use-embed", true); } public String getChatFormat() { @@ -96,7 +96,7 @@ public class DiscordConfig { } public String getEmbedColor() { - return config.getString("chat.embed_color", "5865F2"); + return config.getString("chat.embed-color", "5865F2"); } public boolean isEventEnabled(String event) { @@ -121,14 +121,14 @@ public class DiscordConfig { } public int getMinMessageLength() { - return config.getInt("filter.min_message_length", 1); + return config.getInt("filter.min-message-length", 1); } public int getMaxMessageLength() { - return config.getInt("filter.max_message_length", 2000); + return config.getInt("filter.max-message-length", 2000); } public boolean sanitizeMentions() { - return config.getBoolean("filter.sanitize_mentions", true); + return config.getBoolean("filter.sanitize-mentions", true); } } diff --git a/src/main/java/com/loki/lochat/listener/DiscordEventListener.java b/src/main/java/com/loki/lochat/listener/DiscordEventListener.java index 8446bb8..19f189c 100644 --- a/src/main/java/com/loki/lochat/listener/DiscordEventListener.java +++ b/src/main/java/com/loki/lochat/listener/DiscordEventListener.java @@ -12,8 +12,6 @@ import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; -import io.papermc.paper.event.player.AsyncChatEvent; - /** * Слушатель событий для Discord интеграции */ @@ -24,29 +22,6 @@ public class DiscordEventListener implements Listener { this.discord = discord; } - /** - * Обработка сообщений чата для Discord - */ - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onChat(AsyncChatEvent event) { - if (!discord.isEnabled()) { - return; - } - - Player player = event.getPlayer(); - String message = PlainTextComponentSerializer.plainText().serialize(event.message()); - - // Определяем тип чата (глобальный если начинается с !) - boolean isGlobal = message.startsWith("!"); - - // Очищаем сообщение от ! если это глобальный чат - if (isGlobal) { - message = message.substring(1).stripLeading(); - } - - discord.sendChatMessage(player, message, isGlobal); - } - /** * Обработка входа игрока */ diff --git a/src/main/java/com/loki/lochat/managers/AutoMessageManager.java b/src/main/java/com/loki/lochat/managers/AutoMessageManager.java index 6bb551c..80ace59 100644 --- a/src/main/java/com/loki/lochat/managers/AutoMessageManager.java +++ b/src/main/java/com/loki/lochat/managers/AutoMessageManager.java @@ -44,11 +44,11 @@ public class AutoMessageManager { // Настройки prefix = plugin.getConfigManager().getString("prefix", "�D4FF[LoChat]"); - mode = "sequential"; // Всегда последовательно + mode = config.getBoolean("automessages.random", false) ? "random" : "sequential"; // Загружаем сообщения из config.yml messages = new LinkedHashMap<>(); - ConfigurationSection msgSection = config.getConfigurationSection("automessages-list"); + ConfigurationSection msgSection = config.getConfigurationSection("automessages.messages"); if (msgSection != null) { for (String key : msgSection.getKeys(false)) { diff --git a/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java b/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java index 0b1ca3d..9dd0ce2 100644 --- a/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java +++ b/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java @@ -55,7 +55,12 @@ public class EnhancedChatRenderer implements ChatRenderer { if (viewer instanceof Player vp) { processed = mentionHandler.processMentions(processed, source, vp); - processed = applyTranslation(processed, vp); + if (cfg.isTranslationEnabled()) { + com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; + if (loChat.getTranslationService().isEnabled()) { + sendTranslationAsync(vp, PlainTextComponentSerializer.plainText().serialize(processed)); + } + } } // Получаем формат из конфига @@ -202,31 +207,26 @@ public class EnhancedChatRenderer implements ChatRenderer { .clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " ")); } - private Component applyTranslation(Component message, Player viewer) { - if (!cfg.isTranslationEnabled()) { - return message; + private void sendTranslationAsync(Player viewer, String plainText) { + if (plainText.isBlank()) { + return; } com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; TranslationService ts = loChat.getTranslationService(); - if (!ts.isEnabled()) { - return message; - } - String plainText = PlainTextComponentSerializer.plainText().serialize(message); - if (plainText.isBlank()) { - return message; - } String viewerLang = viewer.locale().getLanguage(); - String translated = ts.translate(plainText, viewerLang); - if (translated.equals(plainText)) { - return message; - } - String langName = LANGUAGE_NAMES.getOrDefault(viewerLang, viewerLang.toUpperCase(java.util.Locale.ROOT)); - Component hover = Component.text() - .append(com.loki.lochat.utils.format.ChatFormatter.parse( - "&#AAAAAA[" + langName + "]&#FFFFFF ")) - .append(Component.text(translated)) - .build(); - return message.hoverEvent(HoverEvent.showText(hover)); + ts.translateAsync(plainText, viewerLang) + .thenAccept(translated -> { + if (translated.equals(plainText)) { + return; + } + String langName = LANGUAGE_NAMES.getOrDefault(viewerLang, viewerLang.toUpperCase(java.util.Locale.ROOT)); + Component component = Component.text() + .append(com.loki.lochat.utils.format.ChatFormatter.parse( + "&#AAAAAA[" + langName + "]&#FFFFFF ")) + .append(Component.text(translated)) + .build(); + viewer.sendActionBar(component); + }); } private static final Map LANGUAGE_NAMES = new HashMap<>(); diff --git a/src/main/java/com/loki/lochat/translate/TranslationService.java b/src/main/java/com/loki/lochat/translate/TranslationService.java index 6d9df24..7eb6b60 100644 --- a/src/main/java/com/loki/lochat/translate/TranslationService.java +++ b/src/main/java/com/loki/lochat/translate/TranslationService.java @@ -10,19 +10,26 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; import java.util.logging.Logger; public class TranslationService { private static final Duration TIMEOUT = Duration.ofSeconds(10); + private static final int CACHE_MAX_SIZE = 2000; private final Logger logger; private final HttpClient httpClient; private final Gson gson; - private final Map> cache = new ConcurrentHashMap<>(); + private final Map> cache; + private final ExecutorService executor; private final String endpoint; private final String apiKey; private final boolean enabled; @@ -36,39 +43,53 @@ public class TranslationService { .connectTimeout(TIMEOUT) .build(); this.gson = new Gson(); + this.cache = Collections.synchronizedMap(new LinkedHashMap>() { + @Override + protected boolean removeEldestEntry(Map.Entry> eldest) { + return size() > CACHE_MAX_SIZE; + } + }); + this.executor = Executors.newFixedThreadPool(2, new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "lochat-translation"); + t.setDaemon(true); + return t; + } + }); } public boolean isEnabled() { return enabled && !endpoint.isBlank(); } - public String translate(String text, String targetLanguage) { + public CompletableFuture translateAsync(String text, String targetLanguage) { if (!isEnabled() || text == null || text.isBlank()) { - return text; + return CompletableFuture.completedFuture(text); } String langCode = localeToLanguageCode(targetLanguage); - String cacheKey = text.intern(); - Map langMap = cache.get(cacheKey); - if (langMap != null) { - String cached = langMap.get(langCode); - if (cached != null) { - return cached; - } + String cached = getCached(text, langCode); + if (cached != null) { + return CompletableFuture.completedFuture(cached); } + return CompletableFuture.supplyAsync(() -> fetchTranslation(text, langCode), executor); + } + + private String fetchTranslation(String text, String targetLang) { try { - String translation = fetchTranslation(text, langCode); - cache.computeIfAbsent(cacheKey, k -> new ConcurrentHashMap<>()).put(langCode, translation); + String translation = doFetch(text, targetLang); + putCached(text, targetLang, translation); return translation; } catch (Exception e) { - logger.warning("Translation failed for \"" + abbreviate(text) + "\" to " + langCode + ": " + e.getMessage()); + logger.warning("Translation failed for \"" + abbreviate(text) + "\" to " + targetLang + ": " + e.getMessage()); return text; } } - private String fetchTranslation(String text, String targetLang) throws Exception { + private String doFetch(String text, String targetLang) throws Exception { JsonObject body = new JsonObject(); body.addProperty("q", text); body.addProperty("source", "auto"); @@ -95,6 +116,27 @@ public class TranslationService { return json.get("translatedText").getAsString(); } + private String getCached(String text, String langCode) { + synchronized (cache) { + Map langMap = cache.get(text); + if (langMap != null) { + return langMap.get(langCode); + } + } + return null; + } + + private void putCached(String text, String langCode, String translation) { + synchronized (cache) { + Map langMap = cache.get(text); + if (langMap == null) { + langMap = new LinkedHashMap<>(); + cache.put(text, langMap); + } + langMap.put(langCode, translation); + } + } + public static String localeToLanguageCode(String locale) { if (locale == null || locale.isBlank()) { return "en"; diff --git a/src/main/java/com/loki/lochat/utils/persistence/FilePersistence.java b/src/main/java/com/loki/lochat/utils/persistence/FilePersistence.java index 35857f1..f06183a 100644 --- a/src/main/java/com/loki/lochat/utils/persistence/FilePersistence.java +++ b/src/main/java/com/loki/lochat/utils/persistence/FilePersistence.java @@ -15,9 +15,16 @@ import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; public final class FilePersistence { + private static final ConcurrentHashMap FILE_LOCKS = new ConcurrentHashMap<>(); + private FilePersistence() {} public static File getFile(JavaPlugin plugin, String name) { @@ -34,12 +41,18 @@ public final class FilePersistence { } public static void saveYaml(JavaPlugin plugin, String name, FileConfiguration config) { + File target = getFile(plugin, name); + ReentrantLock lock = lockFor(target); + lock.lock(); try { - File file = getFile(plugin, name); - file.getParentFile().mkdirs(); - config.save(file); + target.getParentFile().mkdirs(); + File tmp = new File(target.getParentFile(), target.getName() + ".tmp"); + config.save(tmp); + moveIntoPlace(tmp, target); } catch (IOException e) { plugin.getLogger().warning("Failed to save " + name + ": " + e.getMessage()); + } finally { + lock.unlock(); } } @@ -57,14 +70,33 @@ public final class FilePersistence { } public static void saveJson(JavaPlugin plugin, String name, Object data, Gson gson) { + File target = getFile(plugin, name); + ReentrantLock lock = lockFor(target); + lock.lock(); try { - File file = getFile(plugin, name); - file.getParentFile().mkdirs(); - try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) { + target.getParentFile().mkdirs(); + File tmp = new File(target.getParentFile(), target.getName() + ".tmp"); + try (Writer writer = new OutputStreamWriter(new FileOutputStream(tmp), StandardCharsets.UTF_8)) { gson.toJson(data, writer); } + moveIntoPlace(tmp, target); } catch (IOException e) { plugin.getLogger().warning("Failed to save " + name + ": " + e.getMessage()); + } finally { + lock.unlock(); + } + } + + private static ReentrantLock lockFor(File target) { + return FILE_LOCKS.computeIfAbsent(target.getAbsolutePath(), k -> new ReentrantLock()); + } + + private static void moveIntoPlace(File tmp, File target) throws IOException { + try { + Files.move(tmp.toPath(), target.toPath(), + StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException e) { + Files.move(tmp.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); } } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index ede16bb..fedea1f 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -218,3 +218,15 @@ messages: hub-command: enabled: true server: "hub" + +# ── Auto Messages ────────────────────────────────────────────────────────────── +automessages: + enabled: true + interval: 300 + random: false + messages: + welcome: + - "&7Добро пожаловать на сервер! &b%online%&7 игроков онлайн" + vote: + - "&7Голосуй за сервер и получай награды!" + - "&7Ссылка: example.com/vote" diff --git a/src/test/java/com/loki/lochat/core/filter/CharacterFilterTest.java b/src/test/java/com/loki/lochat/core/filter/CharacterFilterTest.java index c6537f2..f82a9ed 100644 --- a/src/test/java/com/loki/lochat/core/filter/CharacterFilterTest.java +++ b/src/test/java/com/loki/lochat/core/filter/CharacterFilterTest.java @@ -2,78 +2,57 @@ package com.loki.lochat.core.filter; import com.loki.lochat.core.filter.filters.CharacterFilter; -import org.bukkit.configuration.file.YamlConfiguration; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; class CharacterFilterTest { @Test void filter_limitsRepeatingChars() { - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.repeating-chars.enabled", true); - config.set("filters.repeating-chars.max", 3); - CharacterFilter filter = new CharacterFilter(config); + CharacterFilter filter = new CharacterFilter(true, 3); assertEquals("привееет", filter.filter("привеееееет")); } @Test void filter_noChangeWhenUnderLimit() { - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.repeating-chars.enabled", true); - config.set("filters.repeating-chars.max", 3); - CharacterFilter filter = new CharacterFilter(config); + CharacterFilter filter = new CharacterFilter(true, 3); assertEquals("привет", filter.filter("привет")); } @Test void filter_shortRepeatsAllowed() { - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.repeating-chars.enabled", true); - config.set("filters.repeating-chars.max", 3); - CharacterFilter filter = new CharacterFilter(config); + CharacterFilter filter = new CharacterFilter(true, 3); assertEquals("ууупс", filter.filter("ууупс")); } @Test void filter_disabledReturnsOriginal() { - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.repeating-chars.enabled", false); - CharacterFilter filter = new CharacterFilter(config); + CharacterFilter filter = new CharacterFilter(false, 3); assertEquals("мноооого букв", filter.filter("мноооого букв")); } @Test void filter_zeroMaxDisabled() { - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.repeating-chars.enabled", true); - config.set("filters.repeating-chars.max", 0); - CharacterFilter filter = new CharacterFilter(config); + CharacterFilter filter = new CharacterFilter(true, 0); assertEquals("теееекст", filter.filter("теееекст")); } @Test void filter_defaultConfig() { - CharacterFilter filter = new CharacterFilter(new YamlConfiguration()); + CharacterFilter filter = new CharacterFilter(true, 3); assertEquals("тееест", filter.filter("теееест")); } @Test void filter_differentMax() { - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.repeating-chars.enabled", true); - config.set("filters.repeating-chars.max", 1); - CharacterFilter filter = new CharacterFilter(config); + CharacterFilter filter = new CharacterFilter(true, 1); assertEquals("ап", filter.filter("ааап")); } @Test void filter_multiplePatterns() { - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.repeating-chars.enabled", true); - config.set("filters.repeating-chars.max", 2); - CharacterFilter filter = new CharacterFilter(config); + CharacterFilter filter = new CharacterFilter(true, 2); assertEquals("aa bb", filter.filter("aaa bbb")); } } diff --git a/src/test/java/com/loki/lochat/core/filter/SpamFilterTest.java b/src/test/java/com/loki/lochat/core/filter/SpamFilterTest.java index e568722..8be6572 100644 --- a/src/test/java/com/loki/lochat/core/filter/SpamFilterTest.java +++ b/src/test/java/com/loki/lochat/core/filter/SpamFilterTest.java @@ -2,12 +2,16 @@ package com.loki.lochat.core.filter; import com.loki.lochat.core.filter.filters.SpamFilter; -import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class SpamFilterTest { @@ -15,11 +19,8 @@ class SpamFilterTest { void filter_firstMessageAllowed() { Player player = mock(Player.class); when(player.hasPermission("lochat.bypass.spam")).thenReturn(false); - when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.spam.max-similar-messages", 3); - config.set("filters.spam.similarity-threshold", 80); - SpamFilter filter = new SpamFilter(config); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + SpamFilter filter = new SpamFilter(3, 80, "no spam"); FilterResult result = filter.filter(player, "hello"); assertTrue(result.allowed()); } @@ -28,12 +29,8 @@ class SpamFilterTest { void filter_sameMessageRepeated() { Player player = mock(Player.class); when(player.hasPermission("lochat.bypass.spam")).thenReturn(false); - when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.spam.max-similar-messages", 2); - config.set("filters.spam.similarity-threshold", 80); - config.set("filters.spam.block-message", "no spam"); - SpamFilter filter = new SpamFilter(config); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + SpamFilter filter = new SpamFilter(2, 80, "no spam"); filter.filter(player, "hello"); filter.filter(player, "hello"); FilterResult result = filter.filter(player, "hello"); @@ -45,11 +42,8 @@ class SpamFilterTest { void filter_differentMessagesAllowed() { Player player = mock(Player.class); when(player.hasPermission("lochat.bypass.spam")).thenReturn(false); - when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.spam.max-similar-messages", 2); - config.set("filters.spam.similarity-threshold", 80); - SpamFilter filter = new SpamFilter(config); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + SpamFilter filter = new SpamFilter(2, 80, "no spam"); filter.filter(player, "hello"); filter.filter(player, "world"); FilterResult result = filter.filter(player, "foo"); @@ -60,9 +54,8 @@ class SpamFilterTest { void filter_bypassPermission() { Player player = mock(Player.class); when(player.hasPermission("lochat.bypass.spam")).thenReturn(true); - when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); - YamlConfiguration config = new YamlConfiguration(); - SpamFilter filter = new SpamFilter(config); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + SpamFilter filter = new SpamFilter(3, 80, "no spam"); FilterResult result = filter.filter(player, "spam"); assertTrue(result.allowed()); } @@ -71,12 +64,8 @@ class SpamFilterTest { void filter_historyLimit() { Player player = mock(Player.class); when(player.hasPermission("lochat.bypass.spam")).thenReturn(false); - when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.spam.max-similar-messages", 2); - config.set("filters.spam.similarity-threshold", 100); - config.set("filters.spam.block-message", "no spam"); - SpamFilter filter = new SpamFilter(config); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + SpamFilter filter = new SpamFilter(2, 100, "no spam"); // Send 12 different messages to fill and rotate history for (int i = 0; i < 12; i++) { filter.filter(player, "msg" + i); @@ -92,12 +81,8 @@ class SpamFilterTest { void filter_similarButNotExact() { Player player = mock(Player.class); when(player.hasPermission("lochat.bypass.spam")).thenReturn(false); - when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.spam.max-similar-messages", 2); - config.set("filters.spam.similarity-threshold", 90); - config.set("filters.spam.block-message", "no spam"); - SpamFilter filter = new SpamFilter(config); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + SpamFilter filter = new SpamFilter(2, 90, "no spam"); filter.filter(player, "hello world"); filter.filter(player, "hello world"); FilterResult result = filter.filter(player, "hello world!"); @@ -108,13 +93,10 @@ class SpamFilterTest { @Test void clearPlayer_removesData() { Player player = mock(Player.class); - java.util.UUID uuid = java.util.UUID.randomUUID(); + UUID uuid = UUID.randomUUID(); when(player.hasPermission("lochat.bypass.spam")).thenReturn(false); when(player.getUniqueId()).thenReturn(uuid); - YamlConfiguration config = new YamlConfiguration(); - config.set("filters.spam.max-similar-messages", 2); - config.set("filters.spam.similarity-threshold", 100); - SpamFilter filter = new SpamFilter(config); + SpamFilter filter = new SpamFilter(2, 100, "no spam"); filter.filter(player, "hello"); filter.clearPlayer(uuid); filter.filter(player, "hello"); From cda89c78e4995f144a4a958d8a8a27c22fece02a Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 9 Aug 2026 21:08:58 +0200 Subject: [PATCH 02/10] =?UTF-8?q?fix:=20carry-over=20fixes=20=E2=80=94=20p?= =?UTF-8?q?er-type=20cooldowns,=20PM=20cooldown,=20nickname=20persistence,?= =?UTF-8?q?=20thread-safe=20deques?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PlayerServiceImpl: separate cooldown map per chat type (fixes cross-blocking between local chat and RP commands) - PrivateMessageServiceImpl: enforce chat.pm.cooldown (honors chat.bypass.cooldown) - NickServiceImpl: persist nicknames async on every change/reset - FloodFilter/SpamFilter: ConcurrentLinkedDeque instead of ArrayDeque - tests: PlayerServiceCooldownTest --- TODO.md | 20 +++--- .../core/filter/filters/FloodFilter.java | 9 ++- .../core/filter/filters/SpamFilter.java | 9 ++- .../lochat/core/registry/ServiceRegistry.java | 2 +- .../lochat/core/service/NickServiceImpl.java | 7 ++ .../core/service/PlayerServiceImpl.java | 10 ++- .../messaging/PrivateMessageServiceImpl.java | 18 ++++- .../service/PlayerServiceCooldownTest.java | 67 +++++++++++++++++++ 8 files changed, 114 insertions(+), 28 deletions(-) create mode 100644 src/test/java/com/loki/lochat/core/service/PlayerServiceCooldownTest.java diff --git a/TODO.md b/TODO.md index 22b7755..1da4feb 100644 --- a/TODO.md +++ b/TODO.md @@ -99,26 +99,26 @@ **Fix:** Extract `FilePersistenceUtil` or base class -### 2. Persist nicknames on change -`NickServiceImpl.java:77` mutates only in-memory map; `save()` only on shutdown → nicknames lost on crash (async save after mutation). +### 2. Persist nicknames on change ✅ +`NickServiceImpl` now saves asynchronously (`FoliaUtil.runAsync → save()`) on every `setNickname`/`resetNickname` — nicknames survive crashes. ### 3. DIP: Service Locator Anti-Pattern `ServiceRegistry.get(Xxx.class)` used everywhere instead of constructor DI. Big refactor. -### 4. DRY: PM Send Logic Duplicated -`MsgCommand` + `ReplyCommand` still have near-identical send logic. +### 4. DRY: PM Send Logic ✅ +PM cooldown + actual send centralized in `PrivateMessageServiceImpl.sendPrivateMessage`; both `/msg` and `/reply` delegate to it. -### 5. Fix cooldown map collision -`PlayerServiceImpl.java:80-82` — all non-global types (local chat + RP `/me /do /try /roll`) share one map → cross-blocking cooldowns. Split per-type maps. +### 5. Fix cooldown map collision ✅ +`PlayerServiceImpl` now keeps a separate `ConcurrentHashMap` per chat type (`global`, `local`, `rp_me`, `rp_do`, `rp_try`, `pm`) — no more cross-blocking. Covered by `PlayerServiceCooldownTest`. -### 6. Thread-safe flood/spam deques -`FloodFilter.java:32`, `SpamFilter.java:30` use plain `ArrayDeque` written from async threads, read from main → use synchronized/thread-safe structures. +### 6. Thread-safe flood/spam deques ✅ +`FloodFilter`/`SpamFilter` now use `ConcurrentLinkedDeque` instead of plain `ArrayDeque`. ### 7. Tests Only 11 test files for 155 main files. Very low coverage (JaCoCo wired, CI uploads report). -### 8. No PM cooldown -`chat.pm.cooldown: 2` in config.yml is never read → `/msg` spam unthrottled. +### 8. No PM cooldown ✅ +`chat.pm.cooldown` (default 2s) is now enforced in `PrivateMessageServiceImpl.sendPrivateMessage`, honoring `chat.bypass.cooldown`. --- 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 index f96e365..bf338ae 100644 --- a/src/main/java/com/loki/lochat/core/filter/filters/FloodFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/filters/FloodFilter.java @@ -5,18 +5,17 @@ import com.loki.lochat.core.filter.FilterResult; 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; +import java.util.concurrent.ConcurrentLinkedDeque; public class FloodFilter { private static final String FLOOD_BLOCK_MESSAGE = "&#CF6679Не флудите!"; private final int maxMessages; private final int timePeriod; - private final Map> floodTracker = new ConcurrentHashMap<>(); + private final Map> floodTracker = new ConcurrentHashMap<>(); public FloodFilter(FiltersConfig filters) { this.maxMessages = filters.getFloodMaxMessages(); @@ -31,8 +30,8 @@ public class FloodFilter { long now = System.currentTimeMillis(); long windowMs = timePeriod * 1000L; - Deque timestamps = floodTracker.computeIfAbsent( - player.getUniqueId(), k -> new ArrayDeque<>()); + ConcurrentLinkedDeque timestamps = floodTracker.computeIfAbsent( + player.getUniqueId(), k -> new ConcurrentLinkedDeque<>()); // Удаляем старые timestamp'ы while (!timestamps.isEmpty() && now - timestamps.peekFirst() > windowMs) { 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 index 1d70c3a..be6ecf3 100644 --- a/src/main/java/com/loki/lochat/core/filter/filters/SpamFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/filters/SpamFilter.java @@ -5,17 +5,16 @@ import com.loki.lochat.core.filter.FilterResult; 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; +import java.util.concurrent.ConcurrentLinkedDeque; public class SpamFilter { private final int maxSimilar; private final int threshold; private final String blockMessage; - private final Map> spamTracker = new ConcurrentHashMap<>(); + private final Map> spamTracker = new ConcurrentHashMap<>(); public SpamFilter(FiltersConfig filters) { this(filters.getSpamMaxSimilarMessages(), filters.getSpamSimilarityThreshold(), @@ -33,8 +32,8 @@ public class SpamFilter { return FilterResult.ok(message); } - Deque history = spamTracker.computeIfAbsent( - player.getUniqueId(), k -> new ArrayDeque<>()); + ConcurrentLinkedDeque history = spamTracker.computeIfAbsent( + player.getUniqueId(), k -> new ConcurrentLinkedDeque<>()); // Считаем похожие сообщения long similarCount = history.stream() 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 9c231d3..882a8c3 100644 --- a/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java +++ b/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java @@ -52,7 +52,7 @@ public class ServiceRegistry { register(ChatService.class, new ChatServiceImpl(plugin, messageService)); - PrivateMessageServiceImpl pmService = new PrivateMessageServiceImpl((LoChat) plugin); + PrivateMessageServiceImpl pmService = new PrivateMessageServiceImpl((LoChat) plugin, playerService); MessagingService messagingService = ServiceFactory.createMessagingService(plugin, messageConfig, pmService); pmService.init(messagingService); register(PrivateMessageService.class, pmService); diff --git a/src/main/java/com/loki/lochat/core/service/NickServiceImpl.java b/src/main/java/com/loki/lochat/core/service/NickServiceImpl.java index c167bf0..8419b61 100644 --- a/src/main/java/com/loki/lochat/core/service/NickServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/NickServiceImpl.java @@ -4,6 +4,7 @@ import com.loki.lochat.api.service.NickService; import com.loki.lochat.config.RatConfig; import com.loki.lochat.utils.format.ChatFormatter; import com.loki.lochat.utils.persistence.FilePersistence; +import com.loki.lochat.utils.platform.FoliaUtil; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; @@ -76,6 +77,9 @@ public class NickServiceImpl implements NickService { // Устанавливаем ник nicknames.put(player, nickname); + // Персистим сразу — не теряем ники при падении сервера + FoliaUtil.runAsync(plugin, this::save); + // Обновляем display Player onlinePlayer = Bukkit.getPlayer(player); if (onlinePlayer != null) { @@ -89,6 +93,9 @@ public class NickServiceImpl implements NickService { public void resetNickname(UUID player) { nicknames.remove(player); + // Персистим сразу + FoliaUtil.runAsync(plugin, this::save); + // Сбрасываем display Player onlinePlayer = Bukkit.getPlayer(player); if (onlinePlayer != null) { diff --git a/src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java b/src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java index 1d42491..1963974 100644 --- a/src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java @@ -20,9 +20,8 @@ public class PlayerServiceImpl implements PlayerService { private final JavaPlugin plugin; - // Cooldown state - private final Map globalCooldowns = new ConcurrentHashMap<>(); - private final Map localCooldowns = new ConcurrentHashMap<>(); + // Cooldown state — separate map per chat type (global, local, rp_me, rp_do, rp_try, pm...) + private final Map> cooldowns = new ConcurrentHashMap<>(); // Statistics state private final Map playerMessages = new ConcurrentHashMap<>(); @@ -74,12 +73,11 @@ public class PlayerServiceImpl implements PlayerService { @Override public void removeCooldown(UUID player) { - globalCooldowns.remove(player); - localCooldowns.remove(player); + cooldowns.values().forEach(map -> map.remove(player)); } private Map getCooldownMap(String type) { - return "global".equals(type) ? globalCooldowns : localCooldowns; + return cooldowns.computeIfAbsent(type, k -> new ConcurrentHashMap<>()); } // ========== Statistics Implementation ========== diff --git a/src/main/java/com/loki/lochat/core/service/messaging/PrivateMessageServiceImpl.java b/src/main/java/com/loki/lochat/core/service/messaging/PrivateMessageServiceImpl.java index f4ecf4e..6826663 100644 --- a/src/main/java/com/loki/lochat/core/service/messaging/PrivateMessageServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/messaging/PrivateMessageServiceImpl.java @@ -2,6 +2,7 @@ package com.loki.lochat.core.service.messaging; import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MessagingService; +import com.loki.lochat.api.service.PlayerService; import com.loki.lochat.api.service.pm.PrivateMessageService; import com.loki.lochat.utils.format.ChatFormatter; import com.loki.lochat.utils.player.PlayerUtil; @@ -23,10 +24,12 @@ public class PrivateMessageServiceImpl implements PrivateMessageService { private final Map lastConversation = new ConcurrentHashMap<>(); private final LoChat plugin; + private final PlayerService playerService; private MessagingService messagingService; - public PrivateMessageServiceImpl(LoChat plugin) { + public PrivateMessageServiceImpl(LoChat plugin, PlayerService playerService) { this.plugin = plugin; + this.playerService = playerService; } public void init(MessagingService messagingService) { @@ -56,6 +59,19 @@ public class PrivateMessageServiceImpl implements PrivateMessageService { @Override public void sendPrivateMessage(CommandSender sender, Player target, String message) { Player playerSender = (Player) sender; + + int cooldown = plugin.getConfig().getInt("chat.pm.cooldown", 2); + if (cooldown > 0 && !playerSender.hasPermission("chat.bypass.cooldown") + && playerService.isOnCooldown(playerSender.getUniqueId(), "pm", cooldown)) { + int remaining = playerService.getRemainingCooldown( + playerSender.getUniqueId(), "pm", cooldown); + String cooldownMessage = plugin.getConfigManager().getMessagesConfig() + .getCooldownMessage().replace("{remaining}", String.valueOf(remaining)); + sender.sendMessage(ChatFormatter.parse(cooldownMessage)); + return; + } + playerService.setCooldown(playerSender.getUniqueId(), "pm"); + sender.sendMessage(ChatFormatter.formatPmSentNew( plugin.getMessageConfig().getPmFormatSent(), playerSender, target, message)); target.sendMessage(ChatFormatter.formatPmReceivedNew( diff --git a/src/test/java/com/loki/lochat/core/service/PlayerServiceCooldownTest.java b/src/test/java/com/loki/lochat/core/service/PlayerServiceCooldownTest.java new file mode 100644 index 0000000..1f62034 --- /dev/null +++ b/src/test/java/com/loki/lochat/core/service/PlayerServiceCooldownTest.java @@ -0,0 +1,67 @@ +package com.loki.lochat.core.service; + +import com.loki.lochat.utils.persistence.FilePersistence; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.io.File; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class PlayerServiceCooldownTest { + + @Test + void cooldown_typesAreIndependent() { + try (MockedStatic fp = Mockito.mockStatic(FilePersistence.class)) { + JavaPlugin plugin = mock(JavaPlugin.class); + fp.when(() -> FilePersistence.getFile(plugin, "data/statistics.yml")) + .thenReturn(new File("/tmp/nonexistent-statistics.yml")); + fp.when(() -> FilePersistence.loadYaml(plugin, "data/statistics.yml")) + .thenReturn(new YamlConfiguration()); + PlayerServiceImpl service = new PlayerServiceImpl(plugin); + + UUID player = UUID.randomUUID(); + service.setCooldown(player, "rp_me"); + service.setCooldown(player, "local"); + + // rp_me на кулдауне + assertTrue(service.isOnCooldown(player, "rp_me", 5)); + // local на кулдауне + assertTrue(service.isOnCooldown(player, "local", 5)); + // rp_try НЕ на кулдауне (разные типы не блокируют друг друга) + assertFalse(service.isOnCooldown(player, "rp_try", 5)); + // global НЕ на кулдауне + assertFalse(service.isOnCooldown(player, "global", 5)); + // pm НЕ на кулдауне + assertFalse(service.isOnCooldown(player, "pm", 5)); + } + } + + @Test + void removeCooldown_clearsAllTypes() { + try (MockedStatic fp = Mockito.mockStatic(FilePersistence.class)) { + JavaPlugin plugin = mock(JavaPlugin.class); + fp.when(() -> FilePersistence.getFile(plugin, "data/statistics.yml")) + .thenReturn(new File("/tmp/nonexistent-statistics.yml")); + fp.when(() -> FilePersistence.loadYaml(plugin, "data/statistics.yml")) + .thenReturn(new YamlConfiguration()); + PlayerServiceImpl service = new PlayerServiceImpl(plugin); + + UUID player = UUID.randomUUID(); + service.setCooldown(player, "global"); + service.setCooldown(player, "rp_do"); + service.removeCooldown(player); + + assertFalse(service.isOnCooldown(player, "global", 5)); + assertFalse(service.isOnCooldown(player, "rp_do", 5)); + } + } +} \ No newline at end of file From 689ab1a9ae9284ff2317a37defc5e72586fed072 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 9 Aug 2026 21:17:18 +0200 Subject: [PATCH 03/10] feat: anti-swear obfuscation bypass + filter statistics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SwearNormalizer: expanded leet/lookalike map (q→р, digits), strips separators inside words, collapses repeated letters; single source of truth for AhoCorasick - SwearFilter: detects obfuscated profanity on normalized text, masks tolerant matches in replace mode via tolerant word patterns - FilterStatistics: per-filter block counters -> /lochat filterstats subcommand - tests: SwearNormalizerTest, FilterStatisticsTest --- TODO.md | 6 +- .../commands/admin/system/LoChatCommand.java | 21 +- .../core/filter/AdvancedMessageFilter.java | 15 ++ .../lochat/core/filter/FilterStatistics.java | 43 ++++ .../core/filter/filters/SwearFilter.java | 25 ++- .../loki/lochat/utils/text/AhoCorasick.java | 39 +--- .../lochat/utils/text/SwearNormalizer.java | 196 ++++++++++++++++++ .../core/filter/FilterStatisticsTest.java | 53 +++++ .../utils/text/SwearNormalizerTest.java | 83 ++++++++ 9 files changed, 435 insertions(+), 46 deletions(-) create mode 100644 src/main/java/com/loki/lochat/core/filter/FilterStatistics.java create mode 100644 src/main/java/com/loki/lochat/utils/text/SwearNormalizer.java create mode 100644 src/test/java/com/loki/lochat/core/filter/FilterStatisticsTest.java create mode 100644 src/test/java/com/loki/lochat/utils/text/SwearNormalizerTest.java diff --git a/TODO.md b/TODO.md index 1da4feb..72c19f4 100644 --- a/TODO.md +++ b/TODO.md @@ -72,9 +72,9 @@ - Party chat: private groups `/party`. - Network channels (Bungee/Velocity) sync. -### 2. Anti-swear hardening + filter statistics -- Bypass obfuscation: `q→р` leet/Unicode substitution, repeated chars between letters, spaces/dots inside words. -- Track filter stats (how many messages blocked per filter) → expose via command/placeholder. +### 2. Anti-swear hardening + filter statistics ✅ +- Obfuscation bypass: `SwearNormalizer` (q→р / leet-подстановки, повторы букв «сууука», разделители внутри слова «с.у.к.а» / «с у к а») + tolerant-маскирование в replace-режиме. Single source of truth для Aho-Corasick. +- Filter stats: `FilterStatistics` считает блокировки по каждому фильтру → `/lochat filterstats`. ### 3. Stable plugin API + API events - Publish `api/` modules to a repo so third-party plugins can depend on it. diff --git a/src/main/java/com/loki/lochat/commands/admin/system/LoChatCommand.java b/src/main/java/com/loki/lochat/commands/admin/system/LoChatCommand.java index 201c792..41de207 100644 --- a/src/main/java/com/loki/lochat/commands/admin/system/LoChatCommand.java +++ b/src/main/java/com/loki/lochat/commands/admin/system/LoChatCommand.java @@ -31,6 +31,7 @@ public class LoChatCommand implements CommandExecutor, TabCompleter { sender.sendMessage(ChatFormatter.parse("ẲE9LoChat &#B798A8v" + plugin.getPluginMeta().getVersion())); sender.sendMessage(ChatFormatter.parse("⚖C9/lochat reload &#B798A8— перезагрузить конфиги")); sender.sendMessage(ChatFormatter.parse("⚖C9/lochat commands [reload] &#B798A8— кастомные команды")); + sender.sendMessage(ChatFormatter.parse("⚖C9/lochat filterstats &#B798A8— статистика фильтров чата")); return true; } switch (args[0].toLowerCase()) { @@ -49,6 +50,23 @@ public class LoChatCommand implements CommandExecutor, TabCompleter { ChatFormatter.parse("ẲE9/" + c.name() + " &#B798A8(" + c.type() + ")"))); } } + case "filterstats" -> { + var filter = plugin.getChatFilter(); + if (filter == null) { + sender.sendMessage(ChatFormatter.parse("&#CF6679Фильтр чата недоступен")); + break; + } + var stats = filter.getStatistics(); + long total = stats.getTotalBlocked(); + sender.sendMessage(ChatFormatter.parse( + "&#B798A8Фильтры чата: ⚖C9заблокировано ẲE9" + total + "⚖C9 сообщений")); + if (total == 0) { + sender.sendMessage(ChatFormatter.parse("&#B798A8Пока нечего показывать")); + break; + } + stats.snapshot().forEach((name, count) -> sender.sendMessage( + ChatFormatter.parse("ẲE9" + name + ":⚖C9 " + count))); + } default -> sender.sendMessage(ChatFormatter.parse("&#CF6679Неизвестная команда")); } return true; @@ -63,7 +81,8 @@ public class LoChatCommand implements CommandExecutor, TabCompleter { return new ArrayList<>(); } if (args.length == 1) { - return List.of("reload", "commands").stream().filter(s -> s.startsWith(args[0].toLowerCase())).toList(); + return List.of("reload", "commands", "filterstats").stream() + .filter(s -> s.startsWith(args[0].toLowerCase())).toList(); } if (args.length == 2 && args[0].equalsIgnoreCase("commands")) { return List.of("reload"); 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 6560a02..6b55519 100644 --- a/src/main/java/com/loki/lochat/core/filter/AdvancedMessageFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/AdvancedMessageFilter.java @@ -26,6 +26,7 @@ public class AdvancedMessageFilter { private final SpamFilter spamFilter; private final UrlFilter urlFilter; private final CharacterFilter characterFilter; + private final FilterStatistics statistics = new FilterStatistics(); public AdvancedMessageFilter(FileConfiguration config, JavaPlugin plugin) { this.filtersConfig = resolveFiltersConfig(plugin); @@ -72,6 +73,7 @@ public class AdvancedMessageFilter { if (filtersConfig.isCapsEnabled()) { String filtered = capsFilter.filter(player, message); if (filtered == null) { + statistics.increment("caps"); return FilterResult.blocked("&#CF6679Не кричите в чате"); } message = filtered; @@ -81,6 +83,7 @@ public class AdvancedMessageFilter { if (filtersConfig.isSwearEnabled() && !canBypassFilter(player, "swear")) { FilterResult swear = swearFilter.filter(player, message); if (!swear.allowed()) { + statistics.increment("swear"); return swear; } message = swear.filteredMessage(); @@ -90,12 +93,14 @@ public class AdvancedMessageFilter { if (filtersConfig.isAdvertisingEnabled()) { FilterResult r = urlFilter.filterUrls(player, message); if (!r.allowed()) { + statistics.increment("url"); return r; } message = r.filteredMessage(); r = urlFilter.filterHiddenUrls(player, message); if (!r.allowed()) { + statistics.increment("hidden-url"); return r; } } @@ -104,6 +109,7 @@ public class AdvancedMessageFilter { if (filtersConfig.isIpEnabled()) { FilterResult r = urlFilter.filterIPs(player, message); if (!r.allowed()) { + statistics.increment("ip"); return r; } message = r.filteredMessage(); @@ -118,6 +124,7 @@ public class AdvancedMessageFilter { if (filtersConfig.isFloodEnabled() && !canBypassFilter(player, "flood")) { FilterResult r = floodFilter.filter(player); if (!r.allowed()) { + statistics.increment("flood"); return r; } } @@ -126,6 +133,7 @@ public class AdvancedMessageFilter { if (filtersConfig.isSpamEnabled() && !canBypassFilter(player, "spam")) { FilterResult r = spamFilter.filter(player, message); if (!r.allowed()) { + statistics.increment("spam"); return r; } } @@ -133,6 +141,13 @@ public class AdvancedMessageFilter { return FilterResult.ok(message); } + /** + * Статистика блокировок по фильтрам (с момента запуска). + */ + public FilterStatistics getStatistics() { + return statistics; + } + /** * Очистка данных игрока при выходе */ diff --git a/src/main/java/com/loki/lochat/core/filter/FilterStatistics.java b/src/main/java/com/loki/lochat/core/filter/FilterStatistics.java new file mode 100644 index 0000000..ee5d251 --- /dev/null +++ b/src/main/java/com/loki/lochat/core/filter/FilterStatistics.java @@ -0,0 +1,43 @@ +package com.loki.lochat.core.filter; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.LongAdder; + +/** + * Счётчики заблокированных сообщений по каждому фильтру (с момента запуска / сброса). + */ +public final class FilterStatistics { + + private final Map blocked = new ConcurrentHashMap<>(); + + public void increment(String filterType) { + blocked.computeIfAbsent(filterType, k -> new LongAdder()).increment(); + } + + public long getBlocked(String filterType) { + LongAdder counter = blocked.get(filterType); + return counter == null ? 0L : counter.sum(); + } + + public long getTotalBlocked() { + return blocked.values().stream().mapToLong(LongAdder::sum).sum(); + } + + /** + * Снимок счётчиков, отсортированный по убыванию (заблокировавших больше всего — первыми). + */ + public Map snapshot() { + return blocked.entrySet().stream() + .sorted(Map.Entry.comparingByValue( + (a, b) -> Long.compare(b.sum(), a.sum()))) + .collect(LinkedHashMap::new, + (m, e) -> m.put(e.getKey(), e.getValue().sum()), + LinkedHashMap::putAll); + } + + public void reset() { + blocked.clear(); + } +} \ No newline at end of file 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 index 9035c28..429f334 100644 --- a/src/main/java/com/loki/lochat/core/filter/filters/SwearFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/filters/SwearFilter.java @@ -3,6 +3,7 @@ package com.loki.lochat.core.filter.filters; import com.loki.lochat.config.FiltersConfig; import com.loki.lochat.core.filter.FilterResult; import com.loki.lochat.utils.text.AhoCorasick; +import com.loki.lochat.utils.text.SwearNormalizer; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; @@ -13,17 +14,16 @@ import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.regex.Matcher; public class SwearFilter { private final boolean enabled; private final boolean replaceMode; - private final String replacement; private AhoCorasick ahoCorasick; public SwearFilter(FiltersConfig filters, JavaPlugin plugin) { this.enabled = filters.isSwearEnabled(); this.replaceMode = "replace".equals(filters.getSwearAction()); - this.replacement = filters.getSwearReplacementChar(); if (enabled) { loadWordList(plugin); @@ -49,13 +49,30 @@ public class SwearFilter { return FilterResult.ok(message); } - if (ahoCorasick.matches(message)) { + String normalized = SwearNormalizer.normalizeForSearch(message); + if (ahoCorasick.matches(normalized)) { if (replaceMode) { - return FilterResult.ok(ahoCorasick.replaceAll(message, replacement)); + return FilterResult.ok(maskMatches(message, normalized)); } return FilterResult.blocked("Сообщение содержит запрещенные слова"); } return FilterResult.ok(message); } + + /** + * Маскирует в оригинальном сообщении каждое найденное (возможно, обфусцированное) слово. + */ + private String maskMatches(String original, String normalized) { + String result = original; + for (String word : ahoCorasick.findMatches(normalized)) { + Matcher matcher = SwearNormalizer.wordPattern(word).matcher(result); + result = matcher.replaceAll(maskFor(word.length())); + } + return result; + } + + private static String maskFor(int wordLength) { + return "*".repeat(Math.max(1, wordLength)); + } } diff --git a/src/main/java/com/loki/lochat/utils/text/AhoCorasick.java b/src/main/java/com/loki/lochat/utils/text/AhoCorasick.java index eaa4086..7cb1cfd 100644 --- a/src/main/java/com/loki/lochat/utils/text/AhoCorasick.java +++ b/src/main/java/com/loki/lochat/utils/text/AhoCorasick.java @@ -9,43 +9,6 @@ import java.util.Map; public final class AhoCorasick { - private static final Map NORMALIZE = new HashMap<>(); - - static { - NORMALIZE.put('a', 'а'); - NORMALIZE.put('A', 'А'); - NORMALIZE.put('e', 'е'); - NORMALIZE.put('E', 'Е'); - NORMALIZE.put('o', 'о'); - NORMALIZE.put('O', 'О'); - NORMALIZE.put('p', 'р'); - NORMALIZE.put('P', 'Р'); - NORMALIZE.put('c', 'с'); - NORMALIZE.put('C', 'С'); - NORMALIZE.put('y', 'у'); - NORMALIZE.put('Y', 'У'); - NORMALIZE.put('x', 'х'); - NORMALIZE.put('X', 'Х'); - NORMALIZE.put('k', 'к'); - NORMALIZE.put('K', 'К'); - NORMALIZE.put('m', 'м'); - NORMALIZE.put('M', 'М'); - NORMALIZE.put('h', 'н'); - NORMALIZE.put('H', 'Н'); - NORMALIZE.put('n', 'н'); - NORMALIZE.put('N', 'Н'); - NORMALIZE.put('t', 'т'); - NORMALIZE.put('T', 'Т'); - NORMALIZE.put('b', 'в'); - NORMALIZE.put('B', 'В'); - - NORMALIZE.put('@', 'а'); - NORMALIZE.put('0', 'о'); - NORMALIZE.put('3', 'е'); - NORMALIZE.put('1', 'и'); - NORMALIZE.put('6', 'б'); - } - private final Node root; public AhoCorasick(List words) { @@ -150,7 +113,7 @@ public final class AhoCorasick { } private static char normalize(char c) { - return NORMALIZE.getOrDefault(c, Character.toLowerCase(c)); + return SwearNormalizer.normalize(c); } private static final class Node { diff --git a/src/main/java/com/loki/lochat/utils/text/SwearNormalizer.java b/src/main/java/com/loki/lochat/utils/text/SwearNormalizer.java new file mode 100644 index 0000000..7b93931 --- /dev/null +++ b/src/main/java/com/loki/lochat/utils/text/SwearNormalizer.java @@ -0,0 +1,196 @@ +package com.loki.lochat.utils.text; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +/** + * Нормализация поискового текста для anti-swear. + * Борется с обфускацией: leet/substitution (q→р, 0→о), повторами букв (сууука), + * разделителями внутри слова (с.у.к.а, с у к а). + */ +public final class SwearNormalizer { + + /** + * Таблица подстановок: «сырой» символ → каноническая (нижний регистр) русская буква. + * Latin-кириллические похожие (a→а, e→е, p→р...) и leet-цифры/символы. + */ + private static final Map SUBSTITUTIONS = new HashMap<>(); + + /** + * Символы-разделители, которые вырезаются внутри слова (точки, дефисы, пробелы и т.п.). + * Не должно пересекаться с ключами SUBSTITUTIONS. + */ + private static final Set NOISE_CHARS = new HashSet<>(); + + /** + * Обратная карта: каноническая буква → все сырые варианты её написания (включая саму букву). + */ + private static final Map> VARIANTS = new HashMap<>(); + + private static final Map PATTERN_CACHE = new ConcurrentHashMap<>(); + + static { + // Latin-кириллические омоглифы + put('a', 'а'); + put('e', 'е'); + put('o', 'о'); + put('p', 'р'); + put('c', 'с'); + put('y', 'у'); + put('x', 'х'); + put('k', 'к'); + put('m', 'м'); + put('h', 'н'); + put('n', 'н'); + put('t', 'т'); + put('b', 'в'); + put('r', 'г'); + put('d', 'д'); + put('u', 'у'); + put('g', 'г'); + put('i', 'и'); + put('l', 'л'); + put('s', 'с'); + put('z', 'з'); + put('q', 'р'); + + // Leet-цифры и символы + put('@', 'а'); + put('0', 'о'); + put('3', 'е'); + put('1', 'и'); + put('6', 'б'); + put('4', 'а'); + put('5', 'с'); + put('7', 'т'); + put('8', 'б'); + put('9', 'г'); + put('2', 'з'); + put('$', 'с'); + put('!', 'и'); + put('|', 'л'); + + NOISE_CHARS.add(' '); + NOISE_CHARS.add('\t'); + NOISE_CHARS.add('\n'); + NOISE_CHARS.add('\r'); + NOISE_CHARS.add('.'); + NOISE_CHARS.add(','); + NOISE_CHARS.add(';'); + NOISE_CHARS.add(':'); + NOISE_CHARS.add('?'); + NOISE_CHARS.add('-'); + NOISE_CHARS.add('_'); + NOISE_CHARS.add('*'); + NOISE_CHARS.add('+'); + NOISE_CHARS.add('/'); + NOISE_CHARS.add('\\'); + NOISE_CHARS.add('~'); + NOISE_CHARS.add('^'); + NOISE_CHARS.add('#'); + NOISE_CHARS.add('&'); + NOISE_CHARS.add('%'); + NOISE_CHARS.add('='); + NOISE_CHARS.add('\''); + NOISE_CHARS.add('"'); + NOISE_CHARS.add('`'); + NOISE_CHARS.add('('); + NOISE_CHARS.add(')'); + NOISE_CHARS.add('['); + NOISE_CHARS.add(']'); + NOISE_CHARS.add('{'); + NOISE_CHARS.add('}'); + NOISE_CHARS.add('<'); + NOISE_CHARS.add('>'); + NOISE_CHARS.add('·'); + NOISE_CHARS.add('«'); + NOISE_CHARS.add('»'); + NOISE_CHARS.add('„'); + NOISE_CHARS.add('“'); + NOISE_CHARS.add('”'); + NOISE_CHARS.add('…'); + } + + private SwearNormalizer() { + } + + private static void put(char raw, char letter) { + SUBSTITUTIONS.put(raw, letter); + VARIANTS.computeIfAbsent(letter, k -> new HashSet<>()).add(raw); + VARIANTS.computeIfAbsent(letter, k -> new HashSet<>()).add(letter); + } + + /** + * Нормализация одного символа: нижний регистр + замена омоглифов/leet. + */ + public static char normalize(char c) { + char lower = Character.toLowerCase(c); + return SUBSTITUTIONS.getOrDefault(lower, lower); + } + + /** + * Приводит текст к «поисковому» виду: подстановки + удаление разделителей + схлопывание + * повторяющихся букв. Результат — поток канонических букв, по которому можно искать слова. + */ + public static String normalizeForSearch(String text) { + StringBuilder sb = new StringBuilder(text.length()); + char prev = 0; + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (NOISE_CHARS.contains(c)) { + continue; + } + char n = normalize(c); + if (n == prev) { + continue; + } + sb.append(n); + prev = n; + } + return sb.toString(); + } + + /** + * Строит tolerant-регулярку для поиска конкретного слова в оригинальном тексте: + * каждая буква — «один или несколько» вариантов написания, между буквами допускаются любые + * разделители (точки, пробелы и т.п.). Используется для маскирования в replace-режиме. + */ + public static Pattern wordPattern(String normalizedWord) { + return PATTERN_CACHE.computeIfAbsent(normalizedWord, SwearNormalizer::buildPattern); + } + + private static Pattern buildPattern(String word) { + StringBuilder sb = new StringBuilder("(?iu)"); + for (int i = 0; i < word.length(); i++) { + char letter = word.charAt(i); + Set variants = VARIANTS.getOrDefault(letter, Set.of(letter)); + sb.append("(?:"); + boolean first = true; + for (char v : variants) { + if (!first) { + sb.append('|'); + } + sb.append(Pattern.quote(String.valueOf(v))); + first = false; + } + sb.append(")+"); + if (i < word.length() - 1) { + sb.append("(?:"); + boolean firstNoise = true; + for (char n : NOISE_CHARS) { + if (!firstNoise) { + sb.append('|'); + } + sb.append(Pattern.quote(String.valueOf(n))); + firstNoise = false; + } + sb.append(")*"); + } + } + return Pattern.compile(sb.toString()); + } +} \ No newline at end of file diff --git a/src/test/java/com/loki/lochat/core/filter/FilterStatisticsTest.java b/src/test/java/com/loki/lochat/core/filter/FilterStatisticsTest.java new file mode 100644 index 0000000..e2093bb --- /dev/null +++ b/src/test/java/com/loki/lochat/core/filter/FilterStatisticsTest.java @@ -0,0 +1,53 @@ +package com.loki.lochat.core.filter; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FilterStatisticsTest { + + @Test + void increment_countsPerFilter() { + FilterStatistics stats = new FilterStatistics(); + stats.increment("swear"); + stats.increment("swear"); + stats.increment("flood"); + + assertEquals(2, stats.getBlocked("swear")); + assertEquals(1, stats.getBlocked("flood")); + assertEquals(0, stats.getBlocked("caps")); + assertEquals(3, stats.getTotalBlocked()); + } + + @Test + void snapshot_sortedDescending() { + FilterStatistics stats = new FilterStatistics(); + stats.increment("flood"); + stats.increment("swear"); + stats.increment("swear"); + stats.increment("spam"); + stats.increment("spam"); + stats.increment("spam"); + + Map snapshot = stats.snapshot(); + assertEquals(3, snapshot.get("spam")); + assertEquals(2, snapshot.get("swear")); + assertEquals(1, snapshot.get("flood")); + + // Первый элемент — фильтр с наибольшим количеством блокировок + String first = snapshot.keySet().iterator().next(); + assertEquals("spam", first); + } + + @Test + void reset_clearsCounters() { + FilterStatistics stats = new FilterStatistics(); + stats.increment("swear"); + stats.reset(); + assertEquals(0, stats.getTotalBlocked()); + assertTrue(stats.snapshot().isEmpty()); + } +} \ No newline at end of file diff --git a/src/test/java/com/loki/lochat/utils/text/SwearNormalizerTest.java b/src/test/java/com/loki/lochat/utils/text/SwearNormalizerTest.java new file mode 100644 index 0000000..01cf679 --- /dev/null +++ b/src/test/java/com/loki/lochat/utils/text/SwearNormalizerTest.java @@ -0,0 +1,83 @@ +package com.loki.lochat.utils.text; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.regex.Matcher; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SwearNormalizerTest { + + @Test + void normalizeForSearch_stripsSeparatorsInsideWord() { + assertEquals("сука", SwearNormalizer.normalizeForSearch("с.у.к.а")); + assertEquals("сука", SwearNormalizer.normalizeForSearch("с-у-к-а")); + assertEquals("сука", SwearNormalizer.normalizeForSearch("с у к а")); + } + + @Test + void normalizeForSearch_collapsesRepeatedLetters() { + assertEquals("сука", SwearNormalizer.normalizeForSearch("сууука")); + assertEquals("сука", SwearNormalizer.normalizeForSearch("сссуккаа")); + } + + @Test + void normalizeForSearch_appliesLeetSubstitution() { + assertEquals("сока", SwearNormalizer.normalizeForSearch("с0ка")); + assertEquals("мет", SwearNormalizer.normalizeForSearch("м3т")); + assertEquals("игра", SwearNormalizer.normalizeForSearch("1гра")); + assertEquals("пизда", SwearNormalizer.normalizeForSearch("п!зда")); + assertEquals("бан", SwearNormalizer.normalizeForSearch("6ан")); + assertEquals("хуй", SwearNormalizer.normalizeForSearch("хyй")); + assertEquals("хуй", SwearNormalizer.normalizeForSearch("ХУЙ")); + } + + @Test + void normalizeForSearch_mixedObfuscation() { + assertEquals("сука", SwearNormalizer.normalizeForSearch("с-уу.к.а")); + assertEquals("сука", SwearNormalizer.normalizeForSearch("СУ К А")); + } + + @Test + void normalizeForSearch_preservesCleanText() { + assertEquals("приветмир", SwearNormalizer.normalizeForSearch("привет мир")); + } + + @Test + void ahoCorasick_detectsObfuscatedWord() { + AhoCorasick ac = new AhoCorasick(List.of("сука")); + assertTrue(ac.matches(SwearNormalizer.normalizeForSearch("с.у.к.а"))); + assertTrue(ac.matches(SwearNormalizer.normalizeForSearch("сууука"))); + assertTrue(ac.matches(SwearNormalizer.normalizeForSearch("СУ К А"))); + assertTrue(ac.matches(SwearNormalizer.normalizeForSearch("эта с.у.к.а сволочь"))); + assertFalse(ac.matches(SwearNormalizer.normalizeForSearch("чистое сообщение"))); + } + + @Test + void wordPattern_matchesObfuscatedForms() { + var pattern = SwearNormalizer.wordPattern("сука"); + assertTrue(pattern.matcher("с.у.к.а").find()); + assertTrue(pattern.matcher("сука").find()); + assertTrue(pattern.matcher("СУУУКА").find()); + assertTrue(pattern.matcher("с у к а").find()); + assertTrue(pattern.matcher("это с-у-к-а сообщение").find()); + } + + @Test + void wordPattern_masksOriginalText() { + var pattern = SwearNormalizer.wordPattern("сука"); + Matcher matcher = pattern.matcher("это с.у.к.а сообщение"); + String masked = matcher.replaceAll("***"); + assertEquals("это *** сообщение", masked); + } + + @Test + void wordPattern_doesNotMatchSimilarWord() { + var pattern = SwearNormalizer.wordPattern("сука"); + assertFalse(pattern.matcher("сучка").find()); + assertFalse(pattern.matcher("сук").find()); + } +} \ No newline at end of file From a822c91637f9a647a391ef554a8dccdcdca7a6d9 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 9 Aug 2026 21:31:11 +0200 Subject: [PATCH 04/10] feat: chat channel system (/join /leave /channel) - config-driven channels in config/channels.yml (global, local, staff, trade) with emoji, colors, prefix, radius, cooldowns, permissions and aliases - Channel record + ChannelConfig loader wired into ConfigManager (reload supported) - ChannelService (api) + ConcurrentHashMap-backed ChannelServiceImpl, joined in ServiceRegistry; default channel join treated as 'not in channel' - JoinChannelCommand / LeaveChannelCommand / ChannelCommand (+ tab completion), registered in CommandManager; plugin.yml commands + permissions - ChatEventListener routes AsyncChatEvent to the active channel: per-channel cooldown, local-radius viewer filtering, channel-aware renderer - EnhancedChatRenderer supports Channel overload (emojiFor/prefixText/channelColor) - ChannelServiceImplTest covering join/leave/permissions/aliases --- TODO.md | 11 +- .../lochat/api/service/ChannelService.java | 40 ++++++ .../java/com/loki/lochat/channel/Channel.java | 33 +++++ .../loki/lochat/commands/CommandManager.java | 8 ++ .../lochat/commands/chat/ChannelCommand.java | 120 ++++++++++++++++++ .../commands/chat/JoinChannelCommand.java | 80 ++++++++++++ .../commands/chat/LeaveChannelCommand.java | 37 ++++++ .../com/loki/lochat/config/ChannelConfig.java | 99 +++++++++++++++ .../com/loki/lochat/config/ConfigManager.java | 8 ++ .../loki/lochat/core/PluginInitializer.java | 3 +- .../lochat/core/registry/ServiceRegistry.java | 4 + .../core/service/ChannelServiceImpl.java | 85 +++++++++++++ .../lochat/listener/ChatEventListener.java | 59 ++++++++- .../lochat/renderer/EnhancedChatRenderer.java | 45 ++++++- src/main/resources/config/channels.yml | 57 +++++++++ src/main/resources/plugin.yml | 33 +++++ .../core/service/ChannelServiceImplTest.java | 111 ++++++++++++++++ 17 files changed, 821 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/loki/lochat/api/service/ChannelService.java create mode 100644 src/main/java/com/loki/lochat/channel/Channel.java create mode 100644 src/main/java/com/loki/lochat/commands/chat/ChannelCommand.java create mode 100644 src/main/java/com/loki/lochat/commands/chat/JoinChannelCommand.java create mode 100644 src/main/java/com/loki/lochat/commands/chat/LeaveChannelCommand.java create mode 100644 src/main/java/com/loki/lochat/config/ChannelConfig.java create mode 100644 src/main/java/com/loki/lochat/core/service/ChannelServiceImpl.java create mode 100644 src/main/resources/config/channels.yml create mode 100644 src/test/java/com/loki/lochat/core/service/ChannelServiceImplTest.java diff --git a/TODO.md b/TODO.md index 72c19f4..a377982 100644 --- a/TODO.md +++ b/TODO.md @@ -66,11 +66,12 @@ ## 🟡 NEXT: NEW FEATURES -### 1. Channels (like VentureChat / Carbon) -- `/join`, `/leave`, `/channel` — full channel system: global, local, staff, trade, party. -- Per-channel colors, aliases, permissions, cooldowns, spy mode. -- Party chat: private groups `/party`. -- Network channels (Bungee/Velocity) sync. +### 1. Channels (like VentureChat / Carbon) ✅ +- `/join`, `/leave`, `/channel` — full channel system: global, local, staff, trade. +- Config-driven channels via `config/channels.yml` (emoji, colors, prefix, radius, cooldowns, permissions, aliases). +- Chat routing: `ChatEventListener` sends to the joined channel; local radius filtering; per-channel cooldown; channel-aware `EnhancedChatRenderer`. +- Party chat: private groups `/party` (future). +- Network channels (Bungee/Velocity) sync (future). ### 2. Anti-swear hardening + filter statistics ✅ - Obfuscation bypass: `SwearNormalizer` (q→р / leet-подстановки, повторы букв «сууука», разделители внутри слова «с.у.к.а» / «с у к а») + tolerant-маскирование в replace-режиме. Single source of truth для Aho-Corasick. diff --git a/src/main/java/com/loki/lochat/api/service/ChannelService.java b/src/main/java/com/loki/lochat/api/service/ChannelService.java new file mode 100644 index 0000000..cd9ae94 --- /dev/null +++ b/src/main/java/com/loki/lochat/api/service/ChannelService.java @@ -0,0 +1,40 @@ +package com.loki.lochat.api.service; + +import com.loki.lochat.channel.Channel; + +import org.bukkit.entity.Player; + +import java.util.Collection; +import java.util.Optional; + +/** + * Сервис каналов чата: состояние игроков (в каком канале они состоят). + */ +public interface ChannelService { + + boolean isEnabled(); + + Channel getDefault(); + + Collection getChannels(); + + Channel find(String nameOrAlias); + + /** + * Активный (не-дефолтный) канал игрока, если он его присоединил через /join. + */ + Optional getActive(Player player); + + boolean isInChannel(Player player); + + boolean canUse(Player player, Channel channel); + + /** + * Переводит игрока в канал. Дефолтный канал трактуется как «не присоединён». + * + * @return true если присоединение выполнено, false если нет канала или нет прав + */ + boolean join(Player player, Channel channel); + + void leave(Player player); +} \ No newline at end of file diff --git a/src/main/java/com/loki/lochat/channel/Channel.java b/src/main/java/com/loki/lochat/channel/Channel.java new file mode 100644 index 0000000..1b112a7 --- /dev/null +++ b/src/main/java/com/loki/lochat/channel/Channel.java @@ -0,0 +1,33 @@ +package com.loki.lochat.channel; + +import java.util.List; + +/** + * Канал чата (global, local, staff, trade...). + * Иммутабельное описание канала из config/channels.yml. + */ +public record Channel( + String id, + String display, + String prefix, + String color, + int radius, + int cooldown, + String permission, + List aliases) { + + /** radius < 1 — глобальный канал (видны всем игрокам) */ + public boolean isGlobalScope() { + return radius < 1; + } + + /** radius > 0 — локальный канал с радиусом действия */ + public boolean isLocalScope() { + return radius > 0; + } + + /** Есть ли требование по праву для входа/использования */ + public boolean hasPermission() { + return permission != null && !permission.isBlank(); + } +} \ No newline at end of file diff --git a/src/main/java/com/loki/lochat/commands/CommandManager.java b/src/main/java/com/loki/lochat/commands/CommandManager.java index cffed59..998bdfd 100644 --- a/src/main/java/com/loki/lochat/commands/CommandManager.java +++ b/src/main/java/com/loki/lochat/commands/CommandManager.java @@ -8,7 +8,10 @@ import com.loki.lochat.commands.admin.chat.ClearChatCommand; import com.loki.lochat.commands.admin.chat.ClearChatConfigCommand; import com.loki.lochat.commands.admin.system.LoChatCommand; import com.loki.lochat.commands.admin.system.ReloadConfigCommand; +import com.loki.lochat.commands.chat.ChannelCommand; import com.loki.lochat.commands.chat.GlobalChatCommand; +import com.loki.lochat.commands.chat.JoinChannelCommand; +import com.loki.lochat.commands.chat.LeaveChannelCommand; import com.loki.lochat.commands.chat.LocalChatCommand; import com.loki.lochat.commands.messaging.IgnoreCommand; import com.loki.lochat.commands.messaging.IgnoreListCommand; @@ -54,6 +57,11 @@ public class CommandManager { reg("g", new GlobalChatCommand(plugin)); reg("l", new LocalChatCommand(plugin)); + // ── Каналы ───────────────────────────────────────────────────────────── + regTab("join", new JoinChannelCommand(plugin)); + reg("leave", new LeaveChannelCommand(plugin)); + regTab("channel", new ChannelCommand(plugin)); + // ── Личные сообщения / игнор ────────────────────────────────────────── reg("msg", new MsgCommand(plugin)); reg("reply", new ReplyCommand(plugin)); diff --git a/src/main/java/com/loki/lochat/commands/chat/ChannelCommand.java b/src/main/java/com/loki/lochat/commands/chat/ChannelCommand.java new file mode 100644 index 0000000..90982d9 --- /dev/null +++ b/src/main/java/com/loki/lochat/commands/chat/ChannelCommand.java @@ -0,0 +1,120 @@ +package com.loki.lochat.commands.chat; + +import com.loki.lochat.LoChat; +import com.loki.lochat.api.service.ChannelService; +import com.loki.lochat.channel.Channel; +import com.loki.lochat.commands.base.PlayerCommand; +import com.loki.lochat.utils.format.ChatFormatter; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Locale; + +/** + * /channel — инфо о текущем канале со списком доступных. + * /channel <канал> — переключиться. /channel off — выйти. + * /channel list — список каналов. + */ +public class ChannelCommand extends PlayerCommand { + + public ChannelCommand(LoChat plugin) { + super(plugin); + } + + @Override + protected boolean executePlayerCommand(@NotNull Player player, @NotNull Command command, + @NotNull String label, @NotNull String[] args) { + ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class); + if (!channels.isEnabled()) { + player.sendMessage(ChatFormatter.parse("&#CF6679Система каналов отключена")); + return true; + } + + if (args.length == 0) { + showCurrent(player, channels); + return true; + } + + switch (args[0].toLowerCase(Locale.ROOT)) { + case "off", "leave" -> { + if (channels.isInChannel(player)) { + channels.leave(player); + player.sendMessage(ChatFormatter.parse("⚖C9Вы вышли из канала")); + } else { + player.sendMessage(ChatFormatter.parse("&#B798A8Вы не находитесь в канале")); + } + } + case "list" -> listChannels(player, channels); + default -> joinChannel(player, channels, args[0]); + } + return true; + } + + private void showCurrent(Player player, ChannelService channels) { + Channel active = channels.getActive(player).orElse(null); + if (active == null) { + Channel def = channels.getDefault(); + player.sendMessage(ChatFormatter.parse("&#B798A8Вы в дефолтном канале [ẲE9" + + (def == null ? "?" : def.display() + "&8 " + def.prefix()) + "&#B798A8]")); + } else { + player.sendMessage(ChatFormatter.parse("⚖C9Вы в канале [ẲE9" + + active.display() + "&8 " + active.prefix() + "⚖C9]")); + } + player.sendMessage(ChatFormatter.parse("&#B798A8/join <канал> ⚖C9— войти, " + + "&#B798A8/channel list ⚖C9— все каналы")); + } + + private void listChannels(Player player, ChannelService channels) { + player.sendMessage(ChatFormatter.parse("&#B798A8Доступные каналы:")); + for (Channel c : channels.getChannels()) { + boolean canUse = channels.canUse(player, c); + boolean active = c.equals(channels.getActive(player).orElse(null)); + player.sendMessage(ChatFormatter.parse((canUse ? "⚖C9" : "&#CF6679") + + (active ? "\u25B8 " : "\u25AA ") + c.display() + "&8 " + c.prefix() + + (canUse ? "" : " &#CF6679(нет прав)") + + " &#B798A8— " + (c.isGlobalScope() ? "глобальный" : "локальный, " + c.radius() + "б"))); + } + } + + private void joinChannel(Player player, ChannelService channels, String name) { + Channel channel = channels.find(name); + if (channel == null) { + player.sendMessage(ChatFormatter.parse("&#CF6679Канал ẲE9" + name + " &#CF6679не найден")); + return; + } + if (!channels.canUse(player, channel)) { + player.sendMessage(ChatFormatter.parse( + plugin.getConfigManager().getMessagesConfig().getNoPermission())); + return; + } + Channel active = channels.getActive(player).orElse(null); + channels.join(player, channel); + if (channel.equals(active)) { + player.sendMessage(ChatFormatter.parse("&#B798A8Вы уже в канале [ẲE9" + + channel.display() + "&8 " + channel.prefix() + "&#B798A8]")); + } else { + player.sendMessage(ChatFormatter.parse("⚖C9Вы вошли в канал [ẲE9" + + channel.display() + "&8 " + channel.prefix() + "⚖C9]")); + } + } + + @Override + protected @Nullable List getTabCompletions(@NotNull CommandSender sender, @NotNull Command command, + @NotNull String alias, @NotNull String[] args) { + if (args.length != 1) { + return null; + } + ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class); + String input = args[0].toLowerCase(Locale.ROOT); + return java.util.stream.Stream.concat( + java.util.stream.Stream.of("list", "off"), + channels.getChannels().stream().map(Channel::id)) + .filter(s -> s.startsWith(input)) + .toList(); + } +} \ No newline at end of file diff --git a/src/main/java/com/loki/lochat/commands/chat/JoinChannelCommand.java b/src/main/java/com/loki/lochat/commands/chat/JoinChannelCommand.java new file mode 100644 index 0000000..bd2c3af --- /dev/null +++ b/src/main/java/com/loki/lochat/commands/chat/JoinChannelCommand.java @@ -0,0 +1,80 @@ +package com.loki.lochat.commands.chat; + +import com.loki.lochat.LoChat; +import com.loki.lochat.api.service.ChannelService; +import com.loki.lochat.channel.Channel; +import com.loki.lochat.commands.base.PlayerCommand; +import com.loki.lochat.utils.format.ChatFormatter; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Locale; + +/** + * /join <канал|алиас> — войти в канал чата. + */ +public class JoinChannelCommand extends PlayerCommand { + + public JoinChannelCommand(LoChat plugin) { + super(plugin); + } + + @Override + protected boolean executePlayerCommand(@NotNull Player player, @NotNull Command command, + @NotNull String label, @NotNull String[] args) { + if (!plugin.getConfigManager().getChannelConfig().isEnabled()) { + player.sendMessage(ChatFormatter.parse("&#CF6679Система каналов отключена")); + return true; + } + if (args.length == 0) { + player.sendMessage(ChatFormatter.parse("&#CF6679Использование: ẲE9/join <канал>")); + return true; + } + + ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class); + Channel channel = channels.find(args[0]); + if (channel == null) { + player.sendMessage(ChatFormatter.parse("&#CF6679Канал ẲE9" + args[0] + " &#CF6679не найден")); + return true; + } + if (!channels.canUse(player, channel)) { + player.sendMessage(ChatFormatter.parse( + plugin.getConfigManager().getMessagesConfig().getNoPermission())); + return true; + } + + Channel active = channels.getActive(player).orElse(null); + channels.join(player, channel); + if (channel.equals(channels.getDefault())) { + player.sendMessage(ChatFormatter.parse("⚖C9Вы в канале [ẲE9" + + channel.display() + "&8 " + channel.prefix() + "⚖C9] (дефолтный)")); + } else if (channel.equals(active)) { + player.sendMessage(ChatFormatter.parse("&#B798A8Вы уже в канале [ẲE9" + + channel.display() + "&8 " + channel.prefix() + "&#B798A8]")); + } else { + player.sendMessage(ChatFormatter.parse("⚖C9Вы вошли в канал [ẲE9" + + channel.display() + "&8 " + channel.prefix() + "⚖C9]")); + } + return true; + } + + @Override + protected @Nullable List getTabCompletions(@NotNull CommandSender sender, @NotNull Command command, + @NotNull String alias, @NotNull String[] args) { + if (args.length != 1) { + return null; + } + ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class); + String input = args[0].toLowerCase(Locale.ROOT); + return channels.getChannels().stream() + .filter(c -> !(sender instanceof Player p) || channels.canUse(p, c)) + .map(Channel::id) + .filter(id -> id.toLowerCase(Locale.ROOT).startsWith(input)) + .toList(); + } +} \ No newline at end of file diff --git a/src/main/java/com/loki/lochat/commands/chat/LeaveChannelCommand.java b/src/main/java/com/loki/lochat/commands/chat/LeaveChannelCommand.java new file mode 100644 index 0000000..83942f5 --- /dev/null +++ b/src/main/java/com/loki/lochat/commands/chat/LeaveChannelCommand.java @@ -0,0 +1,37 @@ +package com.loki.lochat.commands.chat; + +import com.loki.lochat.LoChat; +import com.loki.lochat.api.service.ChannelService; +import com.loki.lochat.commands.base.PlayerCommand; +import com.loki.lochat.utils.format.ChatFormatter; + +import org.bukkit.command.Command; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +/** + * /leave — выйти из текущего канала (вернуться к дефолтному чату). + */ +public class LeaveChannelCommand extends PlayerCommand { + + public LeaveChannelCommand(LoChat plugin) { + super(plugin); + } + + @Override + protected boolean executePlayerCommand(@NotNull Player player, @NotNull Command command, + @NotNull String label, @NotNull String[] args) { + ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class); + if (!channels.isEnabled()) { + player.sendMessage(ChatFormatter.parse("&#CF6679Система каналов отключена")); + return true; + } + if (!channels.isInChannel(player)) { + player.sendMessage(ChatFormatter.parse("&#B798A8Вы не находитесь в канале")); + return true; + } + channels.leave(player); + player.sendMessage(ChatFormatter.parse("⚖C9Вы вышли из канала")); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/loki/lochat/config/ChannelConfig.java b/src/main/java/com/loki/lochat/config/ChannelConfig.java new file mode 100644 index 0000000..682eee3 --- /dev/null +++ b/src/main/java/com/loki/lochat/config/ChannelConfig.java @@ -0,0 +1,99 @@ +package com.loki.lochat.config; + +import com.loki.lochat.channel.Channel; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Конфигурация каналов чата (config/channels.yml). + */ +public class ChannelConfig extends BaseConfig { + + private volatile Map channels = new LinkedHashMap<>(); + private volatile Map byAlias = new LinkedHashMap<>(); + private volatile Channel defaultChannel; + + public ChannelConfig(JavaPlugin plugin) { + super(plugin, "channels.yml", true); + } + + @Override + protected void onLoad() { + loadChannels(); + buildAliasIndex(); + } + + private void loadChannels() { + Map result = new LinkedHashMap<>(); + ConfigurationSection section = getConfig().getConfigurationSection("channels.list"); + if (section != null) { + for (String id : section.getKeys(false)) { + String path = "channels.list." + id; + result.put(id, new Channel( + id, + getConfig().getString(path + ".display", ""), + getConfig().getString(path + ".prefix", id.toUpperCase(Locale.ROOT)), + getConfig().getString(path + ".color", "&7"), + getConfig().getInt(path + ".radius", -1), + getConfig().getInt(path + ".cooldown", 0), + getConfig().getString(path + ".permission", ""), + getConfig().getStringList(path + ".aliases"))); + } + } + this.channels = result; + } + + private void buildAliasIndex() { + Map index = new LinkedHashMap<>(); + for (Channel channel : channels.values()) { + index.put(channel.id().toLowerCase(Locale.ROOT), channel); + for (String alias : channel.aliases()) { + if (alias == null || alias.isBlank()) { + continue; + } + index.putIfAbsent(alias.toLowerCase(Locale.ROOT), channel); + } + } + this.byAlias = index; + + String defaultId = getConfig().getString("channels.default", "global"); + this.defaultChannel = channels.get(defaultId); + if (defaultChannel == null) { + this.defaultChannel = channels.isEmpty() ? null : channels.values().iterator().next(); + } + } + + public boolean isEnabled() { + return getConfig().getBoolean("channels.enabled", true); + } + + public Channel getDefault() { + return defaultChannel; + } + + public Channel get(String id) { + return id == null ? null : channels.get(id.toLowerCase(Locale.ROOT)); + } + + /** + * Поиск канала по имени или алиасу (без учёта регистра). + */ + public Channel find(String name) { + return name == null ? null : byAlias.get(name.toLowerCase(Locale.ROOT)); + } + + public Collection getChannels() { + return channels.values(); + } + + public List getChannelList() { + return List.copyOf(channels.values()); + } +} \ No newline at end of file diff --git a/src/main/java/com/loki/lochat/config/ConfigManager.java b/src/main/java/com/loki/lochat/config/ConfigManager.java index b69083b..3454ecb 100644 --- a/src/main/java/com/loki/lochat/config/ConfigManager.java +++ b/src/main/java/com/loki/lochat/config/ConfigManager.java @@ -21,6 +21,7 @@ public class ConfigManager { private volatile MuteConfig muteConfig; private volatile FiltersConfig filtersConfig; private volatile SoundsConfig soundsConfig; + private volatile ChannelConfig channelConfig; public ConfigManager(LoChat plugin) { this.plugin = plugin; @@ -33,6 +34,7 @@ public class ConfigManager { this.muteConfig = new MuteConfig(plugin); this.filtersConfig = new FiltersConfig(plugin); this.soundsConfig = new SoundsConfig(plugin); + this.channelConfig = new ChannelConfig(plugin); // Инициализируем все конфиги this.appearanceConfig.init(); @@ -40,6 +42,7 @@ public class ConfigManager { this.muteConfig.init(); this.filtersConfig.init(); this.soundsConfig.init(); + this.channelConfig.init(); loader.updateConfig(); } @@ -66,6 +69,10 @@ public class ConfigManager { return soundsConfig; } + public ChannelConfig getChannelConfig() { + return channelConfig; + } + // ========== Обновление и перезагрузка ========== public synchronized void reload() { @@ -77,6 +84,7 @@ public class ConfigManager { this.muteConfig.reload(); this.filtersConfig.reload(); this.soundsConfig.reload(); + this.channelConfig.reload(); plugin.getLogger().info("Все конфигурации успешно перезагружены"); } diff --git a/src/main/java/com/loki/lochat/core/PluginInitializer.java b/src/main/java/com/loki/lochat/core/PluginInitializer.java index 0d24852..81d275f 100644 --- a/src/main/java/com/loki/lochat/core/PluginInitializer.java +++ b/src/main/java/com/loki/lochat/core/PluginInitializer.java @@ -109,7 +109,8 @@ public class PluginInitializer { plugin.setChatFilter(filter); plugin.getServer().getPluginManager().registerEvents( - new ChatEventListener(plugin, messageService, playerService, filter), plugin + new ChatEventListener(plugin, messageService, playerService, filter, + serviceRegistry.get(com.loki.lochat.api.service.ChannelService.class)), plugin ); plugin.getServer().getPluginManager().registerEvents( new PlayerEventListener(plugin, playerService, messagingService, nickService, filter), plugin 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 882a8c3..7f3ca47 100644 --- a/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java +++ b/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java @@ -1,6 +1,7 @@ package com.loki.lochat.core.registry; import com.loki.lochat.LoChat; +import com.loki.lochat.api.service.ChannelService; import com.loki.lochat.api.service.ChatService; import com.loki.lochat.api.service.MentionService; import com.loki.lochat.api.service.MessageService; @@ -14,6 +15,7 @@ import com.loki.lochat.config.ConfigManager; import com.loki.lochat.config.MessageConfig; import com.loki.lochat.core.factory.ServiceFactory; import com.loki.lochat.core.service.ChatServiceImpl; +import com.loki.lochat.core.service.ChannelServiceImpl; import com.loki.lochat.core.service.MentionServiceImpl; import com.loki.lochat.core.service.MessageServiceImpl; import com.loki.lochat.core.service.NickServiceImpl; @@ -47,6 +49,8 @@ public class ServiceRegistry { PlayerService playerService = new PlayerServiceImpl(plugin); register(PlayerService.class, playerService); + register(ChannelService.class, new ChannelServiceImpl(configManager.getChannelConfig())); + MessageService messageService = new MessageServiceImpl(plugin, muteService, playerService); register(MessageService.class, messageService); diff --git a/src/main/java/com/loki/lochat/core/service/ChannelServiceImpl.java b/src/main/java/com/loki/lochat/core/service/ChannelServiceImpl.java new file mode 100644 index 0000000..5dec150 --- /dev/null +++ b/src/main/java/com/loki/lochat/core/service/ChannelServiceImpl.java @@ -0,0 +1,85 @@ +package com.loki.lochat.core.service; + +import com.loki.lochat.api.service.ChannelService; +import com.loki.lochat.channel.Channel; +import com.loki.lochat.config.ChannelConfig; + +import org.bukkit.entity.Player; + +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Реализация сервиса каналов чата. + * Хранит активный (присоединённый) канал каждого игрока в памяти. + */ +public class ChannelServiceImpl implements ChannelService { + + private final ChannelConfig config; + private final Map activeChannels = new ConcurrentHashMap<>(); + + public ChannelServiceImpl(ChannelConfig config) { + this.config = config; + } + + @Override + public boolean isEnabled() { + return config.isEnabled(); + } + + @Override + public Channel getDefault() { + return config.getDefault(); + } + + @Override + public Collection getChannels() { + return config.getChannels(); + } + + @Override + public Channel find(String nameOrAlias) { + return config.find(nameOrAlias); + } + + @Override + public Optional getActive(Player player) { + String id = activeChannels.get(player.getUniqueId()); + if (id == null) { + return Optional.empty(); + } + return Optional.ofNullable(config.get(id)); + } + + @Override + public boolean isInChannel(Player player) { + return activeChannels.containsKey(player.getUniqueId()); + } + + @Override + public boolean canUse(Player player, Channel channel) { + return !channel.hasPermission() || player.hasPermission(channel.permission()); + } + + @Override + public boolean join(Player player, Channel channel) { + if (channel == null || !canUse(player, channel)) { + return false; + } + Channel defaultChannel = config.getDefault(); + if (defaultChannel != null && channel.id().equals(defaultChannel.id())) { + activeChannels.remove(player.getUniqueId()); + return true; + } + activeChannels.put(player.getUniqueId(), channel.id()); + return true; + } + + @Override + public void leave(Player player) { + activeChannels.remove(player.getUniqueId()); + } +} \ No newline at end of file diff --git a/src/main/java/com/loki/lochat/listener/ChatEventListener.java b/src/main/java/com/loki/lochat/listener/ChatEventListener.java index 23e4483..95df000 100644 --- a/src/main/java/com/loki/lochat/listener/ChatEventListener.java +++ b/src/main/java/com/loki/lochat/listener/ChatEventListener.java @@ -1,8 +1,10 @@ package com.loki.lochat.listener; import com.loki.lochat.LoChat; +import com.loki.lochat.api.service.ChannelService; import com.loki.lochat.api.service.MessageService; import com.loki.lochat.api.service.PlayerService; +import com.loki.lochat.channel.Channel; import com.loki.lochat.core.filter.AdvancedMessageFilter; import com.loki.lochat.core.filter.FilterResult; import com.loki.lochat.renderer.EnhancedChatRenderer; @@ -24,13 +26,15 @@ public class ChatEventListener implements Listener { private final MessageService messageService; private final PlayerService playerService; private final AdvancedMessageFilter advancedFilter; + private final ChannelService channelService; public ChatEventListener(JavaPlugin plugin, MessageService messageService, PlayerService playerService, - AdvancedMessageFilter filter) { + AdvancedMessageFilter filter, ChannelService channelService) { this.plugin = plugin; this.messageService = messageService; this.playerService = playerService; this.advancedFilter = filter; + this.channelService = channelService; } @EventHandler(priority = EventPriority.HIGHEST) @@ -39,6 +43,13 @@ public class ChatEventListener implements Listener { String rawMessage = PlainTextComponentSerializer.plainText().serialize(event.message()); LoChat loChat = (LoChat) plugin; + // Система каналов: если игрок в присоединённом канале — маршрутизируем туда + Channel active = channelService.isEnabled() ? channelService.getActive(sender).orElse(null) : null; + if (active != null) { + handleChannelChat(event, sender, active, rawMessage); + return; + } + // Определяем режим чата (глобальный/локальный) ChatMode mode = determineChatMode(loChat, rawMessage); if (mode == null) { @@ -66,6 +77,52 @@ public class ChatEventListener implements Listener { recordStatistics(loChat, sender, isGlobal); } + /** + * Маршрутизация сообщения в присоединённый (не-дефолтный) канал. + */ + private void handleChannelChat(AsyncChatEvent event, Player sender, Channel channel, String rawMessage) { + String plainMessage = rawMessage.stripLeading(); + + // Кулдаун канала + int cooldown = channel.cooldown(); + if (cooldown > 0 && !sender.hasPermission("chat.bypass.cooldown")) { + String cooldownType = "channel_" + channel.id(); + if (playerService.isOnCooldown(sender.getUniqueId(), cooldownType, cooldown)) { + int remaining = playerService.getRemainingCooldown(sender.getUniqueId(), cooldownType, cooldown); + String msg = ((LoChat) plugin).getConfigManager().getMessagesConfig().getCooldownMessage() + .replace("{remaining}", String.valueOf(remaining)); + event.setCancelled(true); + sender.sendMessage(ChatFormatter.parse(msg)); + return; + } + playerService.setCooldown(sender.getUniqueId(), cooldownType); + } + + if (!processFilters(event, sender, plainMessage, channel.isGlobalScope())) { + return; + } + + // Радиус локального канала + if (channel.isLocalScope()) { + int radius = channel.radius(); + event.viewers().removeIf(v -> { + if (!(v instanceof Player p)) { + return false; + } + try { + return !com.loki.lochat.utils.player.DistanceUtil.isInRange(sender, p, radius); + } catch (Exception e) { + return false; + } + }); + } + + event.renderer(new EnhancedChatRenderer(plugin, channel)); + LoChat loChat = (LoChat) plugin; + loChat.getDiscordIntegration().sendChatMessage(sender, plainMessage, channel.isGlobalScope()); + playerService.recordMessage(sender.getUniqueId(), "channel_" + channel.id()); + } + private ChatMode determineChatMode(LoChat loChat, String rawMessage) { boolean localEnabled = loChat.getConfigManager().isLocalEnabled(); boolean isGlobal; diff --git a/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java b/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java index 9dd0ce2..bd2b2df 100644 --- a/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java +++ b/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java @@ -1,9 +1,11 @@ package com.loki.lochat.renderer; +import com.loki.lochat.channel.Channel; import com.loki.lochat.renderer.components.EmojiComponent; import com.loki.lochat.renderer.components.GradientBuilder; import com.loki.lochat.renderer.components.UrlProcessor; import com.loki.lochat.translate.TranslationService; +import com.loki.lochat.utils.format.ChatFormatter; import com.loki.lochat.utils.format.TextFormatter; import com.loki.lochat.utils.player.MentionHandler; @@ -33,12 +35,22 @@ public class EnhancedChatRenderer implements ChatRenderer { private final JavaPlugin plugin; private final boolean isGlobal; + private final Channel channel; private final MentionHandler mentionHandler; private final com.loki.lochat.config.ConfigManager cfg; public EnhancedChatRenderer(JavaPlugin plugin, boolean isGlobal) { + this(plugin, isGlobal, null); + } + + public EnhancedChatRenderer(JavaPlugin plugin, Channel channel) { + this(plugin, channel.isGlobalScope(), channel); + } + + private EnhancedChatRenderer(JavaPlugin plugin, boolean isGlobal, Channel channel) { this.plugin = plugin; this.isGlobal = isGlobal; + this.channel = channel; this.mentionHandler = new MentionHandler(plugin); com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; @@ -85,6 +97,26 @@ public class EnhancedChatRenderer implements ChatRenderer { "message", processed )); } + + private String emojiFor() { + if (channel != null) { + return channel.display() == null ? "" : channel.display(); + } + com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig(); + return isGlobal ? appearanceCfg.getGlobalEmoji() : appearanceCfg.getLocalEmoji(); + } + + private String prefixText() { + if (channel != null) { + return channel.prefix() == null ? channel.id() : channel.prefix(); + } + com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig(); + return isGlobal ? appearanceCfg.getGlobalText() : appearanceCfg.getLocalText(); + } + + private String channelColor() { + return channel != null && channel.color() != null ? channel.color() : "&7"; + } private Component buildFormattedMessage(String format, Map parts) { Component result = Component.empty(); @@ -112,8 +144,7 @@ public class EnhancedChatRenderer implements ChatRenderer { } private Component buildEmojiComponent() { - com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig(); - String emoji = isGlobal ? appearanceCfg.getGlobalEmoji() : appearanceCfg.getLocalEmoji(); + String emoji = emojiFor(); return emoji.isEmpty() ? Component.empty() : Component.text(emoji); } @@ -146,9 +177,11 @@ public class EnhancedChatRenderer implements ChatRenderer { } private Component buildPrefix() { + String text = prefixText(); + if (channel != null) { + return ChatFormatter.parse(channelColor() + text); + } com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig(); - - String text = isGlobal ? appearanceCfg.getGlobalText() : appearanceCfg.getLocalText(); List colors = isGlobal ? appearanceCfg.getGlobalColors() : appearanceCfg.getLocalColors(); return GradientBuilder.build(text, colors); @@ -157,7 +190,9 @@ public class EnhancedChatRenderer implements ChatRenderer { private Component buildSeparator() { com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig(); String text = isGlobal ? appearanceCfg.getGlobalSeparatorText() : appearanceCfg.getLocalSeparatorText(); - String color = isGlobal ? appearanceCfg.getGlobalSeparatorColor() : appearanceCfg.getLocalSeparatorColor(); + String color = channel != null + ? channelColor() + : (isGlobal ? appearanceCfg.getGlobalSeparatorColor() : appearanceCfg.getLocalSeparatorColor()); return Component.text(text, parseColor(color)); } diff --git a/src/main/resources/config/channels.yml b/src/main/resources/config/channels.yml new file mode 100644 index 0000000..99a1bfd --- /dev/null +++ b/src/main/resources/config/channels.yml @@ -0,0 +1,57 @@ +# LoChat channels +# Channels let players choose where their chat messages go. +# /join — join a channel +# /leave — leave the current channel (back to default chat) +# /channel [name|list|off] — view info, switch, or list channels +# +# Per channel: +# display — icon shown before the channel name in messages +# prefix — channel label +# color — color of the prefix (legacy & codes or &#RRGGBB) +# radius -1 = global (all players), >0 = local radius in blocks +# cooldown seconds between messages in this channel (0 = off) +# permission required to join/use the channel (empty = everyone) +# aliases alternative names to reference the channel + +channels: + # Master switch for the channel system + enabled: true + # Default channel id: when a player is not in a joined channel, they fall back to it + default: "global" + + list: + global: + display: "\u2726" + prefix: "GLOBAL" + color: "&b" + radius: -1 + cooldown: 3 + permission: "" + aliases: [ g ] + + local: + display: "\u25C6" + prefix: "LOCAL" + color: "&d" + radius: 100 + cooldown: 1 + permission: "" + aliases: [ l ] + + staff: + display: "\u2694" + prefix: "STAFF" + color: "&6" + radius: -1 + cooldown: 2 + permission: "lochat.channel.staff" + aliases: [ s, staffchat ] + + trade: + display: "\u27A4" + prefix: "TRADE" + color: "&e" + radius: -1 + cooldown: 5 + permission: "lochat.channel.trade" + aliases: [ t, tradechat ] \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 38b6a7e..080a702 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -20,6 +20,24 @@ commands: permission: chat.global.use aliases: [ п ] + join: + description: Join a chat channel + usage: /join + permission: chat.channel.join + aliases: [ войти ] + + leave: + description: Leave the current chat channel + usage: /leave + permission: chat.channel.leave + aliases: [ выйти ] + + channel: + description: View or switch chat channel + usage: /channel [channel|list|off] + permission: chat.channel.use + aliases: [ ch, канал, channels, каналы ] + globalchat: description: Toggle global chat usage: /globalchat toggle @@ -225,6 +243,21 @@ commands: permissions: chat.global.use: default: true + chat.channel.use: + description: Can use /channel + default: true + chat.channel.join: + description: Can use /join + default: true + chat.channel.leave: + description: Can use /leave + default: true + lochat.channel.staff: + description: Access to the staff channel + default: op + lochat.channel.trade: + description: Access to the trade channel + default: op chat.global.toggle: default: true chat.global.color: diff --git a/src/test/java/com/loki/lochat/core/service/ChannelServiceImplTest.java b/src/test/java/com/loki/lochat/core/service/ChannelServiceImplTest.java new file mode 100644 index 0000000..01e3601 --- /dev/null +++ b/src/test/java/com/loki/lochat/core/service/ChannelServiceImplTest.java @@ -0,0 +1,111 @@ +package com.loki.lochat.core.service; + +import com.loki.lochat.api.service.ChannelService; +import com.loki.lochat.channel.Channel; +import com.loki.lochat.config.ChannelConfig; + +import org.bukkit.entity.Player; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ChannelServiceImplTest { + + private final Channel global = new Channel("global", "\u2726", "GLOBAL", "&b", -1, 3, "", List.of("g")); + private final Channel staff = new Channel("staff", "\u2694", "STAFF", "&6", -1, 2, + "lochat.channel.staff", List.of("s")); + + private Player player() { + Player player = mock(Player.class); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + return player; + } + + private ChannelService createService() { + ChannelConfig config = mock(ChannelConfig.class); + when(config.isEnabled()).thenReturn(true); + when(config.getDefault()).thenReturn(global); + when(config.get("global")).thenReturn(global); + when(config.get("staff")).thenReturn(staff); + when(config.find("global")).thenReturn(global); + when(config.find("staff")).thenReturn(staff); + when(config.find("s")).thenReturn(staff); + when(config.getChannels()).thenReturn(List.of(global, staff)); + return new ChannelServiceImpl(config); + } + + @Test + void getActive_emptyByDefault() { + Player player = player(); + ChannelService service = createService(); + assertTrue(service.getActive(player).isEmpty()); + assertFalse(service.isInChannel(player)); + } + + @Test + void join_nonDefaultChannel() { + Player player = player(); + when(player.hasPermission("lochat.channel.staff")).thenReturn(true); + ChannelService service = createService(); + assertTrue(service.join(player, staff)); + Optional active = service.getActive(player); + assertTrue(active.isPresent()); + assertEquals("staff", active.get().id()); + assertTrue(service.isInChannel(player)); + } + + @Test + void join_defaultChannelMeansNotJoined() { + Player player = player(); + ChannelService service = createService(); + assertTrue(service.join(player, global)); + assertTrue(service.getActive(player).isEmpty()); + assertFalse(service.isInChannel(player)); + } + + @Test + void join_withoutPermissionFails() { + Player player = player(); + when(player.hasPermission("lochat.channel.staff")).thenReturn(false); + ChannelService service = createService(); + assertFalse(service.join(player, staff)); + assertFalse(service.isInChannel(player)); + } + + @Test + void join_withPermission() { + Player player = player(); + when(player.hasPermission("lochat.channel.staff")).thenReturn(true); + ChannelService service = createService(); + assertTrue(service.join(player, staff)); + assertEquals("staff", service.getActive(player).orElseThrow().id()); + } + + @Test + void leave_clearsActiveChannel() { + Player player = player(); + when(player.hasPermission("lochat.channel.staff")).thenReturn(true); + ChannelService service = createService(); + service.join(player, staff); + service.leave(player); + assertFalse(service.isInChannel(player)); + assertTrue(service.getActive(player).isEmpty()); + } + + @Test + void find_byAlias() { + ChannelService service = createService(); + Channel byId = service.find("staff"); + Channel byAlias = service.find("s"); + assertEquals("staff", byId.id()); + assertEquals("staff", byAlias.id()); + } +} \ No newline at end of file From dd725ad255ca879bd655ce78bd83e7e61d22ed08 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 20 Sep 2026 16:18:00 +0200 Subject: [PATCH 05/10] port: build for spigot 1.20.1 (universal jar) - replace paper-api with spigot-api 1.20.1, add spigot snapshots repo - bundle Adventure 4.17.0 + platform-bukkit 4.3.4 via shadowJar with relocate to com.loki.lochat.libs.kyori/gson and mergeServiceFiles - version 1.5.5-1.20.1, api-version 1.20, remove folia-supported, run-paper - single universal jar loads on Spigot/Mohist 1.20.1 and Paper 1.20.x-1.21.x (Paper is superset) - CI: add spigot-1.20.1 branch --- .github/workflows/build.yml | 2 +- build.gradle.kts | 34 ++++++++++++++++++++++++++-------- src/main/resources/plugin.yml | 7 +++---- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3022fbd..84d410c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ name: Build & Test on: push: - branches: [main, develop] + branches: [main, develop, spigot-1.20.1] pull_request: branches: [main] diff --git a/build.gradle.kts b/build.gradle.kts index 6c5d9b8..3f97440 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,16 +1,20 @@ plugins { java - id("xyz.jpenilla.run-paper") version "2.3.1" + id("com.gradleup.shadow") version "9.0.0" jacoco checkstyle } group = "com.loki" -version = "1.5.5-1.21.x" +// Universal jar: compiled against Spigot 1.20.1 (lowest common denominator). +// Single jar loads on both Spigot/Mohist 1.20.1 and Paper 1.20.x-1.21.x +// because Paper is a superset of Spigot API. Adventure is bundled & relocated. +version = "1.5.5-1.20.1" repositories { mavenCentral() maven("https://repo.papermc.io/repository/maven-public/") + maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") maven("https://repo.extendedclip.com/content/repositories/placeholderapi/") maven("https://repo.rosewooddev.io/repository/public/") maven("https://jitpack.io") @@ -18,16 +22,23 @@ repositories { } dependencies { - compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") + compileOnly("org.spigotmc:spigot-api:1.20.1-R0.1-SNAPSHOT") compileOnly("me.clip:placeholderapi:2.11.6") compileOnly("org.black_ixx:playerpoints:3.2.7") compileOnly("net.luckperms:api:5.4") implementation("com.google.code.gson:gson:2.10.1") + // Adventure bundled & relocated -> works on Spigot (no server Adventure) and Paper (relocated avoids clash) + implementation("net.kyori:adventure-api:4.17.0") + implementation("net.kyori:adventure-text-minimessage:4.17.0") + implementation("net.kyori:adventure-text-serializer-legacy:4.17.0") + implementation("net.kyori:adventure-text-serializer-plain:4.17.0") + implementation("net.kyori:adventure-text-serializer-gson:4.17.0") + implementation("net.kyori:adventure-platform-bukkit:4.3.4") testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.0") testImplementation("org.mockito:mockito-core:5.11.0") - testImplementation("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") + testImplementation("org.spigotmc:spigot-api:1.20.1-R0.1-SNAPSHOT") testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.0") } @@ -79,12 +90,19 @@ jacoco { } tasks.jar { + archiveClassifier.set("plain") archiveFileName.set("LoChat-${project.version}.jar") - from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) duplicatesStrategy = DuplicatesStrategy.EXCLUDE } -// Локальный Paper для тестов: .\gradlew.bat runServer -tasks.runServer { - minecraftVersion("1.21.4") // lowest supported; runs on any 1.21.x Paper server +tasks.shadowJar { + archiveClassifier.set("") + archiveFileName.set("LoChat-${project.version}.jar") + mergeServiceFiles() + relocate("net.kyori", "com.loki.lochat.libs.kyori") + relocate("com.google.gson", "com.loki.lochat.libs.gson") + // do NOT minimize(): strips ServiceLoader classes Adventure needs +} +tasks.build { + dependsOn(tasks.shadowJar) } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 080a702..82393c9 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,10 +1,9 @@ name: LoChat -version: '1.5.5-1.21' +version: '1.5.5-1.20.1' main: com.loki.lochat.LoChat -api-version: '1.21' -folia-supported: true +api-version: '1.20' author: loki -description: Advanced chat plugin for Paper/Folia 1.21 — Spacelegacy edition +description: Advanced chat plugin for Spigot/Mohist 1.20.1 and Paper 1.20.x-1.21.x — Spacelegacy edition (universal jar) softdepend: - PlaceholderAPI From 88e4e68cca464d1cc32d48c47ba483a4c0e40395 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 20 Sep 2026 16:18:40 +0200 Subject: [PATCH 06/10] port: compat layer - add compat/Adventure (BukkitAudiences lifecycle with legacy fallback) - add compat/Msg (send/broadcast/actionBar/title/legacy/plain/displayName/playerListName/kick/itemMeta/inventoryTitle) - wire Adventure.init/close in LoChat onEnable/onDisable - remove unused reflection-based utils/platform/AdventureManager --- src/main/java/com/loki/lochat/LoChat.java | 4 +- .../com/loki/lochat/compat/Adventure.java | 50 +++++ src/main/java/com/loki/lochat/compat/Msg.java | 174 ++++++++++++++++++ .../utils/platform/AdventureManager.java | 95 ---------- 4 files changed, 227 insertions(+), 96 deletions(-) create mode 100644 src/main/java/com/loki/lochat/compat/Adventure.java create mode 100644 src/main/java/com/loki/lochat/compat/Msg.java delete mode 100644 src/main/java/com/loki/lochat/utils/platform/AdventureManager.java diff --git a/src/main/java/com/loki/lochat/LoChat.java b/src/main/java/com/loki/lochat/LoChat.java index 016cdb4..90115f2 100644 --- a/src/main/java/com/loki/lochat/LoChat.java +++ b/src/main/java/com/loki/lochat/LoChat.java @@ -47,7 +47,8 @@ public final class LoChat extends JavaPlugin { @Override public void onEnable() { instance = this; - getLogger().info("Загрузка LoChat v" + getPluginMeta().getVersion() + " для Paper/Folia " + Bukkit.getMinecraftVersion() + "..."); + com.loki.lochat.compat.Adventure.init(this); + getLogger().info("Загрузка LoChat v" + getDescription().getVersion() + " для Spigot " + Bukkit.getBukkitVersion() + "..."); // Инициализация initializer = new PluginInitializer(this); @@ -95,6 +96,7 @@ public final class LoChat extends JavaPlugin { @Override public void onDisable() { shutdown.shutdown(serviceRegistry, autoMessageManager, discordIntegration, gradientModule); + com.loki.lochat.compat.Adventure.close(); } public void reload() { diff --git a/src/main/java/com/loki/lochat/compat/Adventure.java b/src/main/java/com/loki/lochat/compat/Adventure.java new file mode 100644 index 0000000..a5cc300 --- /dev/null +++ b/src/main/java/com/loki/lochat/compat/Adventure.java @@ -0,0 +1,50 @@ +package com.loki.lochat.compat; + +import net.kyori.adventure.platform.bukkit.BukkitAudiences; + +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.Nullable; + +/** + * Holds BukkitAudiences for Spigot. Falls back to legacy text if creation fails. + */ +public final class Adventure { + + private static BukkitAudiences audiences; + private static boolean fallback; + + private Adventure() { + } + + public static void init(Plugin plugin) { + try { + audiences = BukkitAudiences.create(plugin); + fallback = false; + } catch (Throwable t) { + audiences = null; + fallback = true; + plugin.getLogger().warning( + "[LoChat] BukkitAudiences init failed, falling back to legacy text: " + t.getMessage()); + } + } + + public static void close() { + if (audiences != null) { + try { + audiences.close(); + } catch (Exception ignored) { + } + audiences = null; + } + fallback = false; + } + + @Nullable + public static BukkitAudiences audiences() { + return audiences; + } + + public static boolean isFallback() { + return fallback || audiences == null; + } +} diff --git a/src/main/java/com/loki/lochat/compat/Msg.java b/src/main/java/com/loki/lochat/compat/Msg.java new file mode 100644 index 0000000..155688b --- /dev/null +++ b/src/main/java/com/loki/lochat/compat/Msg.java @@ -0,0 +1,174 @@ +package com.loki.lochat.compat; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.kyori.adventure.title.Title; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.inventory.meta.ItemMeta; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; + +/** + * Static helpers for sending Adventure components on Spigot via BukkitAudiences. + * Falls back to legacy String when audiences not available. + */ +public final class Msg { + + private static final LegacyComponentSerializer LEGACY = LegacyComponentSerializer.builder() + .character('§') + .hexColors() + .useUnusualXRepeatedCharacterHexFormat() + .build(); + + private static final PlainTextComponentSerializer PLAIN = PlainTextComponentSerializer.plainText(); + + private Msg() { + } + + public static void send(CommandSender sender, Component component) { + if (sender == null || component == null) { + return; + } + if (!Adventure.isFallback() && Adventure.audiences() != null) { + try { + Adventure.audiences().sender(sender).sendMessage(component); + return; + } catch (Throwable ignored) { + } + } + sender.sendMessage(legacy(component)); + } + + public static void send(Collection senders, Component component) { + if (senders == null || component == null) { + return; + } + for (CommandSender s : senders) { + send(s, component); + } + } + + public static void broadcast(Component component) { + if (component == null) { + return; + } + if (!Adventure.isFallback() && Adventure.audiences() != null) { + try { + Adventure.audiences().all().sendMessage(component); + return; + } catch (Throwable ignored) { + } + } + String legacy = legacy(component); + Bukkit.getOnlinePlayers().forEach(p -> p.sendMessage(legacy)); + Bukkit.getConsoleSender().sendMessage(legacy); + } + + public static void actionBar(Player player, Component component) { + if (player == null || component == null) { + return; + } + if (!Adventure.isFallback() && Adventure.audiences() != null) { + try { + Adventure.audiences().player(player).sendActionBar(component); + return; + } catch (Throwable ignored) { + } + } + // Fallback: send as chat + player.sendMessage(legacy(component)); + } + + public static void title(Player player, Component title, Component subtitle, Title.Times times) { + if (player == null) { + return; + } + Component t = title != null ? title : Component.empty(); + Component s = subtitle != null ? subtitle : Component.empty(); + Title.Times tm = times != null ? times : Title.Times.times( + Duration.ofMillis(500), Duration.ofMillis(3000), Duration.ofMillis(500)); + if (!Adventure.isFallback() && Adventure.audiences() != null) { + try { + Adventure.audiences().player(player).showTitle(Title.title(t, s, tm)); + return; + } catch (Throwable ignored) { + } + } + // Fallback via Bukkit API (legacy strings, 1.20.1 supports titles) + player.sendTitle(legacy(t), legacy(s), + (int) (tm.fadeIn().toMillis() / 50), + (int) (tm.stay().toMillis() / 50), + (int) (tm.fadeOut().toMillis() / 50)); + } + + public static String legacy(Component component) { + if (component == null) { + return ""; + } + return LEGACY.serialize(component); + } + + public static String plain(Component component) { + if (component == null) { + return ""; + } + return PLAIN.serialize(component); + } + + public static void setDisplayName(Player player, Component component) { + if (player == null) { + return; + } + player.setDisplayName(component == null ? "" : legacy(component)); + } + + public static void setPlayerListName(Player player, Component component) { + if (player == null) { + return; + } + player.setPlayerListName(component == null ? "" : legacy(component)); + } + + public static void kick(Player player, Component reason) { + if (player == null) { + return; + } + String msg = reason == null ? "" : legacy(reason); + // Must be on main thread; caller should ensure via scheduler if needed + if (Bukkit.isPrimaryThread()) { + player.kickPlayer(msg); + } else { + Bukkit.getScheduler().runTask( + Bukkit.getPluginManager().getPlugin("LoChat"), () -> player.kickPlayer(msg)); + } + } + + public static void itemName(ItemMeta meta, Component name) { + if (meta == null) { + return; + } + meta.setDisplayName(name == null ? null : legacy(name)); + } + + public static void itemLore(ItemMeta meta, List lore) { + if (meta == null) { + return; + } + if (lore == null) { + meta.setLore(null); + return; + } + List lines = lore.stream().map(Msg::legacy).toList(); + meta.setLore(lines); + } + + public static String inventoryTitle(Component title) { + return legacy(title); + } +} diff --git a/src/main/java/com/loki/lochat/utils/platform/AdventureManager.java b/src/main/java/com/loki/lochat/utils/platform/AdventureManager.java deleted file mode 100644 index c860ebe..0000000 --- a/src/main/java/com/loki/lochat/utils/platform/AdventureManager.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.loki.lochat.utils.platform; - -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; - -/** - * Менеджер для работы с Adventure API на Arclight/Forge серверах - */ -public class AdventureManager { - - private AdventureManager() { - } - - private static boolean adventureAvailable = false; - private static Object audiences; - - public static void init(Plugin plugin) { - try { - // Проверяем наличие Adventure API - Class.forName("net.kyori.adventure.text.Component"); - Class audiencesClass = Class.forName("net.kyori.adventure.platform.bukkit.BukkitAudiences"); - - // Создаем BukkitAudiences через рефлексию - Object audiencesInstance = audiencesClass.getMethod("create", Plugin.class).invoke(null, plugin); - audiences = audiencesInstance; - adventureAvailable = true; - - plugin.getLogger().info("Adventure API обнаружен - используем нативную поддержку"); - } catch (Exception e) { - adventureAvailable = false; - plugin.getLogger().info("Adventure API не найден - используем legacy режим"); - } - } - - public static void shutdown() { - if (audiences != null && adventureAvailable) { - try { - audiences.getClass().getMethod("close").invoke(audiences); - } catch (Exception e) { - // Игнорируем ошибки при закрытии - } - audiences = null; - } - adventureAvailable = false; - } - - public static void sendMessage(CommandSender sender, Object message) { - if (adventureAvailable && audiences != null) { - try { - // Используем Adventure API - Object senderAudience = audiences.getClass().getMethod("sender", CommandSender.class).invoke(audiences, sender); - senderAudience.getClass() - .getMethod("sendMessage", Class.forName("net.kyori.adventure.text.Component")) - .invoke(senderAudience, message); - return; - } catch (Exception e) { - // Fallback на legacy - } - } - - // Legacy fallback - if (message instanceof String) { - sender.sendMessage((String) message); - } else { - sender.sendMessage(message.toString()); - } - } - - public static void sendMessage(Player player, Object message) { - if (adventureAvailable && audiences != null) { - try { - // Используем Adventure API - Object playerAudience = audiences.getClass().getMethod("player", Player.class).invoke(audiences, player); - playerAudience.getClass() - .getMethod("sendMessage", Class.forName("net.kyori.adventure.text.Component")) - .invoke(playerAudience, message); - return; - } catch (Exception e) { - // Fallback на legacy - } - } - - // Legacy fallback - if (message instanceof String) { - player.sendMessage((String) message); - } else { - player.sendMessage(message.toString()); - } - } - - public static boolean isAdventureAvailable() { - return adventureAvailable; - } -} From 7f513a7104fd5a3ce57330c06118921d199ae681 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 20 Sep 2026 16:38:37 +0200 Subject: [PATCH 07/10] port: replace Paper component APIs, chat pipeline, events and schedulers - Msg.send/broadcast/actionBar/title/legacy/plain for all 58 files sendMessage replacements (plan step 3) - ChatEventListener: AsyncChatEvent->AsyncPlayerChatEvent, getRecipients filtering, manual render+Msg.send loop, HIGHEST|ignoreCancelled, nobodyHeard via Bukkit scheduler (step 4) - EnhancedChatRenderer: remove ChatRenderer/Audience, render(Player,Component,Component,Player), getLocale String, Msg.actionBar - PlayerEventListener: join/quit/death String API, getCustomName/getDisplayName legacy, Msg.broadcast - FoliaUtil: IS_FOLIA false, Bukkit scheduler only; AutoMessageManager BukkitTask - CustomCommandManager: reflective getCommandMap; other mechanical fixes (displayName, kick, inventory titles) - single universal jar (variant A) - loads on Spigot 1.20.1 and Paper --- .../commands/CustomCommandsCommand.java | 18 ++-- .../admin/broadcast/AnnounceCommand.java | 14 +-- .../admin/broadcast/DiscordCommand.java | 23 ++--- .../commands/admin/chat/ChatSpyCommand.java | 4 +- .../commands/admin/chat/ClearChatCommand.java | 4 +- .../admin/chat/ClearChatConfigCommand.java | 20 +++-- .../commands/admin/system/HubCommand.java | 8 +- .../commands/admin/system/LoChatCommand.java | 32 +++---- .../admin/system/ReloadConfigCommand.java | 8 +- .../lochat/commands/chat/ChannelCommand.java | 26 +++--- .../commands/chat/GlobalChatCommand.java | 8 +- .../commands/chat/JoinChannelCommand.java | 16 ++-- .../commands/chat/LeaveChannelCommand.java | 8 +- .../commands/chat/LocalChatCommand.java | 4 +- .../commands/messaging/IgnoreCommand.java | 12 +-- .../commands/messaging/IgnoreListCommand.java | 10 ++- .../lochat/commands/messaging/MsgCommand.java | 12 +-- .../commands/messaging/ReplyCommand.java | 10 ++- .../commands/messaging/UnignoreCommand.java | 10 ++- .../commands/moderation/ban/BanCommand.java | 22 ++--- .../commands/moderation/ban/UnbanCommand.java | 12 +-- .../moderation/mute/MuteBlameCommand.java | 4 +- .../commands/moderation/mute/MuteCommand.java | 30 ++++--- .../moderation/mute/MuteHistoryCommand.java | 6 +- .../moderation/mute/MuteListCommand.java | 4 +- .../moderation/mute/UnmuteCommand.java | 20 +++-- .../moderation/warn/SilentWarnCommand.java | 12 +-- .../commands/moderation/warn/WarnCommand.java | 16 ++-- .../lochat/commands/nick/NickCommand.java | 32 +++---- .../commands/nick/PlayerInfoCommand.java | 38 ++++---- .../loki/lochat/commands/rp/DoCommand.java | 4 +- .../loki/lochat/commands/rp/MeCommand.java | 4 +- .../loki/lochat/commands/rp/RollCommand.java | 8 +- .../com/loki/lochat/commands/rp/RpUtil.java | 10 ++- .../loki/lochat/commands/rp/TryCommand.java | 4 +- .../com/loki/lochat/config/MessageConfig.java | 6 +- .../loki/lochat/core/filter/MuteFilter.java | 4 +- .../lochat/core/service/ChatServiceImpl.java | 14 +-- .../lochat/core/service/NickServiceImpl.java | 16 ++-- .../messaging/PrivateMessageServiceImpl.java | 8 +- .../core/service/messaging/SpyService.java | 6 +- .../gradient/config/GradientMessages.java | 6 +- .../gradient/gui/GradientConfirmGUI.java | 14 +-- .../lochat/gradient/util/DisplayNameUtil.java | 6 +- .../integrations/PlaceholderAPIHook.java | 2 +- .../lochat/listener/ChatEventListener.java | 88 ++++++++++++------- .../lochat/listener/DiscordEventListener.java | 11 +-- .../lochat/listener/ModerationListener.java | 3 +- .../lochat/listener/PlayerEventListener.java | 45 +++++----- .../lochat/managers/AutoMessageManager.java | 42 +++------ .../lochat/managers/CustomCommandManager.java | 30 ++++++- .../managers/commands/MessageSender.java | 36 ++++---- .../commands/PlaceholderProcessor.java | 4 +- .../lochat/renderer/EnhancedChatRenderer.java | 66 +++++++------- .../lochat/utils/format/ChatFormatter.java | 2 +- .../loki/lochat/utils/platform/FoliaUtil.java | 40 ++------- 56 files changed, 506 insertions(+), 416 deletions(-) diff --git a/src/main/java/com/loki/lochat/commands/CustomCommandsCommand.java b/src/main/java/com/loki/lochat/commands/CustomCommandsCommand.java index 63ca74e..f3be1b7 100644 --- a/src/main/java/com/loki/lochat/commands/CustomCommandsCommand.java +++ b/src/main/java/com/loki/lochat/commands/CustomCommandsCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.managers.CustomCommandManager; import com.loki.lochat.utils.format.ChatFormatter; @@ -28,7 +30,7 @@ public class CustomCommandsCommand implements CommandExecutor, TabCompleter { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!sender.hasPermission("lochat.customcommands")) { - sender.sendMessage(ChatFormatter.parse("<#F44336>❌ У вас нет прав для использования этой команды!")); + Msg.send(sender, ChatFormatter.parse("<#F44336>❌ У вас нет прав для использования этой команды!")); return true; } @@ -42,14 +44,14 @@ public class CustomCommandsCommand implements CommandExecutor, TabCompleter { case "reload" -> reloadCommands(sender); case "enable" -> { if (args.length < 2) { - sender.sendMessage(ChatFormatter.parse("<#FFA726>⚠️ Использование: /customcommands enable <команда>")); + Msg.send(sender, ChatFormatter.parse("<#FFA726>⚠️ Использование: /customcommands enable <команда>")); return true; } toggleCommand(sender, args[1], true); } case "disable" -> { if (args.length < 2) { - sender.sendMessage(ChatFormatter.parse("<#FFA726>⚠️ Использование: /customcommands disable <команда>")); + Msg.send(sender, ChatFormatter.parse("<#FFA726>⚠️ Использование: /customcommands disable <команда>")); return true; } toggleCommand(sender, args[1], false); @@ -73,7 +75,7 @@ public class CustomCommandsCommand implements CommandExecutor, TabCompleter { ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ """); - sender.sendMessage(message); + Msg.send(sender, message); } private void showCommandsList(CommandSender sender) { @@ -91,21 +93,21 @@ public class CustomCommandsCommand implements CommandExecutor, TabCompleter { } message.append(ChatFormatter.parse("\n▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬")); - sender.sendMessage(message.build()); + Msg.send(sender, message.build()); } private void reloadCommands(CommandSender sender) { try { commandManager.reload(); - sender.sendMessage(ChatFormatter.parse("<#4CAF50>✅ Кастомные команды успешно перезагружены!")); + Msg.send(sender, ChatFormatter.parse("<#4CAF50>✅ Кастомные команды успешно перезагружены!")); } catch (Exception e) { - sender.sendMessage(ChatFormatter.parse("<#F44336>❌ Ошибка при перезагрузке команд: " + e.getMessage())); + Msg.send(sender, ChatFormatter.parse("<#F44336>❌ Ошибка при перезагрузке команд: " + e.getMessage())); plugin.getLogger().severe("Ошибка перезагрузки кастомных команд: " + e.getMessage()); } } private void toggleCommand(CommandSender sender, String commandName, boolean enable) { - sender.sendMessage(ChatFormatter.parse( + Msg.send(sender, ChatFormatter.parse( "<#FFA726>⚠️ Для изменения статуса команд отредактируйте файл custom-commands.yml и выполните /customcommands reload" )); } diff --git a/src/main/java/com/loki/lochat/commands/admin/broadcast/AnnounceCommand.java b/src/main/java/com/loki/lochat/commands/admin/broadcast/AnnounceCommand.java index 1d29c4c..d5249c1 100644 --- a/src/main/java/com/loki/lochat/commands/admin/broadcast/AnnounceCommand.java +++ b/src/main/java/com/loki/lochat/commands/admin/broadcast/AnnounceCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.admin.broadcast; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.utils.format.ChatFormatter; @@ -26,12 +28,12 @@ public class AnnounceCommand implements CommandExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (args.length == 0) { - sender.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/announce <сообщение>"))); + Msg.send(sender, ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/announce <сообщение>"))); return true; } if (args[0].equalsIgnoreCase("reload") && sender.hasPermission("chat.announce.reload")) { plugin.reload(); - sender.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("announce.reloaded"))); + Msg.send(sender, ChatFormatter.parse(plugin.getMessageConfig().get("announce.reloaded"))); return true; } @@ -39,17 +41,17 @@ public class AnnounceCommand implements CommandExecutor { Component formatted = ChatFormatter.formatAnnouncement(plugin.getConfigManager().getAnnouncementFormat(), message); for (Player player : Bukkit.getOnlinePlayers()) { - player.sendMessage(formatted); + Msg.send(player, formatted); if (plugin.getConfigManager().isAnnouncementActionBarEnabled()) { - player.sendActionBar(formatted); + Msg.actionBar(player, formatted); } if (plugin.getConfigManager().isAnnouncementTitleEnabled()) { - player.showTitle(Title.title( + Msg.title(player, ChatFormatter.parse(plugin.getConfigManager().getAnnouncementTitleHeader()), ChatFormatter.parse(message), Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(plugin.getConfigManager().getAnnouncementTitleDuration()), - Duration.ofMillis(500)))); + Duration.ofMillis(500))); } } plugin.getLogger().info("[ANNOUNCE] " + sender.getName() + ": " + ChatFormatter.stripTags(message)); diff --git a/src/main/java/com/loki/lochat/commands/admin/broadcast/DiscordCommand.java b/src/main/java/com/loki/lochat/commands/admin/broadcast/DiscordCommand.java index 5421172..1b30b3a 100644 --- a/src/main/java/com/loki/lochat/commands/admin/broadcast/DiscordCommand.java +++ b/src/main/java/com/loki/lochat/commands/admin/broadcast/DiscordCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.admin.broadcast; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.integrations.DiscordIntegration; import com.loki.lochat.utils.format.ChatFormatter; @@ -27,7 +29,7 @@ public class DiscordCommand implements CommandExecutor, TabCompleter { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (!sender.hasPermission("lochat.discord.admin")) { - sender.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("errors.no-permission"))); + Msg.send(sender, ChatFormatter.parse(plugin.getMessageConfig().get("errors.no-permission"))); return true; } if (args.length == 0) { @@ -35,29 +37,30 @@ public class DiscordCommand implements CommandExecutor, TabCompleter { return true; } switch (args[0].toLowerCase()) { - case "status" -> sender.sendMessage(ChatFormatter.parse( + case "status" -> Msg.send(sender, ChatFormatter.parse( discord.isEnabled() ? "⚖C9Discord ẲE9включён" : "⚖C9Discord &#CF6679отключён")); case "reload" -> { try { discord.reload(); - sender.sendMessage(ChatFormatter.parse("⚖C9Discord ẲE9перезагружен")); + Msg.send(sender, ChatFormatter.parse("⚖C9Discord ẲE9перезагружен")); } catch (Exception e) { - sender.sendMessage(ChatFormatter.parse("&#CF6679Ошибка: " + e.getMessage())); + Msg.send(sender, ChatFormatter.parse("&#CF6679Ошибка: " + e.getMessage())); } } case "test" -> { if (!discord.isEnabled()) { - sender.sendMessage(ChatFormatter.parse("&#CF6679Discord отключён")); + Msg.send(sender, ChatFormatter.parse("&#CF6679Discord отключён")); return true; } String msg = args.length > 1 ? String.join(" ", Arrays.copyOfRange(args, 1, args.length)) : "Тест от " + sender.getName(); - sender.sendMessage(ChatFormatter.parse("&#B798A8Отправка...")); + Msg.send(sender, ChatFormatter.parse("&#B798A8Отправка...")); java.util.concurrent.CompletableFuture.runAsync(() -> { discord.sendTestMessage(msg, sender.getName()); - if (sender instanceof org.bukkit.entity.Player p) { - p.getScheduler().run(plugin, t -> sender.sendMessage(ChatFormatter.parse("⚖C9Отправлено ẲE9✓")), null); + if (sender instanceof org.bukkit.entity.Player) { + org.bukkit.Bukkit.getScheduler().runTask(plugin, + () -> Msg.send(sender, ChatFormatter.parse("⚖C9Отправлено ẲE9✓"))); } else { - sender.sendMessage(ChatFormatter.parse("⚖C9Отправлено ẲE9✓")); + Msg.send(sender, ChatFormatter.parse("⚖C9Отправлено ẲE9✓")); } }); } @@ -67,7 +70,7 @@ public class DiscordCommand implements CommandExecutor, TabCompleter { } private void sendHelp(CommandSender s) { - s.sendMessage(ChatFormatter.parse("&#B798A8/discordadmin status|reload|test")); + Msg.send(s, ChatFormatter.parse("&#B798A8/discordadmin status|reload|test")); } @Override diff --git a/src/main/java/com/loki/lochat/commands/admin/chat/ChatSpyCommand.java b/src/main/java/com/loki/lochat/commands/admin/chat/ChatSpyCommand.java index 4416656..40c7d71 100644 --- a/src/main/java/com/loki/lochat/commands/admin/chat/ChatSpyCommand.java +++ b/src/main/java/com/loki/lochat/commands/admin/chat/ChatSpyCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.admin.chat; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.utils.format.ChatFormatter; @@ -27,7 +29,7 @@ public class ChatSpyCommand implements CommandExecutor { return true; } boolean enabled = messagingService.toggleSpy(player.getUniqueId()); - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get(enabled ? "spy.enabled" : "spy.disabled"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get(enabled ? "spy.enabled" : "spy.disabled"))); return true; } } diff --git a/src/main/java/com/loki/lochat/commands/admin/chat/ClearChatCommand.java b/src/main/java/com/loki/lochat/commands/admin/chat/ClearChatCommand.java index 062c9e6..2d1db8f 100644 --- a/src/main/java/com/loki/lochat/commands/admin/chat/ClearChatCommand.java +++ b/src/main/java/com/loki/lochat/commands/admin/chat/ClearChatCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.admin.chat; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.utils.format.ChatFormatter; @@ -26,7 +28,7 @@ public class ClearChatCommand implements CommandExecutor { player.sendMessage(""); } if (plugin.getConfigManager().isClearChatMessageEnabled()) { - player.sendMessage(ChatFormatter.parse( + Msg.send(player, ChatFormatter.parse( plugin.getConfigManager().getClearChatMessage().replace("{player}", sender.getName()))); } } diff --git a/src/main/java/com/loki/lochat/commands/admin/chat/ClearChatConfigCommand.java b/src/main/java/com/loki/lochat/commands/admin/chat/ClearChatConfigCommand.java index e7c57cb..ffc5914 100644 --- a/src/main/java/com/loki/lochat/commands/admin/chat/ClearChatConfigCommand.java +++ b/src/main/java/com/loki/lochat/commands/admin/chat/ClearChatConfigCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.admin.chat; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.utils.format.ChatFormatter; @@ -24,37 +26,37 @@ public class ClearChatConfigCommand implements CommandExecutor, TabCompleter { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (!sender.hasPermission("lochat.clearchat.config")) { - sender.sendMessage(plugin.getMessageConfig().getComponent("errors.no-permission")); + Msg.send(sender, plugin.getMessageConfig().getComponent("errors.no-permission")); return true; } if (args.length == 0) { - sender.sendMessage(ChatFormatter.parse("&#B798A8Использование: ẲE9/clearchatconfig ")); + Msg.send(sender, ChatFormatter.parse("&#B798A8Использование: ẲE9/clearchatconfig ")); return true; } switch (args[0].toLowerCase()) { case "enable" -> { plugin.getConfigManager().setClearChatMessageEnabled(true); - sender.sendMessage(ChatFormatter.parse("⚖C9Сообщение очистки ẲE9включено")); + Msg.send(sender, ChatFormatter.parse("⚖C9Сообщение очистки ẲE9включено")); } case "disable" -> { plugin.getConfigManager().setClearChatMessageEnabled(false); - sender.sendMessage(ChatFormatter.parse("⚖C9Сообщение очистки &#B798A8отключено")); + Msg.send(sender, ChatFormatter.parse("⚖C9Сообщение очистки &#B798A8отключено")); } case "message" -> { if (args.length < 2) { - sender.sendMessage(ChatFormatter.parse("&#CF6679Укажите текст!")); + Msg.send(sender, ChatFormatter.parse("&#CF6679Укажите текст!")); return true; } String msg = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); plugin.getConfigManager().setClearChatMessage(msg); - sender.sendMessage(ChatFormatter.parse("⚖C9Сообщение установлено: &f" + msg)); + Msg.send(sender, ChatFormatter.parse("⚖C9Сообщение установлено: &f" + msg)); } case "status" -> { - sender.sendMessage(ChatFormatter.parse("&#B798A8Включено: " + Msg.send(sender, ChatFormatter.parse("&#B798A8Включено: " + (plugin.getConfigManager().isClearChatMessageEnabled() ? "ẲE9да" : "&#CF6679нет"))); - sender.sendMessage(ChatFormatter.parse("&#B798A8Текст: &f" + plugin.getConfigManager().getClearChatMessage())); + Msg.send(sender, ChatFormatter.parse("&#B798A8Текст: &f" + plugin.getConfigManager().getClearChatMessage())); } - default -> sender.sendMessage(ChatFormatter.parse("&#CF6679Неизвестный аргумент")); + default -> Msg.send(sender, ChatFormatter.parse("&#CF6679Неизвестный аргумент")); } return true; } diff --git a/src/main/java/com/loki/lochat/commands/admin/system/HubCommand.java b/src/main/java/com/loki/lochat/commands/admin/system/HubCommand.java index 382a3fb..8ea4fe3 100644 --- a/src/main/java/com/loki/lochat/commands/admin/system/HubCommand.java +++ b/src/main/java/com/loki/lochat/commands/admin/system/HubCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.admin.system; +import com.loki.lochat.compat.Msg; + import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; import com.loki.lochat.LoChat; @@ -21,11 +23,11 @@ public class HubCommand implements CommandExecutor { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!(sender instanceof Player player)) { - sender.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("general.players-only"))); + Msg.send(sender, ChatFormatter.parse(plugin.getMessageConfig().get("general.players-only"))); return true; } if (!plugin.getConfigManager().getBoolean("hub_command.enabled", true)) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("general.command-disabled"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("general.command-disabled"))); return true; } String server = plugin.getConfigManager().getString("hub_command.server", "hub"); @@ -33,7 +35,7 @@ public class HubCommand implements CommandExecutor { out.writeUTF("Connect"); out.writeUTF(server); player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray()); - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("hub.connecting").replace("%server%", server))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("hub.connecting").replace("%server%", server))); return true; } } diff --git a/src/main/java/com/loki/lochat/commands/admin/system/LoChatCommand.java b/src/main/java/com/loki/lochat/commands/admin/system/LoChatCommand.java index 41de207..5d74ca4 100644 --- a/src/main/java/com/loki/lochat/commands/admin/system/LoChatCommand.java +++ b/src/main/java/com/loki/lochat/commands/admin/system/LoChatCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.admin.system; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.utils.format.ChatFormatter; @@ -24,50 +26,48 @@ public class LoChatCommand implements CommandExecutor, TabCompleter { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (!sender.hasPermission("lochat.admin")) { - sender.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getNoPermission())); + Msg.send(sender, ChatFormatter.parse(plugin.getMessageConfig().getNoPermission())); return true; } if (args.length == 0) { - sender.sendMessage(ChatFormatter.parse("ẲE9LoChat &#B798A8v" + plugin.getPluginMeta().getVersion())); - sender.sendMessage(ChatFormatter.parse("⚖C9/lochat reload &#B798A8— перезагрузить конфиги")); - sender.sendMessage(ChatFormatter.parse("⚖C9/lochat commands [reload] &#B798A8— кастомные команды")); - sender.sendMessage(ChatFormatter.parse("⚖C9/lochat filterstats &#B798A8— статистика фильтров чата")); + Msg.send(sender, ChatFormatter.parse("ẲE9LoChat &#B798A8v" + plugin.getDescription().getVersion())); + Msg.send(sender, ChatFormatter.parse("⚖C9/lochat reload &#B798A8— перезагрузить конфиги")); + Msg.send(sender, ChatFormatter.parse("⚖C9/lochat commands [reload] &#B798A8— кастомные команды")); + Msg.send(sender, ChatFormatter.parse("⚖C9/lochat filterstats &#B798A8— статистика фильтров чата")); return true; } switch (args[0].toLowerCase()) { case "reload" -> { plugin.reload(); - sender.sendMessage(ChatFormatter.parse("⚖C9Конфигурация ẲE9перезагружена!")); + Msg.send(sender, ChatFormatter.parse("⚖C9Конфигурация ẲE9перезагружена!")); } case "commands" -> { if (args.length > 1 && args[1].equalsIgnoreCase("reload")) { plugin.getCustomCommandManager().reload(); - sender.sendMessage(ChatFormatter.parse("⚖C9Кастомные команды ẲE9перезагружены!")); + Msg.send(sender, ChatFormatter.parse("⚖C9Кастомные команды ẲE9перезагружены!")); } else { var cmds = plugin.getCustomCommandManager().getCommands(); - sender.sendMessage(ChatFormatter.parse("&#B798A8Кастомные команды ⚖C9(" + cmds.size() + "⚖C9):")); - cmds.values().forEach(c -> sender.sendMessage( - ChatFormatter.parse("ẲE9/" + c.name() + " &#B798A8(" + c.type() + ")"))); + Msg.send(sender, ChatFormatter.parse("&#B798A8Кастомные команды ⚖C9(" + cmds.size() + "⚖C9):")); + cmds.values().forEach(c -> Msg.send(sender, ChatFormatter.parse("ẲE9/" + c.name() + " &#B798A8(" + c.type() + ")"))); } } case "filterstats" -> { var filter = plugin.getChatFilter(); if (filter == null) { - sender.sendMessage(ChatFormatter.parse("&#CF6679Фильтр чата недоступен")); + Msg.send(sender, ChatFormatter.parse("&#CF6679Фильтр чата недоступен")); break; } var stats = filter.getStatistics(); long total = stats.getTotalBlocked(); - sender.sendMessage(ChatFormatter.parse( + Msg.send(sender, ChatFormatter.parse( "&#B798A8Фильтры чата: ⚖C9заблокировано ẲE9" + total + "⚖C9 сообщений")); if (total == 0) { - sender.sendMessage(ChatFormatter.parse("&#B798A8Пока нечего показывать")); + Msg.send(sender, ChatFormatter.parse("&#B798A8Пока нечего показывать")); break; } - stats.snapshot().forEach((name, count) -> sender.sendMessage( - ChatFormatter.parse("ẲE9" + name + ":⚖C9 " + count))); + stats.snapshot().forEach((name, count) -> Msg.send(sender, ChatFormatter.parse("ẲE9" + name + ":⚖C9 " + count))); } - default -> sender.sendMessage(ChatFormatter.parse("&#CF6679Неизвестная команда")); + default -> Msg.send(sender, ChatFormatter.parse("&#CF6679Неизвестная команда")); } return true; } diff --git a/src/main/java/com/loki/lochat/commands/admin/system/ReloadConfigCommand.java b/src/main/java/com/loki/lochat/commands/admin/system/ReloadConfigCommand.java index 2ed603f..1f32952 100644 --- a/src/main/java/com/loki/lochat/commands/admin/system/ReloadConfigCommand.java +++ b/src/main/java/com/loki/lochat/commands/admin/system/ReloadConfigCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.admin.system; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.utils.format.ChatFormatter; @@ -19,14 +21,14 @@ public class ReloadConfigCommand implements CommandExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (!sender.hasPermission("lochat.admin.reload")) { - sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoPermission())); + Msg.send(sender, ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoPermission())); return true; } try { plugin.reload(); - sender.sendMessage(ChatFormatter.parse("⚖C9Все конфигурации ẲE9успешно перезагружены!")); + Msg.send(sender, ChatFormatter.parse("⚖C9Все конфигурации ẲE9успешно перезагружены!")); } catch (Exception e) { - sender.sendMessage(ChatFormatter.parse("&#CF6679Ошибка при перезагрузке: " + e.getMessage())); + Msg.send(sender, ChatFormatter.parse("&#CF6679Ошибка при перезагрузке: " + e.getMessage())); plugin.getLogger().severe("Ошибка при перезагрузке: " + e.getMessage()); } return true; diff --git a/src/main/java/com/loki/lochat/commands/chat/ChannelCommand.java b/src/main/java/com/loki/lochat/commands/chat/ChannelCommand.java index 90982d9..2a6d0b7 100644 --- a/src/main/java/com/loki/lochat/commands/chat/ChannelCommand.java +++ b/src/main/java/com/loki/lochat/commands/chat/ChannelCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.chat; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.ChannelService; import com.loki.lochat.channel.Channel; @@ -31,7 +33,7 @@ public class ChannelCommand extends PlayerCommand { @NotNull String label, @NotNull String[] args) { ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class); if (!channels.isEnabled()) { - player.sendMessage(ChatFormatter.parse("&#CF6679Система каналов отключена")); + Msg.send(player, ChatFormatter.parse("&#CF6679Система каналов отключена")); return true; } @@ -44,9 +46,9 @@ public class ChannelCommand extends PlayerCommand { case "off", "leave" -> { if (channels.isInChannel(player)) { channels.leave(player); - player.sendMessage(ChatFormatter.parse("⚖C9Вы вышли из канала")); + Msg.send(player, ChatFormatter.parse("⚖C9Вы вышли из канала")); } else { - player.sendMessage(ChatFormatter.parse("&#B798A8Вы не находитесь в канале")); + Msg.send(player, ChatFormatter.parse("&#B798A8Вы не находитесь в канале")); } } case "list" -> listChannels(player, channels); @@ -59,22 +61,22 @@ public class ChannelCommand extends PlayerCommand { Channel active = channels.getActive(player).orElse(null); if (active == null) { Channel def = channels.getDefault(); - player.sendMessage(ChatFormatter.parse("&#B798A8Вы в дефолтном канале [ẲE9" + Msg.send(player, ChatFormatter.parse("&#B798A8Вы в дефолтном канале [ẲE9" + (def == null ? "?" : def.display() + "&8 " + def.prefix()) + "&#B798A8]")); } else { - player.sendMessage(ChatFormatter.parse("⚖C9Вы в канале [ẲE9" + Msg.send(player, ChatFormatter.parse("⚖C9Вы в канале [ẲE9" + active.display() + "&8 " + active.prefix() + "⚖C9]")); } - player.sendMessage(ChatFormatter.parse("&#B798A8/join <канал> ⚖C9— войти, " + Msg.send(player, ChatFormatter.parse("&#B798A8/join <канал> ⚖C9— войти, " + "&#B798A8/channel list ⚖C9— все каналы")); } private void listChannels(Player player, ChannelService channels) { - player.sendMessage(ChatFormatter.parse("&#B798A8Доступные каналы:")); + Msg.send(player, ChatFormatter.parse("&#B798A8Доступные каналы:")); for (Channel c : channels.getChannels()) { boolean canUse = channels.canUse(player, c); boolean active = c.equals(channels.getActive(player).orElse(null)); - player.sendMessage(ChatFormatter.parse((canUse ? "⚖C9" : "&#CF6679") + Msg.send(player, ChatFormatter.parse((canUse ? "⚖C9" : "&#CF6679") + (active ? "\u25B8 " : "\u25AA ") + c.display() + "&8 " + c.prefix() + (canUse ? "" : " &#CF6679(нет прав)") + " &#B798A8— " + (c.isGlobalScope() ? "глобальный" : "локальный, " + c.radius() + "б"))); @@ -84,21 +86,21 @@ public class ChannelCommand extends PlayerCommand { private void joinChannel(Player player, ChannelService channels, String name) { Channel channel = channels.find(name); if (channel == null) { - player.sendMessage(ChatFormatter.parse("&#CF6679Канал ẲE9" + name + " &#CF6679не найден")); + Msg.send(player, ChatFormatter.parse("&#CF6679Канал ẲE9" + name + " &#CF6679не найден")); return; } if (!channels.canUse(player, channel)) { - player.sendMessage(ChatFormatter.parse( + Msg.send(player, ChatFormatter.parse( plugin.getConfigManager().getMessagesConfig().getNoPermission())); return; } Channel active = channels.getActive(player).orElse(null); channels.join(player, channel); if (channel.equals(active)) { - player.sendMessage(ChatFormatter.parse("&#B798A8Вы уже в канале [ẲE9" + Msg.send(player, ChatFormatter.parse("&#B798A8Вы уже в канале [ẲE9" + channel.display() + "&8 " + channel.prefix() + "&#B798A8]")); } else { - player.sendMessage(ChatFormatter.parse("⚖C9Вы вошли в канал [ẲE9" + Msg.send(player, ChatFormatter.parse("⚖C9Вы вошли в канал [ẲE9" + channel.display() + "&8 " + channel.prefix() + "⚖C9]")); } } diff --git a/src/main/java/com/loki/lochat/commands/chat/GlobalChatCommand.java b/src/main/java/com/loki/lochat/commands/chat/GlobalChatCommand.java index bdc592f..12e9da1 100644 --- a/src/main/java/com/loki/lochat/commands/chat/GlobalChatCommand.java +++ b/src/main/java/com/loki/lochat/commands/chat/GlobalChatCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.chat; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.ChatService; import com.loki.lochat.utils.format.ChatFormatter; @@ -30,16 +32,16 @@ public class GlobalChatCommand implements CommandExecutor { if (command.getName().equalsIgnoreCase("globalchat")) { if (args.length > 0 && args[0].equalsIgnoreCase("toggle")) { boolean enabled = chatService.toggleGlobalChat(player.getUniqueId()); - player.sendMessage(ChatFormatter.parse( + Msg.send(player, ChatFormatter.parse( plugin.getMessageConfig().get(enabled ? "global.toggled-on" : "global.toggled-off"))); } else { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/globalchat toggle"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/globalchat toggle"))); } return true; } if (args.length == 0) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/g <сообщение>"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/g <сообщение>"))); return true; } diff --git a/src/main/java/com/loki/lochat/commands/chat/JoinChannelCommand.java b/src/main/java/com/loki/lochat/commands/chat/JoinChannelCommand.java index bd2c3af..33e64f6 100644 --- a/src/main/java/com/loki/lochat/commands/chat/JoinChannelCommand.java +++ b/src/main/java/com/loki/lochat/commands/chat/JoinChannelCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.chat; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.ChannelService; import com.loki.lochat.channel.Channel; @@ -28,22 +30,22 @@ public class JoinChannelCommand extends PlayerCommand { protected boolean executePlayerCommand(@NotNull Player player, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (!plugin.getConfigManager().getChannelConfig().isEnabled()) { - player.sendMessage(ChatFormatter.parse("&#CF6679Система каналов отключена")); + Msg.send(player, ChatFormatter.parse("&#CF6679Система каналов отключена")); return true; } if (args.length == 0) { - player.sendMessage(ChatFormatter.parse("&#CF6679Использование: ẲE9/join <канал>")); + Msg.send(player, ChatFormatter.parse("&#CF6679Использование: ẲE9/join <канал>")); return true; } ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class); Channel channel = channels.find(args[0]); if (channel == null) { - player.sendMessage(ChatFormatter.parse("&#CF6679Канал ẲE9" + args[0] + " &#CF6679не найден")); + Msg.send(player, ChatFormatter.parse("&#CF6679Канал ẲE9" + args[0] + " &#CF6679не найден")); return true; } if (!channels.canUse(player, channel)) { - player.sendMessage(ChatFormatter.parse( + Msg.send(player, ChatFormatter.parse( plugin.getConfigManager().getMessagesConfig().getNoPermission())); return true; } @@ -51,13 +53,13 @@ public class JoinChannelCommand extends PlayerCommand { Channel active = channels.getActive(player).orElse(null); channels.join(player, channel); if (channel.equals(channels.getDefault())) { - player.sendMessage(ChatFormatter.parse("⚖C9Вы в канале [ẲE9" + Msg.send(player, ChatFormatter.parse("⚖C9Вы в канале [ẲE9" + channel.display() + "&8 " + channel.prefix() + "⚖C9] (дефолтный)")); } else if (channel.equals(active)) { - player.sendMessage(ChatFormatter.parse("&#B798A8Вы уже в канале [ẲE9" + Msg.send(player, ChatFormatter.parse("&#B798A8Вы уже в канале [ẲE9" + channel.display() + "&8 " + channel.prefix() + "&#B798A8]")); } else { - player.sendMessage(ChatFormatter.parse("⚖C9Вы вошли в канал [ẲE9" + Msg.send(player, ChatFormatter.parse("⚖C9Вы вошли в канал [ẲE9" + channel.display() + "&8 " + channel.prefix() + "⚖C9]")); } return true; diff --git a/src/main/java/com/loki/lochat/commands/chat/LeaveChannelCommand.java b/src/main/java/com/loki/lochat/commands/chat/LeaveChannelCommand.java index 83942f5..505125a 100644 --- a/src/main/java/com/loki/lochat/commands/chat/LeaveChannelCommand.java +++ b/src/main/java/com/loki/lochat/commands/chat/LeaveChannelCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.chat; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.ChannelService; import com.loki.lochat.commands.base.PlayerCommand; @@ -23,15 +25,15 @@ public class LeaveChannelCommand extends PlayerCommand { @NotNull String label, @NotNull String[] args) { ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class); if (!channels.isEnabled()) { - player.sendMessage(ChatFormatter.parse("&#CF6679Система каналов отключена")); + Msg.send(player, ChatFormatter.parse("&#CF6679Система каналов отключена")); return true; } if (!channels.isInChannel(player)) { - player.sendMessage(ChatFormatter.parse("&#B798A8Вы не находитесь в канале")); + Msg.send(player, ChatFormatter.parse("&#B798A8Вы не находитесь в канале")); return true; } channels.leave(player); - player.sendMessage(ChatFormatter.parse("⚖C9Вы вышли из канала")); + Msg.send(player, ChatFormatter.parse("⚖C9Вы вышли из канала")); return true; } } \ No newline at end of file diff --git a/src/main/java/com/loki/lochat/commands/chat/LocalChatCommand.java b/src/main/java/com/loki/lochat/commands/chat/LocalChatCommand.java index 74f2e61..14c8076 100644 --- a/src/main/java/com/loki/lochat/commands/chat/LocalChatCommand.java +++ b/src/main/java/com/loki/lochat/commands/chat/LocalChatCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.chat; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.ChatService; import com.loki.lochat.utils.format.ChatFormatter; @@ -28,7 +30,7 @@ public class LocalChatCommand implements CommandExecutor { } if (args.length == 0) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/l <сообщение>"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/l <сообщение>"))); 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 383ec9c..9e9fd79 100644 --- a/src/main/java/com/loki/lochat/commands/messaging/IgnoreCommand.java +++ b/src/main/java/com/loki/lochat/commands/messaging/IgnoreCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.messaging; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.utils.format.ChatFormatter; @@ -28,26 +30,26 @@ public class IgnoreCommand implements CommandExecutor { return true; } if (args.length == 0) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/ignore <ник>"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/ignore <ник>"))); return true; } Player target = Bukkit.getPlayer(args[0]); if (target == null) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getPlayerNotFound())); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getPlayerNotFound())); return true; } if (target.equals(player)) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.self"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("ignore.self"))); return true; } if (messagingService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.already-ignored"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("ignore.already-ignored"))); return true; } messagingService.addIgnore(player.getUniqueId(), target.getUniqueId()); - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.added", "{player}", target.getName()))); + Msg.send(player, 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 604813e..3372e68 100644 --- a/src/main/java/com/loki/lochat/commands/messaging/IgnoreListCommand.java +++ b/src/main/java/com/loki/lochat/commands/messaging/IgnoreListCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.messaging; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.utils.format.ChatFormatter; @@ -33,18 +35,18 @@ public class IgnoreListCommand implements CommandExecutor { Set ignored = messagingService.getIgnoredPlayers(player.getUniqueId()); if (ignored.isEmpty()) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-empty"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-empty"))); return true; } - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-header"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-header"))); for (UUID uuid : ignored) { String name = PlayerUtil.getPlayerName(uuid); if (name != null) { - player.sendMessage(ChatFormatter.parse("⚖C9◆ &f" + name)); + Msg.send(player, ChatFormatter.parse("⚖C9◆ &f" + name)); } } - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-footer") + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-footer") .replace("{count}", String.valueOf(ignored.size())))); return true; 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 e852dbb..dfe6626 100644 --- a/src/main/java/com/loki/lochat/commands/messaging/MsgCommand.java +++ b/src/main/java/com/loki/lochat/commands/messaging/MsgCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.messaging; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.api.service.pm.PrivateMessageService; @@ -34,25 +36,25 @@ public class MsgCommand implements CommandExecutor { return true; } if (args.length < 2) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/msg <ник> <сообщение>"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/msg <ник> <сообщение>"))); return true; } Player target = Bukkit.getPlayer(args[0]); if (target == null) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.player-offline"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("pm.player-offline"))); return true; } if (target.equals(player)) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.self"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("pm.self"))); return true; } if (messagingService.isIgnoring(target.getUniqueId(), player.getUniqueId())) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); + Msg.send(player, 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"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); return true; } 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 86c50f9..3d75c95 100644 --- a/src/main/java/com/loki/lochat/commands/messaging/ReplyCommand.java +++ b/src/main/java/com/loki/lochat/commands/messaging/ReplyCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.messaging; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.api.service.pm.PrivateMessageService; @@ -37,23 +39,23 @@ public class ReplyCommand implements CommandExecutor { return true; } if (args.length == 0) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/reply <сообщение>"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/reply <сообщение>"))); return true; } Optional lastUuid = messagingService.getLastConversation(player.getUniqueId()); if (lastUuid.isEmpty()) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.no-reply"))); + Msg.send(player, 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"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("pm.player-offline"))); return true; } if (messagingService.isIgnoring(target.getUniqueId(), player.getUniqueId())) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); 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 0c2a20d..79cba28 100644 --- a/src/main/java/com/loki/lochat/commands/messaging/UnignoreCommand.java +++ b/src/main/java/com/loki/lochat/commands/messaging/UnignoreCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.messaging; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.utils.format.ChatFormatter; @@ -28,22 +30,22 @@ public class UnignoreCommand implements CommandExecutor { return true; } if (args.length == 0) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/unignore <ник>"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/unignore <ник>"))); return true; } Player target = Bukkit.getPlayer(args[0]); if (target == null) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getPlayerNotFound())); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getPlayerNotFound())); return true; } if (!messagingService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.not-ignored"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("ignore.not-ignored"))); return true; } messagingService.removeIgnore(player.getUniqueId(), target.getUniqueId()); - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.removed", "{player}", target.getName()))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("ignore.removed", "{player}", target.getName()))); return true; } } diff --git a/src/main/java/com/loki/lochat/commands/moderation/ban/BanCommand.java b/src/main/java/com/loki/lochat/commands/moderation/ban/BanCommand.java index 82c6bf9..85c8079 100644 --- a/src/main/java/com/loki/lochat/commands/moderation/ban/BanCommand.java +++ b/src/main/java/com/loki/lochat/commands/moderation/ban/BanCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.moderation.ban; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MuteService; import com.loki.lochat.api.service.PunishmentService; @@ -41,35 +43,35 @@ public class BanCommand implements CommandExecutor, TabCompleter { @NotNull String label, @NotNull String[] args) { MessagesConfig msg = plugin.getConfigManager().getMessagesConfig(); if (!sender.hasPermission("lochat.ban")) { - sender.sendMessage(ChatFormatter.parse(msg.getNoPermission())); + Msg.send(sender, ChatFormatter.parse(msg.getNoPermission())); return true; } if (args.length < 1) { - sender.sendMessage(ChatFormatter.parse(msg.getBanUsage())); - sender.sendMessage(ChatFormatter.parse(msg.getMuteTimeHelp())); + Msg.send(sender, ChatFormatter.parse(msg.getBanUsage())); + Msg.send(sender, ChatFormatter.parse(msg.getMuteTimeHelp())); return true; } String targetName = args[0]; UUID targetUuid = PlayerUtil.findPlayerUUID(targetName); if (targetUuid == null) { - sender.sendMessage(ChatFormatter.parse(msg.getPlayerNotFound())); + Msg.send(sender, ChatFormatter.parse(msg.getPlayerNotFound())); return true; } if (sender instanceof Player p && p.getUniqueId().equals(targetUuid)) { - sender.sendMessage(ChatFormatter.parse("&#CF6679Нельзя забанить самого себя")); + Msg.send(sender, ChatFormatter.parse("&#CF6679Нельзя забанить самого себя")); return true; } Player onlineTarget = Bukkit.getPlayer(targetUuid); if (onlineTarget != null && onlineTarget.hasPermission("lochat.bypass.ban") && !sender.hasPermission("lochat.ban.override")) { - sender.sendMessage(ChatFormatter.parse("&#CF6679Нельзя забанить этого игрока")); + Msg.send(sender, ChatFormatter.parse("&#CF6679Нельзя забанить этого игрока")); return true; } if (punishmentService.isBanned(targetUuid)) { - sender.sendMessage(ChatFormatter.parse(msg.getAlreadyBanned())); + Msg.send(sender, ChatFormatter.parse(msg.getAlreadyBanned())); return true; } @@ -90,13 +92,13 @@ public class BanCommand implements CommandExecutor, TabCompleter { String durationLabel = durationMs == 0 ? msg.getModerationDurationPermanent() : muteService.formatTime(durationMs); - sender.sendMessage(ChatFormatter.parse(msg.getBanConfirm().replace("{player}", finalName).replace("{duration}", durationLabel))); + Msg.send(sender, ChatFormatter.parse(msg.getBanConfirm().replace("{player}", finalName).replace("{duration}", durationLabel))); BanRecord ban = punishmentService.getActiveBan(targetUuid); if (ban != null) { Player kicked = Bukkit.getPlayer(targetUuid); if (kicked != null && kicked.isOnline()) { - kicked.kick(punishmentService.buildBanKickMessage(ban)); + Msg.kick(kicked, punishmentService.buildBanKickMessage(ban)); } } return true; @@ -126,7 +128,7 @@ public class BanCommand implements CommandExecutor, TabCompleter { } else { durationMs = muteService.parseTime(timeStr); if (durationMs < 0) { - sender.sendMessage(ChatFormatter.parse(msg.getInvalidTime())); + Msg.send(sender, ChatFormatter.parse(msg.getInvalidTime())); return new DurationResult(0, "", false); } } diff --git a/src/main/java/com/loki/lochat/commands/moderation/ban/UnbanCommand.java b/src/main/java/com/loki/lochat/commands/moderation/ban/UnbanCommand.java index 3c75a6e..d575623 100644 --- a/src/main/java/com/loki/lochat/commands/moderation/ban/UnbanCommand.java +++ b/src/main/java/com/loki/lochat/commands/moderation/ban/UnbanCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.moderation.ban; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.PunishmentService; import com.loki.lochat.config.MessagesConfig; @@ -37,22 +39,22 @@ public class UnbanCommand implements CommandExecutor, TabCompleter { @NotNull String label, @NotNull String[] args) { MessagesConfig msg = plugin.getConfigManager().getMessagesConfig(); if (!sender.hasPermission("lochat.unban")) { - sender.sendMessage(ChatFormatter.parse(msg.getNoPermission())); + Msg.send(sender, ChatFormatter.parse(msg.getNoPermission())); return true; } if (args.length < 1) { - sender.sendMessage(ChatFormatter.parse("&#B798A8Использование: ẲE9/lunban <ник>")); + Msg.send(sender, ChatFormatter.parse("&#B798A8Использование: ẲE9/lunban <ник>")); return true; } UUID uuid = PlayerUtil.findPlayerUUID(args[0]); if (uuid == null) { - sender.sendMessage(ChatFormatter.parse(msg.getPlayerNotFound())); + Msg.send(sender, ChatFormatter.parse(msg.getPlayerNotFound())); return true; } if (!punishmentService.isBanned(uuid)) { - sender.sendMessage(ChatFormatter.parse(msg.getNotBanned())); + Msg.send(sender, ChatFormatter.parse(msg.getNotBanned())); return true; } @@ -62,7 +64,7 @@ public class UnbanCommand implements CommandExecutor, TabCompleter { } punishmentService.unban(uuid); - sender.sendMessage(ChatFormatter.parse(msg.getUnbanConfirm().replace("{player}", name))); + Msg.send(sender, ChatFormatter.parse(msg.getUnbanConfirm().replace("{player}", name))); return true; } diff --git a/src/main/java/com/loki/lochat/commands/moderation/mute/MuteBlameCommand.java b/src/main/java/com/loki/lochat/commands/moderation/mute/MuteBlameCommand.java index 7eae4be..f96c1fe 100644 --- a/src/main/java/com/loki/lochat/commands/moderation/mute/MuteBlameCommand.java +++ b/src/main/java/com/loki/lochat/commands/moderation/mute/MuteBlameCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.moderation.mute; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MuteService; import com.loki.lochat.data.model.MuteData; @@ -32,7 +34,7 @@ public class MuteBlameCommand implements CommandExecutor, TabCompleter { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (!sender.hasPermission("lochat.muteblame")) { - sender.sendMessage(plugin.getMessageConfig().getComponent("errors.no-permission")); + Msg.send(sender, plugin.getMessageConfig().getComponent("errors.no-permission")); return true; } if (args.length < 1) { diff --git a/src/main/java/com/loki/lochat/commands/moderation/mute/MuteCommand.java b/src/main/java/com/loki/lochat/commands/moderation/mute/MuteCommand.java index 4987710..66b64de 100644 --- a/src/main/java/com/loki/lochat/commands/moderation/mute/MuteCommand.java +++ b/src/main/java/com/loki/lochat/commands/moderation/mute/MuteCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.moderation.mute; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MuteService; import com.loki.lochat.config.MessagesConfig; @@ -40,14 +42,14 @@ public class MuteCommand implements CommandExecutor, TabCompleter { MessagesConfig hm = plugin.getConfigManager().getMessagesConfig(); if (!sender.hasPermission("lochat.mute")) { - sender.sendMessage(plugin.getMessageConfig().getComponent("errors.no-permission")); + Msg.send(sender, plugin.getMessageConfig().getComponent("errors.no-permission")); return true; } if (args.length < 1) { - sender.sendMessage(ChatFormatter.parse(hm.getMuteUsage())); - sender.sendMessage(ChatFormatter.parse(hm.getMuteTimeHelp())); - sender.sendMessage(ChatFormatter.parse(hm.getMuteSilentHelp())); + Msg.send(sender, ChatFormatter.parse(hm.getMuteUsage())); + Msg.send(sender, ChatFormatter.parse(hm.getMuteTimeHelp())); + Msg.send(sender, ChatFormatter.parse(hm.getMuteSilentHelp())); return true; } @@ -63,18 +65,18 @@ public class MuteCommand implements CommandExecutor, TabCompleter { if (sender instanceof Player p && !muteService.canMuteForDuration(p, duration)) { long max = muteService.getMaxDuration(p); - sender.sendMessage(ChatFormatter.parse("&#CF6679Нельзя мутить на такой срок! &#B798A8Максимум: ẲE9" + + Msg.send(sender, ChatFormatter.parse("&#CF6679Нельзя мутить на такой срок! &#B798A8Максимум: ẲE9" + (max == 0 ? "навсегда" : muteService.formatTime(max)))); return true; } UUID targetUUID = com.loki.lochat.utils.player.PlayerUtil.findPlayerUUID(targetName); if (targetUUID == null) { - sender.sendMessage(plugin.getMessageConfig().getComponent("errors.player-not-found")); + Msg.send(sender, plugin.getMessageConfig().getComponent("errors.player-not-found")); return true; } if (muteService.isMuted(targetUUID)) { - sender.sendMessage(ChatFormatter.parse("&#CF6679Игрок уже замучен!")); + Msg.send(sender, ChatFormatter.parse("&#CF6679Игрок уже замучен!")); return true; } @@ -104,7 +106,7 @@ public class MuteCommand implements CommandExecutor, TabCompleter { } if (silent && !sender.hasPermission("lochat.mute.silent")) { - sender.sendMessage(ChatFormatter.parse(hm.getNoSilentMutePermission())); + Msg.send(sender, ChatFormatter.parse(hm.getNoSilentMutePermission())); return null; } @@ -131,7 +133,7 @@ public class MuteCommand implements CommandExecutor, TabCompleter { } long dur = muteService.parseTime(timeStr); if (dur < 0) { - sender.sendMessage(ChatFormatter.parse(hm.getMuteTimeHelp())); + Msg.send(sender, ChatFormatter.parse(hm.getMuteTimeHelp())); } return dur; } @@ -145,27 +147,27 @@ public class MuteCommand implements CommandExecutor, TabCompleter { muteService.mute(targetUUID, finalName, duration, reason, op); String timeDisplay = duration == 0 ? "навсегда" : muteService.formatTime(duration); - sender.sendMessage(ChatFormatter.parse("⚖C9Игрок ẲE9" + finalName + " ⚖C9замучен на ẲE9" + timeDisplay)); - sender.sendMessage(ChatFormatter.parse("&#B798A8Причина: &f" + reason)); + Msg.send(sender, ChatFormatter.parse("⚖C9Игрок ẲE9" + finalName + " ⚖C9замучен на ẲE9" + timeDisplay)); + Msg.send(sender, ChatFormatter.parse("&#B798A8Причина: &f" + reason)); if (target != null) { String youMsg = duration == 0 ? hm.getMutedPermanent().replace("{reason}", reason) : hm.getMutedMessage().replace("{reason}", reason).replace("{time}", timeDisplay); - target.sendMessage(ChatFormatter.parse(youMsg)); + Msg.send(target, ChatFormatter.parse(youMsg)); } if (silent) { String msgText = hm.getPlayerMutedSilent().replace("{player}", finalName).replace("{time}", timeDisplay); for (Player p : Bukkit.getOnlinePlayers()) { if (p.hasPermission("lochat.mute.see-silent")) { - p.sendMessage(ChatFormatter.parse(msgText)); + Msg.send(p, ChatFormatter.parse(msgText)); } } } else { String broadcastMsg = (duration == 0 ? hm.getPlayerMutedPermanent() : hm.getPlayerMuted()) .replace("{player}", finalName).replace("{time}", timeDisplay).replace("{reason}", reason); - Bukkit.broadcast(ChatFormatter.parse(broadcastMsg)); + Msg.broadcast(ChatFormatter.parse(broadcastMsg)); } String voiceCmd = plugin.getConfig().getString("moderation.voice-mute-console-command", ""); diff --git a/src/main/java/com/loki/lochat/commands/moderation/mute/MuteHistoryCommand.java b/src/main/java/com/loki/lochat/commands/moderation/mute/MuteHistoryCommand.java index 6966cc6..dac0b97 100644 --- a/src/main/java/com/loki/lochat/commands/moderation/mute/MuteHistoryCommand.java +++ b/src/main/java/com/loki/lochat/commands/moderation/mute/MuteHistoryCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.moderation.mute; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MuteService; import com.loki.lochat.data.model.MuteData; @@ -33,7 +35,7 @@ public class MuteHistoryCommand implements CommandExecutor, TabCompleter { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (!sender.hasPermission("lochat.mutehistory")) { - sender.sendMessage(plugin.getMessageConfig().getComponent("errors.no-permission")); + Msg.send(sender, plugin.getMessageConfig().getComponent("errors.no-permission")); return true; } if (args.length < 1) { @@ -46,7 +48,7 @@ public class MuteHistoryCommand implements CommandExecutor, TabCompleter { uuid = muteService.getUUIDByName(args[0]); } if (uuid == null) { - sender.sendMessage(plugin.getMessageConfig().getComponent("errors.player-not-found")); + Msg.send(sender, plugin.getMessageConfig().getComponent("errors.player-not-found")); return true; } diff --git a/src/main/java/com/loki/lochat/commands/moderation/mute/MuteListCommand.java b/src/main/java/com/loki/lochat/commands/moderation/mute/MuteListCommand.java index 1b722af..00c9af5 100644 --- a/src/main/java/com/loki/lochat/commands/moderation/mute/MuteListCommand.java +++ b/src/main/java/com/loki/lochat/commands/moderation/mute/MuteListCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.moderation.mute; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MuteService; import com.loki.lochat.data.model.MuteData; @@ -29,7 +31,7 @@ public class MuteListCommand implements CommandExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (!sender.hasPermission("lochat.mutelist")) { - sender.sendMessage(plugin.getMessageConfig().getComponent("errors.no-permission")); + Msg.send(sender, plugin.getMessageConfig().getComponent("errors.no-permission")); return true; } diff --git a/src/main/java/com/loki/lochat/commands/moderation/mute/UnmuteCommand.java b/src/main/java/com/loki/lochat/commands/moderation/mute/UnmuteCommand.java index d7f4b6b..54c14ef 100644 --- a/src/main/java/com/loki/lochat/commands/moderation/mute/UnmuteCommand.java +++ b/src/main/java/com/loki/lochat/commands/moderation/mute/UnmuteCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.moderation.mute; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MuteService; import com.loki.lochat.utils.format.ChatFormatter; @@ -30,11 +32,11 @@ public class UnmuteCommand implements CommandExecutor, TabCompleter { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (!sender.hasPermission("lochat.unmute")) { - sender.sendMessage(plugin.getMessageConfig().getComponent("errors.no-permission")); + Msg.send(sender, plugin.getMessageConfig().getComponent("errors.no-permission")); return true; } if (args.length < 1) { - sender.sendMessage(ChatFormatter.parse("&#CF6679Использование: /unmute <ник> [-s]")); + Msg.send(sender, ChatFormatter.parse("&#CF6679Использование: /unmute <ник> [-s]")); return true; } @@ -48,13 +50,13 @@ public class UnmuteCommand implements CommandExecutor, TabCompleter { } } if (silent && !sender.hasPermission("lochat.mute.silent")) { - sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoSilentUnmutePermission())); + Msg.send(sender, ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoSilentUnmutePermission())); return true; } UUID uuid = com.loki.lochat.utils.player.PlayerUtil.findPlayerUUID(targetName); if (uuid == null) { - sender.sendMessage(plugin.getMessageConfig().getComponent("errors.player-not-found")); + Msg.send(sender, plugin.getMessageConfig().getComponent("errors.player-not-found")); return true; } @@ -63,22 +65,22 @@ public class UnmuteCommand implements CommandExecutor, TabCompleter { String op = sender.getName(); if (muteService.unmute(uuid, op)) { - sender.sendMessage(ChatFormatter.parse("⚖C9Игрок ẲE9" + name + " ⚖C9размучен")); + Msg.send(sender, ChatFormatter.parse("⚖C9Игрок ẲE9" + name + " ⚖C9размучен")); if (target != null) { - target.sendMessage(ChatFormatter.parse("⚖C9Вы были ẲE9размучены!")); + Msg.send(target, ChatFormatter.parse("⚖C9Вы были ẲE9размучены!")); } if (silent) { String msg = "&#B798A8[⚖C9ТИХО&#B798A8] ẲE9" + name + " &#B798A8размучен ⚖C9(" + op + ")"; for (Player p : Bukkit.getOnlinePlayers()) { if (p.hasPermission("lochat.mute.see-silent")) { - p.sendMessage(ChatFormatter.parse(msg)); + Msg.send(p, ChatFormatter.parse(msg)); } } } else { - Bukkit.broadcast(ChatFormatter.parse("⚖C9Игрок ẲE9" + name + " ⚖C9размучен модератором ẲE9" + op)); + Msg.broadcast(ChatFormatter.parse("⚖C9Игрок ẲE9" + name + " ⚖C9размучен модератором ẲE9" + op)); } } else { - sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getPlayerNotMutedSimple())); + Msg.send(sender, ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getPlayerNotMutedSimple())); } return true; } diff --git a/src/main/java/com/loki/lochat/commands/moderation/warn/SilentWarnCommand.java b/src/main/java/com/loki/lochat/commands/moderation/warn/SilentWarnCommand.java index 617dbad..9db2c9f 100644 --- a/src/main/java/com/loki/lochat/commands/moderation/warn/SilentWarnCommand.java +++ b/src/main/java/com/loki/lochat/commands/moderation/warn/SilentWarnCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.moderation.warn; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.PunishmentService; import com.loki.lochat.config.MessagesConfig; @@ -37,17 +39,17 @@ public class SilentWarnCommand implements CommandExecutor, TabCompleter { @NotNull String label, @NotNull String[] args) { MessagesConfig msg = plugin.getConfigManager().getMessagesConfig(); if (!sender.hasPermission("lochat.warn.silent")) { - sender.sendMessage(ChatFormatter.parse(msg.getNoPermission())); + Msg.send(sender, ChatFormatter.parse(msg.getNoPermission())); return true; } if (args.length < 1) { - sender.sendMessage(ChatFormatter.parse(msg.getSilentWarnUsage())); + Msg.send(sender, ChatFormatter.parse(msg.getSilentWarnUsage())); return true; } UUID targetUuid = PlayerUtil.findPlayerUUID(args[0]); if (targetUuid == null) { - sender.sendMessage(ChatFormatter.parse(msg.getPlayerNotFound())); + Msg.send(sender, ChatFormatter.parse(msg.getPlayerNotFound())); return true; } @@ -62,12 +64,12 @@ public class SilentWarnCommand implements CommandExecutor, TabCompleter { punishmentService.addWarn(targetUuid, name, mod, reason, true); - sender.sendMessage(ChatFormatter.parse(msg.getWarnStaffConfirm().replace("{player}", name).replace("{reason}", reason))); + Msg.send(sender, ChatFormatter.parse(msg.getWarnStaffConfirm().replace("{player}", name).replace("{reason}", reason))); String staffLine = msg.getSilentWarnStaff().replace("{player}", name).replace("{reason}", reason); for (Player p : Bukkit.getOnlinePlayers()) { if (p.hasPermission("lochat.warn.notify")) { - p.sendMessage(ChatFormatter.parse(staffLine)); + Msg.send(p, ChatFormatter.parse(staffLine)); } } return true; diff --git a/src/main/java/com/loki/lochat/commands/moderation/warn/WarnCommand.java b/src/main/java/com/loki/lochat/commands/moderation/warn/WarnCommand.java index a414758..8da20e0 100644 --- a/src/main/java/com/loki/lochat/commands/moderation/warn/WarnCommand.java +++ b/src/main/java/com/loki/lochat/commands/moderation/warn/WarnCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.moderation.warn; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.PunishmentService; import com.loki.lochat.config.MessagesConfig; @@ -37,22 +39,22 @@ public class WarnCommand implements CommandExecutor, TabCompleter { @NotNull String label, @NotNull String[] args) { MessagesConfig msg = plugin.getConfigManager().getMessagesConfig(); if (!sender.hasPermission("lochat.warn")) { - sender.sendMessage(ChatFormatter.parse(msg.getNoPermission())); + Msg.send(sender, ChatFormatter.parse(msg.getNoPermission())); return true; } if (args.length < 1) { - sender.sendMessage(ChatFormatter.parse(msg.getWarnUsage())); + Msg.send(sender, ChatFormatter.parse(msg.getWarnUsage())); return true; } UUID targetUuid = PlayerUtil.findPlayerUUID(args[0]); if (targetUuid == null) { - sender.sendMessage(ChatFormatter.parse(msg.getPlayerNotFound())); + Msg.send(sender, ChatFormatter.parse(msg.getPlayerNotFound())); return true; } if (sender instanceof Player p && p.getUniqueId().equals(targetUuid)) { - sender.sendMessage(ChatFormatter.parse("&#CF6679Нельзя выдать варн самому себе")); + Msg.send(sender, ChatFormatter.parse("&#CF6679Нельзя выдать варн самому себе")); return true; } @@ -65,16 +67,16 @@ public class WarnCommand implements CommandExecutor, TabCompleter { Player targetOnline = Bukkit.getPlayer(targetUuid); if (targetOnline != null && targetOnline.hasPermission("lochat.bypass.warn")) { - sender.sendMessage(ChatFormatter.parse("&#CF6679У этого игрока защита от варнов")); + Msg.send(sender, ChatFormatter.parse("&#CF6679У этого игрока защита от варнов")); return true; } punishmentService.addWarn(targetUuid, name, mod, reason, false); - sender.sendMessage(ChatFormatter.parse(msg.getWarnStaffConfirm().replace("{player}", name).replace("{reason}", reason))); + Msg.send(sender, ChatFormatter.parse(msg.getWarnStaffConfirm().replace("{player}", name).replace("{reason}", reason))); if (targetOnline != null && targetOnline.isOnline()) { - targetOnline.sendMessage(ChatFormatter.parse(msg.getWarnReceived().replace("{reason}", reason))); + Msg.send(targetOnline, ChatFormatter.parse(msg.getWarnReceived().replace("{reason}", reason))); } return true; } diff --git a/src/main/java/com/loki/lochat/commands/nick/NickCommand.java b/src/main/java/com/loki/lochat/commands/nick/NickCommand.java index f4e97f5..0aaf6d0 100644 --- a/src/main/java/com/loki/lochat/commands/nick/NickCommand.java +++ b/src/main/java/com/loki/lochat/commands/nick/NickCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.nick; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.NickService; import com.loki.lochat.utils.format.ChatFormatter; @@ -29,55 +31,55 @@ public class NickCommand implements CommandExecutor, TabCompleter { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!(sender instanceof Player player)) { - sender.sendMessage(Component.text(plugin.getConfigManager().getMessagesConfig().getPlayerOnly(), NamedTextColor.RED)); + Msg.send(sender, Component.text(plugin.getConfigManager().getMessagesConfig().getPlayerOnly(), NamedTextColor.RED)); return true; } if (!player.hasPermission("lochat.nick")) { - player.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoPermission())); + Msg.send(player, ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoPermission())); return true; } if (args.length == 0) { var nick = nickService.getNickname(player.getUniqueId()); if (nick.isPresent()) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.current", "{nick}", nick.get()))); - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.reset-usage"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.current", "{nick}", nick.get()))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.reset-usage"))); } else { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.no-nick"))); - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.set-usage"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.no-nick"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.set-usage"))); } return true; } if (args[0].equalsIgnoreCase("reset")) { if (nickService.getNickname(player.getUniqueId()).isEmpty()) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.no-nick"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.no-nick"))); return true; } nickService.resetNickname(player.getUniqueId()); - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.reset-success"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.reset-success"))); return true; } String nickname = String.join(" ", args); if (!nickService.isValidNickname(nickname)) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.invalid"))); - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.requirements-header"))); - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.requirements-length"))); - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.requirements-chars"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.invalid"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.requirements-header"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.requirements-length"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.requirements-chars"))); return true; } if (nickService.isNicknameTaken(nickname)) { var cur = nickService.getNickname(player.getUniqueId()); if (cur.isEmpty() || !cur.get().equals(nickname)) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.taken"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.taken"))); return true; } } if (nickService.setNickname(player.getUniqueId(), nickname)) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.set-success", "{nick}", nickname))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.set-success", "{nick}", nickname))); } else { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.error"))); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().get("nick.error"))); } return true; } diff --git a/src/main/java/com/loki/lochat/commands/nick/PlayerInfoCommand.java b/src/main/java/com/loki/lochat/commands/nick/PlayerInfoCommand.java index 5ff34a3..5ef3e01 100644 --- a/src/main/java/com/loki/lochat/commands/nick/PlayerInfoCommand.java +++ b/src/main/java/com/loki/lochat/commands/nick/PlayerInfoCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.nick; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MuteService; import com.loki.lochat.api.service.PunishmentService; @@ -41,17 +43,17 @@ public class PlayerInfoCommand implements CommandExecutor, TabCompleter { public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (!sender.hasPermission("lochat.playerinfo")) { - sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoPermission())); + Msg.send(sender, ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoPermission())); return true; } if (args.length < 1) { - sender.sendMessage(ChatFormatter.parse("&#B798A8Использование: ẲE9/playerinfo <ник>")); + Msg.send(sender, ChatFormatter.parse("&#B798A8Использование: ẲE9/playerinfo <ник>")); return true; } UUID uuid = PlayerUtil.findPlayerUUID(args[0]); if (uuid == null) { - sender.sendMessage(ChatFormatter.parse("&#CF6679Игрок не найден")); + Msg.send(sender, ChatFormatter.parse("&#CF6679Игрок не найден")); return true; } @@ -62,22 +64,22 @@ public class PlayerInfoCommand implements CommandExecutor, TabCompleter { Player target = Bukkit.getPlayer(uuid); - sender.sendMessage(ChatFormatter.parse("═══════ Профиль: " + name + " ═══════")); + Msg.send(sender, ChatFormatter.parse("═══════ Профиль: " + name + " ═══════")); if (target != null) { - sender.sendMessage(ChatFormatter.parse("⚖C9❤ &fЗдоровье: ẲE9" + Math.round(target.getHealth()) + "/20")); - sender.sendMessage(ChatFormatter.parse("⚖C9✦ &fРежим: ẲE9" + target.getGameMode().name().toLowerCase())); - sender.sendMessage(ChatFormatter.parse("⚖C9◆ &fМир: ẲE9" + target.getWorld().getName())); + Msg.send(sender, ChatFormatter.parse("⚖C9❤ &fЗдоровье: ẲE9" + Math.round(target.getHealth()) + "/20")); + Msg.send(sender, ChatFormatter.parse("⚖C9✦ &fРежим: ẲE9" + target.getGameMode().name().toLowerCase())); + Msg.send(sender, ChatFormatter.parse("⚖C9◆ &fМир: ẲE9" + target.getWorld().getName())); } else { - sender.sendMessage(ChatFormatter.parse("&#B798A8Игрок ẲE9не в сети")); + Msg.send(sender, ChatFormatter.parse("&#B798A8Игрок ẲE9не в сети")); } if (plugin.getGradientModule() != null && target != null) { - sender.sendMessage(ChatFormatter.parse("⚖C9⚡ &fГрадиент: " + plugin.getGradientModule().getFormattedName(target))); + Msg.send(sender, ChatFormatter.parse("⚖C9⚡ &fГрадиент: " + plugin.getGradientModule().getFormattedName(target))); } int warns = punishmentService.getWarnCount(uuid); - sender.sendMessage(ChatFormatter.parse("&#CF6679⚠ &fВарнов всего: ẲE9" + warns)); + Msg.send(sender, ChatFormatter.parse("&#CF6679⚠ &fВарнов всего: ẲE9" + warns)); List recent = punishmentService.getWarns(uuid); int show = Math.min(5, recent.size()); @@ -86,7 +88,7 @@ public class PlayerInfoCommand implements CommandExecutor, TabCompleter { String tag = w.silent ? "&#B798A8[тихий] " : ""; String reason = w.reason != null ? w.reason : "—"; long t = w.time; - sender.sendMessage(ChatFormatter.parse( + Msg.send(sender, ChatFormatter.parse( tag + "⚖C9• &7" + formatTime(t) + " &#B798A8от ẲE9" + w.moderator + "⚖C9: &f" + reason)); } @@ -95,27 +97,27 @@ public class PlayerInfoCommand implements CommandExecutor, TabCompleter { String until = ban.isPermanent() ? "&#CF6679навсегда" : "ẲE9" + muteService.formatTime(Math.max(0, ban.until - System.currentTimeMillis())); - sender.sendMessage(ChatFormatter.parse("&#CF6679🚫 &fБан: &cактивен &7(" + until + "&7)")); - sender.sendMessage(ChatFormatter.parse("&#B798A8Причина: &f" + (ban.reason != null ? ban.reason : "—"))); + Msg.send(sender, ChatFormatter.parse("&#CF6679🚫 &fБан: &cактивен &7(" + until + "&7)")); + Msg.send(sender, ChatFormatter.parse("&#B798A8Причина: &f" + (ban.reason != null ? ban.reason : "—"))); } else { - sender.sendMessage(ChatFormatter.parse("⚖C9🚫 &fБан: &aнет")); + Msg.send(sender, ChatFormatter.parse("⚖C9🚫 &fБан: &aнет")); } if (muteService.isMuted(uuid)) { MuteData md = muteService.getMuteData(uuid); long rem = muteService.getRemainingTime(uuid); String remStr = rem < 0 ? "навсегда" : muteService.formatTime(rem); - sender.sendMessage(ChatFormatter.parse("&#CF6679🔇 &fМут: &cда &7(" + remStr + "&7)")); + Msg.send(sender, ChatFormatter.parse("&#CF6679🔇 &fМут: &cда &7(" + remStr + "&7)")); if (md != null) { - sender.sendMessage(ChatFormatter.parse("&#B798A8Причина: &f" + (md.getReason() != null ? md.getReason() : "—"))); + Msg.send(sender, ChatFormatter.parse("&#B798A8Причина: &f" + (md.getReason() != null ? md.getReason() : "—"))); } } else { - sender.sendMessage(ChatFormatter.parse("⚖C9🔇 &fМут: &aнет")); + Msg.send(sender, ChatFormatter.parse("⚖C9🔇 &fМут: &aнет")); } var muteHist = muteService.getPlayerHistory(uuid); if (muteHist != null && !muteHist.isEmpty()) { - sender.sendMessage(ChatFormatter.parse("&#B798A8Записей в истории мутов: ẲE9" + muteHist.size())); + Msg.send(sender, ChatFormatter.parse("&#B798A8Записей в истории мутов: ẲE9" + muteHist.size())); } 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 99cf0b4..9edc95f 100644 --- a/src/main/java/com/loki/lochat/commands/rp/DoCommand.java +++ b/src/main/java/com/loki/lochat/commands/rp/DoCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.rp; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.PlayerService; import com.loki.lochat.commands.base.PlayerCommand; @@ -37,7 +39,7 @@ public class DoCommand extends PlayerCommand { 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)); + Msg.send(player, ChatFormatter.parse(msg)); 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 6d46948..60cf832 100644 --- a/src/main/java/com/loki/lochat/commands/rp/MeCommand.java +++ b/src/main/java/com/loki/lochat/commands/rp/MeCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.rp; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.PlayerService; import com.loki.lochat.commands.base.PlayerCommand; @@ -37,7 +39,7 @@ public class MeCommand extends PlayerCommand { 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)); + Msg.send(player, ChatFormatter.parse(msg)); return true; } diff --git a/src/main/java/com/loki/lochat/commands/rp/RollCommand.java b/src/main/java/com/loki/lochat/commands/rp/RollCommand.java index a870190..fa9da28 100644 --- a/src/main/java/com/loki/lochat/commands/rp/RollCommand.java +++ b/src/main/java/com/loki/lochat/commands/rp/RollCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.rp; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.commands.base.PlayerCommand; import com.loki.lochat.config.RatConfig; @@ -41,19 +43,19 @@ public class RollCommand extends PlayerCommand { try { max = Integer.parseInt(args[0]); if (max < 2) { - player.sendMessage(ChatFormatter.parse( + Msg.send(player, ChatFormatter.parse( plugin.getConfigManager().getString("rp.roll-min-error", "Минимальное значение кубика: 2"))); return true; } if (max > RatConfig.ROLL_ABSOLUTE_MAX) { - player.sendMessage(ChatFormatter.parse( + Msg.send(player, ChatFormatter.parse( plugin.getConfigManager().getString("rp.roll-max-error", "Максимальное значение кубика: 1000000"))); return true; } } catch (NumberFormatException e) { - player.sendMessage(ChatFormatter.parse( + Msg.send(player, ChatFormatter.parse( plugin.getConfigManager().getString("rp.roll-invalid-number", "Укажите число: /roll [максимум]"))); 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 7dfa5a3..8b6d01d 100644 --- a/src/main/java/com/loki/lochat/commands/rp/RpUtil.java +++ b/src/main/java/com/loki/lochat/commands/rp/RpUtil.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.rp; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.utils.player.DistanceUtil; @@ -22,7 +24,7 @@ public final class RpUtil { public static void sendToRadius(Player sender, Component message, int radius) { for (Player player : Bukkit.getOnlinePlayers()) { if (DistanceUtil.isInRange(sender, player, radius)) { - player.sendMessage(message); + Msg.send(player, message); } } } @@ -39,8 +41,10 @@ public final class RpUtil { } catch (Exception ignored) { } - // Пробуем взять displayName (может содержать градиент от NickService) - Component displayName = player.displayName(); + // Пробуем взять displayName (может содержать градиент от NickService) — Spigot: legacy String + String displayNameLegacy = player.getDisplayName(); + Component displayName = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer + .legacySection().deserialize(displayNameLegacy); String plain = PlainTextComponentSerializer.plainText().serialize(displayName); // Если displayName == обычное имя — возвращаем его напрямую (без потери цветов не нужно) if (plain.equals(player.getName())) { 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 5dd38be..b6c4d0b 100644 --- a/src/main/java/com/loki/lochat/commands/rp/TryCommand.java +++ b/src/main/java/com/loki/lochat/commands/rp/TryCommand.java @@ -1,5 +1,7 @@ package com.loki.lochat.commands.rp; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.PlayerService; import com.loki.lochat.commands.base.PlayerCommand; @@ -40,7 +42,7 @@ public class TryCommand extends PlayerCommand { 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)); + Msg.send(player, ChatFormatter.parse(msg)); return true; } diff --git a/src/main/java/com/loki/lochat/config/MessageConfig.java b/src/main/java/com/loki/lochat/config/MessageConfig.java index 1b3f7bd..f643afb 100644 --- a/src/main/java/com/loki/lochat/config/MessageConfig.java +++ b/src/main/java/com/loki/lochat/config/MessageConfig.java @@ -1,5 +1,7 @@ package com.loki.lochat.config; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.utils.format.ChatFormatter; @@ -64,14 +66,14 @@ public class MessageConfig { * Отправляет сообщение игроку */ public void send(CommandSender sender, String path) { - sender.sendMessage(getComponent(path)); + Msg.send(sender, getComponent(path)); } /** * Отправляет сообщение игроку с заменами */ public void send(CommandSender sender, String path, String... replacements) { - sender.sendMessage(getComponent(path, replacements)); + Msg.send(sender, getComponent(path, replacements)); } // Shortcuts diff --git a/src/main/java/com/loki/lochat/core/filter/MuteFilter.java b/src/main/java/com/loki/lochat/core/filter/MuteFilter.java index 5e7662e..797c0b4 100644 --- a/src/main/java/com/loki/lochat/core/filter/MuteFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/MuteFilter.java @@ -1,5 +1,7 @@ package com.loki.lochat.core.filter; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.api.filter.MessageFilter; import com.loki.lochat.api.service.MuteService; import com.loki.lochat.config.MessagesConfig; @@ -49,6 +51,6 @@ public class MuteFilter implements MessageFilter { .replace("{time}", timeStr); } - player.sendMessage(ChatFormatter.parse(msg)); + Msg.send(player, ChatFormatter.parse(msg)); } } 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 f17c6ee..e418c09 100644 --- a/src/main/java/com/loki/lochat/core/service/ChatServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/ChatServiceImpl.java @@ -1,5 +1,7 @@ package com.loki.lochat.core.service; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.ChatService; import com.loki.lochat.api.service.MessageService; @@ -52,9 +54,10 @@ public class ChatServiceImpl implements ChatService { return; } EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, true); + Component senderDisplay = Component.text(sender.getDisplayName()); for (Player p : Bukkit.getOnlinePlayers()) { if (!isGlobalChatDisabled(p.getUniqueId())) { - p.sendMessage(renderer.render(sender, sender.displayName(), msg, p)); + Msg.send(p, renderer.render(sender, senderDisplay, msg, p)); } } } @@ -69,14 +72,15 @@ public class ChatServiceImpl implements ChatService { EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, false); int count = 0; + Component senderDisplay2 = Component.text(sender.getDisplayName()); for (Player p : Bukkit.getOnlinePlayers()) { if (DistanceUtil.isInRange(sender, p, radius)) { - p.sendMessage(renderer.render(sender, sender.displayName(), msg, p)); + Msg.send(p, renderer.render(sender, senderDisplay2, msg, p)); count++; } } if (count <= 1) { - sender.sendMessage(ChatFormatter.parse( + Msg.send(sender, ChatFormatter.parse( plugin.getConfig().getString("messages.local.nobody-heard", "Вас никто не услышал — рядом нет игроков"))); } @@ -135,7 +139,7 @@ public class ChatServiceImpl implements ChatService { int minLength = appearance.getMinMessageLength(); int maxLength = appearance.getMaxMessageLength(); if (text.length() < minLength || text.length() > maxLength) { - sender.sendMessage(ChatFormatter.parse( + Msg.send(sender, ChatFormatter.parse( "&#CF6679Сообщение должно содержать от " + minLength + " до " + maxLength + " символов")); return null; } @@ -148,7 +152,7 @@ public class ChatServiceImpl implements ChatService { if (filter != null) { FilterResult result = filter.filterMessage(sender, text); if (!result.allowed()) { - sender.sendMessage(ChatFormatter.parse(result.blockReason())); + Msg.send(sender, ChatFormatter.parse(result.blockReason())); return null; } text = result.filteredMessage(); diff --git a/src/main/java/com/loki/lochat/core/service/NickServiceImpl.java b/src/main/java/com/loki/lochat/core/service/NickServiceImpl.java index 8419b61..ab9bbcf 100644 --- a/src/main/java/com/loki/lochat/core/service/NickServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/NickServiceImpl.java @@ -1,5 +1,7 @@ package com.loki.lochat.core.service; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.api.service.NickService; import com.loki.lochat.config.RatConfig; import com.loki.lochat.utils.format.ChatFormatter; @@ -99,8 +101,9 @@ public class NickServiceImpl implements NickService { // Сбрасываем display Player onlinePlayer = Bukkit.getPlayer(player); if (onlinePlayer != null) { - onlinePlayer.displayName(Component.text(onlinePlayer.getName())); - onlinePlayer.playerListName(Component.text(onlinePlayer.getName())); + Component reset = Component.text(onlinePlayer.getName()); + Msg.setDisplayName(onlinePlayer, reset); + Msg.setPlayerListName(onlinePlayer, reset); } } @@ -155,12 +158,13 @@ public class NickServiceImpl implements NickService { if (nickname.isPresent()) { // Парсим ник с цветами Component nickComponent = ChatFormatter.parse(nickname.get()); - player.displayName(nickComponent); - player.playerListName(nickComponent); + Msg.setDisplayName(player, nickComponent); + Msg.setPlayerListName(player, nickComponent); } else { // Используем обычное имя - player.displayName(Component.text(player.getName())); - player.playerListName(Component.text(player.getName())); + Component reset = Component.text(player.getName()); + Msg.setDisplayName(player, reset); + Msg.setPlayerListName(player, reset); } } diff --git a/src/main/java/com/loki/lochat/core/service/messaging/PrivateMessageServiceImpl.java b/src/main/java/com/loki/lochat/core/service/messaging/PrivateMessageServiceImpl.java index 6826663..dd22845 100644 --- a/src/main/java/com/loki/lochat/core/service/messaging/PrivateMessageServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/messaging/PrivateMessageServiceImpl.java @@ -1,5 +1,7 @@ package com.loki.lochat.core.service.messaging; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.api.service.PlayerService; @@ -67,14 +69,14 @@ public class PrivateMessageServiceImpl implements PrivateMessageService { playerSender.getUniqueId(), "pm", cooldown); String cooldownMessage = plugin.getConfigManager().getMessagesConfig() .getCooldownMessage().replace("{remaining}", String.valueOf(remaining)); - sender.sendMessage(ChatFormatter.parse(cooldownMessage)); + Msg.send(sender, ChatFormatter.parse(cooldownMessage)); return; } playerService.setCooldown(playerSender.getUniqueId(), "pm"); - sender.sendMessage(ChatFormatter.formatPmSentNew( + Msg.send(sender, ChatFormatter.formatPmSentNew( plugin.getMessageConfig().getPmFormatSent(), playerSender, target, message)); - target.sendMessage(ChatFormatter.formatPmReceivedNew( + Msg.send(target, ChatFormatter.formatPmReceivedNew( plugin.getMessageConfig().getPmFormatReceived(), playerSender, target, message)); if (plugin.getConfigManager().isPmSoundEnabled()) { Sound sound = PlayerUtil.parseSound(plugin.getConfigManager().getPmSoundType(), null); diff --git a/src/main/java/com/loki/lochat/core/service/messaging/SpyService.java b/src/main/java/com/loki/lochat/core/service/messaging/SpyService.java index 0ccd3b8..b3a4bc7 100644 --- a/src/main/java/com/loki/lochat/core/service/messaging/SpyService.java +++ b/src/main/java/com/loki/lochat/core/service/messaging/SpyService.java @@ -1,5 +1,7 @@ package com.loki.lochat.core.service.messaging; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.config.MessageConfig; import com.loki.lochat.utils.format.ChatFormatter; @@ -46,7 +48,7 @@ public class SpyService { 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 + Msg.send(spy, ChatFormatter.parse(format .replace("{sender}", sender.getName()) .replace("{receiver}", receiver.getName()) .replace("{message}", message))); @@ -67,7 +69,7 @@ public class SpyService { for (UUID spyUuid : spyEnabled) { Player spy = Bukkit.getPlayer(spyUuid); if (spy != null && spy.isOnline() && !spy.equals(sender)) { - spy.sendMessage(ChatFormatter.parse(format + Msg.send(spy, ChatFormatter.parse(format .replace("{type}", chatType) .replace("{sender}", sender.getName()) .replace("{message}", plainMessage))); diff --git a/src/main/java/com/loki/lochat/gradient/config/GradientMessages.java b/src/main/java/com/loki/lochat/gradient/config/GradientMessages.java index 9e0916e..72d7367 100644 --- a/src/main/java/com/loki/lochat/gradient/config/GradientMessages.java +++ b/src/main/java/com/loki/lochat/gradient/config/GradientMessages.java @@ -1,5 +1,7 @@ package com.loki.lochat.gradient.config; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.utils.format.ChatFormatter; import net.kyori.adventure.text.Component; @@ -86,13 +88,13 @@ public class GradientMessages { * Отправляет сообщение игроку */ public void send(CommandSender sender, String key) { - sender.sendMessage(getComponent(key)); + Msg.send(sender, getComponent(key)); } /** * Отправляет сообщение игроку с заменами */ public void send(CommandSender sender, String key, String... replacements) { - sender.sendMessage(getComponent(key, replacements)); + Msg.send(sender, getComponent(key, replacements)); } } 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 40a95af..45dda20 100644 --- a/src/main/java/com/loki/lochat/gradient/gui/GradientConfirmGUI.java +++ b/src/main/java/com/loki/lochat/gradient/gui/GradientConfirmGUI.java @@ -1,5 +1,7 @@ package com.loki.lochat.gradient.gui; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.config.RatConfig; import com.loki.lochat.gradient.GradientModule; import com.loki.lochat.gradient.util.GradientUtil; @@ -55,7 +57,7 @@ public final class GradientConfirmGUI implements InventoryHolder { return; } this.inventory = Bukkit.createInventory(this, 27, - Component.text("Подтверждение", NamedTextColor.DARK_GRAY)); + Msg.inventoryTitle(Component.text("Подтверждение", NamedTextColor.DARK_GRAY))); setupItems(); } @@ -72,7 +74,7 @@ public final class GradientConfirmGUI implements InventoryHolder { String preview = buildPreview(); // Для GUI используем LegacyComponentSerializer с поддержкой HEX Component previewComponent = SERIALIZER.deserialize(preview); - skullMeta.displayName(previewComponent.decoration(TextDecoration.ITALIC, false)); + Msg.itemName(skullMeta, previewComponent.decoration(TextDecoration.ITALIC, false)); List lore = new ArrayList<>(); lore.add(Component.empty()); @@ -83,7 +85,7 @@ public final class GradientConfirmGUI implements InventoryHolder { .append(Component.text(price + " PP", NamedTextColor.GOLD)) .decoration(TextDecoration.ITALIC, false)); } - skullMeta.lore(lore); + Msg.itemLore(skullMeta, lore); head.setItemMeta(skullMeta); inventory.setItem(RatConfig.GUI_PREVIEW_SLOT, head); @@ -93,7 +95,7 @@ public final class GradientConfirmGUI implements InventoryHolder { confirmLore.add(Component.empty()); confirmLore.add(Component.text("Нажмите для подтверждения", NamedTextColor.GREEN) .decoration(TextDecoration.ITALIC, false)); - confirmMeta.lore(confirmLore); + Msg.itemLore(confirmMeta, confirmLore); confirm.setItemMeta(confirmMeta); inventory.setItem(RatConfig.GUI_CONFIRM_SLOT, confirm); @@ -103,7 +105,7 @@ public final class GradientConfirmGUI implements InventoryHolder { cancelLore.add(Component.empty()); cancelLore.add(Component.text("Нажмите для отмены", NamedTextColor.RED) .decoration(TextDecoration.ITALIC, false)); - cancelMeta.lore(cancelLore); + Msg.itemLore(cancelMeta, cancelLore); cancel.setItemMeta(cancelMeta); inventory.setItem(RatConfig.GUI_CANCEL_SLOT, cancel); } @@ -187,7 +189,7 @@ public final class GradientConfirmGUI implements InventoryHolder { private ItemStack createItem(Material material, String name) { ItemStack item = new ItemStack(material); ItemMeta meta = item.getItemMeta(); - meta.displayName(LegacyComponentSerializer.legacySection().deserialize(name) + Msg.itemName(meta, LegacyComponentSerializer.legacySection().deserialize(name) .decoration(TextDecoration.ITALIC, false)); item.setItemMeta(meta); return item; 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 c1ac15b..05c478c 100644 --- a/src/main/java/com/loki/lochat/gradient/util/DisplayNameUtil.java +++ b/src/main/java/com/loki/lochat/gradient/util/DisplayNameUtil.java @@ -1,5 +1,7 @@ package com.loki.lochat.gradient.util; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.gradient.GradientModule; import com.loki.lochat.gradient.config.GradientConfig; import com.loki.lochat.gradient.data.GradientPlayerData; @@ -32,8 +34,8 @@ public final class DisplayNameUtil { // Конвертируем в legacy формат для display name displayName = convertMiniMessageToLegacy(displayName); var component = SERIALIZER.deserialize(displayName); - player.displayName(component); - player.playerListName(component); + Msg.setDisplayName(player, component); + Msg.setPlayerListName(player, component); } /** diff --git a/src/main/java/com/loki/lochat/integrations/PlaceholderAPIHook.java b/src/main/java/com/loki/lochat/integrations/PlaceholderAPIHook.java index 72a671d..729f22c 100644 --- a/src/main/java/com/loki/lochat/integrations/PlaceholderAPIHook.java +++ b/src/main/java/com/loki/lochat/integrations/PlaceholderAPIHook.java @@ -40,7 +40,7 @@ public class PlaceholderAPIHook extends PlaceholderExpansion { @Override public @NotNull String getVersion() { - return plugin.getPluginMeta().getVersion(); + return plugin.getDescription().getVersion(); } @Override diff --git a/src/main/java/com/loki/lochat/listener/ChatEventListener.java b/src/main/java/com/loki/lochat/listener/ChatEventListener.java index 95df000..3f0f6e8 100644 --- a/src/main/java/com/loki/lochat/listener/ChatEventListener.java +++ b/src/main/java/com/loki/lochat/listener/ChatEventListener.java @@ -5,21 +5,24 @@ import com.loki.lochat.api.service.ChannelService; import com.loki.lochat.api.service.MessageService; import com.loki.lochat.api.service.PlayerService; import com.loki.lochat.channel.Channel; +import com.loki.lochat.compat.Msg; import com.loki.lochat.core.filter.AdvancedMessageFilter; import com.loki.lochat.core.filter.FilterResult; import com.loki.lochat.renderer.EnhancedChatRenderer; import com.loki.lochat.utils.format.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 org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.plugin.java.JavaPlugin; -import io.papermc.paper.event.player.AsyncChatEvent; +import java.util.HashSet; +import java.util.Set; public class ChatEventListener implements Listener { private final JavaPlugin plugin; @@ -37,10 +40,10 @@ public class ChatEventListener implements Listener { this.channelService = channelService; } - @EventHandler(priority = EventPriority.HIGHEST) - public void onChat(AsyncChatEvent event) { + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onChat(AsyncPlayerChatEvent event) { Player sender = event.getPlayer(); - String rawMessage = PlainTextComponentSerializer.plainText().serialize(event.message()); + String rawMessage = event.getMessage(); LoChat loChat = (LoChat) plugin; // Система каналов: если игрок в присоединённом канале — маршрутизируем туда @@ -54,7 +57,7 @@ public class ChatEventListener implements Listener { ChatMode mode = determineChatMode(loChat, rawMessage); if (mode == null) { event.setCancelled(true); - sender.sendMessage(ChatFormatter.parse("&#CF6679Этот режим чата отключен!")); + Msg.send(sender, ChatFormatter.parse("&#CF6679Этот режим чата отключен!")); return; } @@ -62,7 +65,8 @@ public class ChatEventListener implements Listener { boolean isGlobal = mode.isGlobal; // Проверки: мут, кулдаун, фильтры - if (!processFilters(event, sender, plainMessage, isGlobal)) { + Component messageComponent = processFilters(event, sender, plainMessage, isGlobal); + if (messageComponent == null) { return; } @@ -71,8 +75,17 @@ public class ChatEventListener implements Listener { handleLocalChat(event, sender, loChat); } - // Рендеринг и интеграции - event.renderer(new EnhancedChatRenderer(plugin, isGlobal)); + // Рендеринг и доставка + EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, isGlobal); + Component displayName = buildDisplayName(sender); + Set recipients = new HashSet<>(event.getRecipients()); + event.setCancelled(true); + for (Player viewer : recipients) { + Component rendered = renderer.render(sender, displayName, messageComponent, viewer); + Msg.send(viewer, rendered); + } + Component consoleRendered = renderer.render(sender, displayName, messageComponent, null); + Bukkit.getConsoleSender().sendMessage(Msg.plain(consoleRendered)); loChat.getDiscordIntegration().sendChatMessage(sender, plainMessage, isGlobal); recordStatistics(loChat, sender, isGlobal); } @@ -80,7 +93,7 @@ public class ChatEventListener implements Listener { /** * Маршрутизация сообщения в присоединённый (не-дефолтный) канал. */ - private void handleChannelChat(AsyncChatEvent event, Player sender, Channel channel, String rawMessage) { + private void handleChannelChat(AsyncPlayerChatEvent event, Player sender, Channel channel, String rawMessage) { String plainMessage = rawMessage.stripLeading(); // Кулдаун канала @@ -92,23 +105,21 @@ public class ChatEventListener implements Listener { String msg = ((LoChat) plugin).getConfigManager().getMessagesConfig().getCooldownMessage() .replace("{remaining}", String.valueOf(remaining)); event.setCancelled(true); - sender.sendMessage(ChatFormatter.parse(msg)); + Msg.send(sender, ChatFormatter.parse(msg)); return; } playerService.setCooldown(sender.getUniqueId(), cooldownType); } - if (!processFilters(event, sender, plainMessage, channel.isGlobalScope())) { + Component messageComponent = processFilters(event, sender, plainMessage, channel.isGlobalScope()); + if (messageComponent == null) { return; } // Радиус локального канала if (channel.isLocalScope()) { int radius = channel.radius(); - event.viewers().removeIf(v -> { - if (!(v instanceof Player p)) { - return false; - } + event.getRecipients().removeIf(p -> { try { return !com.loki.lochat.utils.player.DistanceUtil.isInRange(sender, p, radius); } catch (Exception e) { @@ -117,7 +128,16 @@ public class ChatEventListener implements Listener { }); } - event.renderer(new EnhancedChatRenderer(plugin, channel)); + EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, channel); + Component displayName = buildDisplayName(sender); + Set recipients = new HashSet<>(event.getRecipients()); + event.setCancelled(true); + for (Player viewer : recipients) { + Component rendered = renderer.render(sender, displayName, messageComponent, viewer); + Msg.send(viewer, rendered); + } + Component consoleRendered = renderer.render(sender, displayName, messageComponent, null); + Bukkit.getConsoleSender().sendMessage(Msg.plain(consoleRendered)); LoChat loChat = (LoChat) plugin; loChat.getDiscordIntegration().sendChatMessage(sender, plainMessage, channel.isGlobalScope()); playerService.recordMessage(sender.getUniqueId(), "channel_" + channel.id()); @@ -147,21 +167,21 @@ public class ChatEventListener implements Listener { return new ChatMode(isGlobal, message); } - private boolean processFilters(AsyncChatEvent event, Player sender, String plainMessage, boolean isGlobal) { + private Component processFilters(AsyncPlayerChatEvent event, Player sender, String plainMessage, boolean isGlobal) { LoChat loChat = (LoChat) plugin; // Мут, кулдаун if (!messageService.processMessage(sender, plainMessage)) { event.setCancelled(true); - return false; + return null; } // Капс, реклама, повторы FilterResult filterResult = advancedFilter.filterMessage(sender, plainMessage); if (!filterResult.allowed()) { event.setCancelled(true); - sender.sendMessage(ChatFormatter.parse(filterResult.blockReason())); - return false; + Msg.send(sender, ChatFormatter.parse(filterResult.blockReason())); + return null; } String filteredMessage = filterResult.filteredMessage(); @@ -177,17 +197,13 @@ public class ChatEventListener implements Listener { messageComponent = ChatFormatter.parseWithDefaultMessageColor(filteredMessage, defaultColor); } - event.message(messageComponent); - return true; + return messageComponent; } - private void handleLocalChat(AsyncChatEvent event, Player sender, LoChat loChat) { + private void handleLocalChat(AsyncPlayerChatEvent event, Player sender, LoChat loChat) { int radius = loChat.getConfigManager().getAppearanceConfig().getLocalRadius(); - event.viewers().removeIf(v -> { - if (!(v instanceof Player p)) { - return false; - } + event.getRecipients().removeIf(p -> { try { return !com.loki.lochat.utils.player.DistanceUtil.isInRange(sender, p, radius); } catch (Exception e) { @@ -195,18 +211,24 @@ public class ChatEventListener implements Listener { } }); - long recipients = event.viewers().stream() - .filter(v -> v instanceof Player p && !p.equals(sender)) + long recipients = event.getRecipients().stream() + .filter(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 - ); + Bukkit.getScheduler().runTask(plugin, () -> Msg.send(sender, ChatFormatter.parse(msg))); } } + private Component buildDisplayName(Player sender) { + String legacyName = sender.getDisplayName(); + if (legacyName == null || legacyName.isBlank()) { + return Component.text(sender.getName()); + } + return ChatFormatter.parse(legacyName); + } + private void recordStatistics(LoChat loChat, Player sender, boolean isGlobal) { playerService.recordMessage(sender.getUniqueId(), isGlobal ? "global" : "local"); } diff --git a/src/main/java/com/loki/lochat/listener/DiscordEventListener.java b/src/main/java/com/loki/lochat/listener/DiscordEventListener.java index 19f189c..0845a10 100644 --- a/src/main/java/com/loki/lochat/listener/DiscordEventListener.java +++ b/src/main/java/com/loki/lochat/listener/DiscordEventListener.java @@ -2,8 +2,6 @@ package com.loki.lochat.listener; import com.loki.lochat.integrations.DiscordIntegration; -import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; - import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -56,12 +54,11 @@ public class DiscordEventListener implements Listener { if (!discord.isEnabled()) { return; } - + Player player = event.getEntity(); - String deathMessage = event.deathMessage() != null ? - PlainTextComponentSerializer.plainText().serialize(event.deathMessage()) : - "умер"; - + String raw = event.getDeathMessage(); + String deathMessage = raw != null ? raw : "умер"; + discord.sendPlayerDeath(player, deathMessage); } } diff --git a/src/main/java/com/loki/lochat/listener/ModerationListener.java b/src/main/java/com/loki/lochat/listener/ModerationListener.java index 9bd513b..3b4f260 100644 --- a/src/main/java/com/loki/lochat/listener/ModerationListener.java +++ b/src/main/java/com/loki/lochat/listener/ModerationListener.java @@ -1,6 +1,7 @@ package com.loki.lochat.listener; import com.loki.lochat.api.service.PunishmentService; +import com.loki.lochat.compat.Msg; import com.loki.lochat.data.model.BanRecord; import org.bukkit.event.EventHandler; @@ -28,6 +29,6 @@ public class ModerationListener implements Listener { if (ban == null) { return; } - event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED, punishmentService.buildBanKickMessage(ban)); + event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED, Msg.legacy(punishmentService.buildBanKickMessage(ban))); } } diff --git a/src/main/java/com/loki/lochat/listener/PlayerEventListener.java b/src/main/java/com/loki/lochat/listener/PlayerEventListener.java index 8e96066..49b5c1f 100644 --- a/src/main/java/com/loki/lochat/listener/PlayerEventListener.java +++ b/src/main/java/com/loki/lochat/listener/PlayerEventListener.java @@ -4,12 +4,11 @@ import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.api.service.NickService; import com.loki.lochat.api.service.PlayerService; +import com.loki.lochat.compat.Msg; import com.loki.lochat.config.ConfigManager; import com.loki.lochat.core.filter.AdvancedMessageFilter; import com.loki.lochat.utils.format.ChatFormatter; -import net.kyori.adventure.text.Component; - import org.bukkit.entity.Entity; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; @@ -52,7 +51,8 @@ public class PlayerEventListener implements Listener { if (configManager.isJoinMessageEnabled()) { String format = configManager.getJoinMessageFormat(); String message = format.replace("{player}", player.getName()); - event.joinMessage(ChatFormatter.parse(message)); + event.setJoinMessage(null); + Msg.broadcast(ChatFormatter.parse(message)); } } @@ -72,7 +72,8 @@ public class PlayerEventListener implements Listener { if (configManager.isQuitMessageEnabled()) { String format = configManager.getQuitMessageFormat(); String message = format.replace("{player}", player.getName()); - event.quitMessage(ChatFormatter.parse(message)); + event.setQuitMessage(null); + Msg.broadcast(ChatFormatter.parse(message)); } } @@ -82,25 +83,25 @@ public class PlayerEventListener implements Listener { return; } - Player player = event.getPlayer(); + Player player = event.getEntity(); Player killer = player.getKiller(); - + String message; - + if (killer != null) { // Убийство игроком ItemStack weapon = killer.getInventory().getItemInMainHand(); String weaponName = getWeaponName(weapon); - + message = configManager.getDeathPlayerKillFormat() .replace("{player}", player.getName()) .replace("{killer}", killer.getName()) .replace("{weapon}", weaponName); } else { // Проверяем убийцу-моба - Entity lastDamager = player.getLastDamageCause() != null ? - player.getLastDamageCause().getEntity() : null; - + Entity lastDamager = player.getLastDamageCause() != null + ? player.getLastDamageCause().getEntity() : null; + if (lastDamager instanceof LivingEntity mob && !(lastDamager instanceof Player)) { String mobName = getMobName(mob); message = configManager.getDeathMobKillFormat() @@ -108,32 +109,36 @@ public class PlayerEventListener implements Listener { .replace("{killer}", mobName); } else { // Обычная смерть - Component deathMsg = event.deathMessage(); - String deathText = deathMsg != null ? ChatFormatter.toPlain(deathMsg) : "умер"; + String deathMsg = event.getDeathMessage(); + String deathText = deathMsg != null ? ChatFormatter.toPlain(ChatFormatter.parse(deathMsg)) : "умер"; message = configManager.getDeathDefaultFormat() .replace("{player}", player.getName()) .replace("{death_message}", deathText); } } - - event.deathMessage(ChatFormatter.parse(message)); + + event.setDeathMessage(Msg.legacy(ChatFormatter.parse(message))); } private String getWeaponName(ItemStack item) { if (item == null || item.getType().isAir()) { return "Руки"; } - + if (item.hasItemMeta() && item.getItemMeta().hasDisplayName()) { - return ChatFormatter.toPlain(item.getItemMeta().displayName()); + String dn = item.getItemMeta().getDisplayName(); + if (dn != null) { + return ChatFormatter.toPlain(ChatFormatter.parse(dn)); + } } - + return translateMaterial(item.getType().name()); } private String getMobName(LivingEntity mob) { - if (mob.customName() != null) { - return ChatFormatter.toPlain(mob.customName()); + String cn = mob.getCustomName(); + if (cn != null) { + return ChatFormatter.toPlain(ChatFormatter.parse(cn)); } return translateEntityType(mob.getType().name()); } diff --git a/src/main/java/com/loki/lochat/managers/AutoMessageManager.java b/src/main/java/com/loki/lochat/managers/AutoMessageManager.java index 80ace59..f6e7baa 100644 --- a/src/main/java/com/loki/lochat/managers/AutoMessageManager.java +++ b/src/main/java/com/loki/lochat/managers/AutoMessageManager.java @@ -1,8 +1,9 @@ package com.loki.lochat.managers; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.utils.format.ChatFormatter; -import com.loki.lochat.utils.platform.FoliaUtil; import net.kyori.adventure.text.Component; @@ -18,16 +19,13 @@ import java.util.Map; import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; -import io.papermc.paper.threadedregions.scheduler.ScheduledTask; - public class AutoMessageManager { private final LoChat plugin; private final Random random = new Random(); private final AtomicInteger currentIndex = new AtomicInteger(0); - // ✅ FIX: Сохраняем ссылку на задачу для отмены - private Object scheduledTask; // BukkitTask или ScheduledTask + private BukkitTask scheduledTask; private Map> messages; private List messageOrder; @@ -90,37 +88,19 @@ public class AutoMessageManager { int intervalSeconds = plugin.getConfigManager().getAutoMessagesInterval(); long intervalTicks = intervalSeconds * 20L; - // Используем FoliaUtil для совместимости с Paper и Folia - if (FoliaUtil.isFolia()) { - long delayMs = intervalTicks * 50; - long periodMs = intervalTicks * 50; - scheduledTask = Bukkit.getAsyncScheduler().runAtFixedRate( - plugin, - task -> broadcastNextMessage(), - delayMs, - periodMs, - java.util.concurrent.TimeUnit.MILLISECONDS - ); - } else { - scheduledTask = Bukkit.getScheduler().runTaskTimerAsynchronously( - plugin, - this::broadcastNextMessage, - intervalTicks, - intervalTicks - ); - } + scheduledTask = Bukkit.getScheduler().runTaskTimerAsynchronously( + plugin, + this::broadcastNextMessage, + intervalTicks, + intervalTicks + ); plugin.getLogger().info("Автосообщения запущены (интервал: " + intervalSeconds + " сек, режим: " + mode + ")"); } public void stop() { - // ✅ FIX: Отменяем scheduled task if (scheduledTask != null) { - if (scheduledTask instanceof ScheduledTask) { - ((ScheduledTask) scheduledTask).cancel(); - } else if (scheduledTask instanceof BukkitTask) { - ((BukkitTask) scheduledTask).cancel(); - } + scheduledTask.cancel(); scheduledTask = null; } } @@ -159,7 +139,7 @@ public class AutoMessageManager { } for (var player : Bukkit.getOnlinePlayers()) { - player.sendMessage(formatted); + Msg.send(player, formatted); } } } diff --git a/src/main/java/com/loki/lochat/managers/CustomCommandManager.java b/src/main/java/com/loki/lochat/managers/CustomCommandManager.java index 4eed14f..ac93b45 100644 --- a/src/main/java/com/loki/lochat/managers/CustomCommandManager.java +++ b/src/main/java/com/loki/lochat/managers/CustomCommandManager.java @@ -1,5 +1,7 @@ package com.loki.lochat.managers; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.LoChat; import com.loki.lochat.commands.CustomCommand; import com.loki.lochat.managers.commands.MessageSender; @@ -66,9 +68,29 @@ public class CustomCommandManager { plugin.getLogger().info("Загружено " + commands.size() + " кастомных команд"); } + private CommandMap getCommandMap() { + try { + java.lang.reflect.Method m = Bukkit.getServer().getClass().getMethod("getCommandMap"); + return (CommandMap) m.invoke(Bukkit.getServer()); + } catch (Exception ignored) { + } + try { + java.lang.reflect.Method m2 = Bukkit.getPluginManager().getClass().getMethod("getCommandMap"); + return (CommandMap) m2.invoke(Bukkit.getPluginManager()); + } catch (Exception ignored) { + } + try { + java.lang.reflect.Field f = Bukkit.getPluginManager().getClass().getDeclaredField("commandMap"); + f.setAccessible(true); + return (CommandMap) f.get(Bukkit.getPluginManager()); + } catch (Exception ex) { + throw new RuntimeException("Unable to get CommandMap", ex); + } + } + private void registerCommand(CustomCommandData data) { try { - CommandMap commandMap = Bukkit.getCommandMap(); + CommandMap commandMap = getCommandMap(); CustomCommand command = new CustomCommand(data.name(), this); commandMap.register("lochat", command); @@ -108,7 +130,7 @@ public class CustomCommandManager { } if (data.permission() != null && !player.hasPermission(data.permission())) { - player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getNoPermission())); + Msg.send(player, ChatFormatter.parse(plugin.getMessageConfig().getNoPermission())); return true; } @@ -126,7 +148,7 @@ public class CustomCommandManager { private void sendBungeeCommand(Player player, CustomCommandData data) { if (!data.message().isEmpty()) { - player.sendMessage(ChatFormatter.parse(data.message())); + Msg.send(player, ChatFormatter.parse(data.message())); } String serverName = getServerFromConfig(data); @@ -157,7 +179,7 @@ public class CustomCommandManager { player.sendPluginMessage(plugin, "BungeeCord", b.toByteArray()); } catch (Exception e) { plugin.getLogger().severe("Ошибка подключения к серверу " + serverName + ": " + e.getMessage()); - player.sendMessage(ChatFormatter.parse("<#F44336>❌ Ошибка подключения к серверу")); + Msg.send(player, ChatFormatter.parse("<#F44336>❌ Ошибка подключения к серверу")); } } diff --git a/src/main/java/com/loki/lochat/managers/commands/MessageSender.java b/src/main/java/com/loki/lochat/managers/commands/MessageSender.java index e1643ce..a349cd5 100644 --- a/src/main/java/com/loki/lochat/managers/commands/MessageSender.java +++ b/src/main/java/com/loki/lochat/managers/commands/MessageSender.java @@ -1,5 +1,7 @@ package com.loki.lochat.managers.commands; +import com.loki.lochat.compat.Msg; + import com.loki.lochat.utils.format.ChatFormatter; import net.kyori.adventure.text.Component; @@ -17,15 +19,15 @@ 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)); + case "player" -> Msg.send(sender, formatted); + case "broadcast" -> Msg.broadcast(formatted); + case "server" -> Bukkit.getOnlinePlayers().forEach(p -> Msg.send(p, formatted)); default -> { Player targetPlayer = Bukkit.getPlayer(target); if (targetPlayer != null && targetPlayer.isOnline()) { - targetPlayer.sendMessage(formatted); + Msg.send(targetPlayer, formatted); } } } @@ -35,27 +37,23 @@ public class MessageSender { 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( + + Title.Times times = 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)); + case "player" -> Msg.title(sender, title, subtitle, times); + case "broadcast", "server" -> Bukkit.getOnlinePlayers().forEach(p -> Msg.title(p, title, subtitle, times)); default -> { Player targetPlayer = Bukkit.getPlayer(target); if (targetPlayer != null && targetPlayer.isOnline()) { - targetPlayer.showTitle(titleObj); + Msg.title(targetPlayer, title, subtitle, times); } } } @@ -65,12 +63,12 @@ public class MessageSender { Component formatted = ChatFormatter.parse(message); switch (target.toLowerCase()) { - case "player" -> sender.sendActionBar(formatted); - case "broadcast", "server" -> Bukkit.getOnlinePlayers().forEach(p -> p.sendActionBar(formatted)); + case "player" -> Msg.actionBar(sender, formatted); + case "broadcast", "server" -> Bukkit.getOnlinePlayers().forEach(p -> Msg.actionBar(p, formatted)); default -> { Player targetPlayer = Bukkit.getPlayer(target); if (targetPlayer != null && targetPlayer.isOnline()) { - targetPlayer.sendActionBar(formatted); + Msg.actionBar(targetPlayer, 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 index 3274815..ddfeca9 100644 --- a/src/main/java/com/loki/lochat/managers/commands/PlaceholderProcessor.java +++ b/src/main/java/com/loki/lochat/managers/commands/PlaceholderProcessor.java @@ -12,9 +12,7 @@ public class PlaceholderProcessor { // Базовые плейсхолдеры result = result.replace("{player}", player.getName()); - result = result.replace("{displayname}", - net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText() - .serialize(player.displayName())); + 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())); diff --git a/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java b/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java index bd2b2df..416bef0 100644 --- a/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java +++ b/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java @@ -1,6 +1,7 @@ package com.loki.lochat.renderer; import com.loki.lochat.channel.Channel; +import com.loki.lochat.compat.Msg; import com.loki.lochat.renderer.components.EmojiComponent; import com.loki.lochat.renderer.components.GradientBuilder; import com.loki.lochat.renderer.components.UrlProcessor; @@ -9,7 +10,6 @@ import com.loki.lochat.utils.format.ChatFormatter; import com.loki.lochat.utils.format.TextFormatter; import com.loki.lochat.utils.player.MentionHandler; -import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; @@ -24,12 +24,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import io.papermc.paper.chat.ChatRenderer; - /** * Рендерер чата — глобальный и локальный режимы */ -public class EnhancedChatRenderer implements ChatRenderer { +public class EnhancedChatRenderer { private static final TextColor FALLBACK_COLOR = NamedTextColor.WHITE; @@ -57,20 +55,19 @@ public class EnhancedChatRenderer implements ChatRenderer { this.cfg = loChat.getConfigManager(); } - @Override - public Component render(Player source, Component sourceDisplayName, Component message, Audience viewer) { + public Component render(Player source, Component sourceDisplayName, Component message, Player viewer) { // Обрабатываем сообщение Component processed = message; processed = TextFormatter.formatMarkdown(processed); processed = UrlProcessor.process(processed); processed = EmojiComponent.processEmojis(processed); - if (viewer instanceof Player vp) { - processed = mentionHandler.processMentions(processed, source, vp); + if (viewer != null) { + processed = mentionHandler.processMentions(processed, source, viewer); if (cfg.isTranslationEnabled()) { com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; if (loChat.getTranslationService().isEnabled()) { - sendTranslationAsync(vp, PlainTextComponentSerializer.plainText().serialize(processed)); + sendTranslationAsync(viewer, PlainTextComponentSerializer.plainText().serialize(processed)); } } } @@ -78,15 +75,15 @@ public class EnhancedChatRenderer implements ChatRenderer { // Получаем формат из конфига com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig(); String format = isGlobal ? appearanceCfg.getGlobalChatFormat() : appearanceCfg.getLocalChatFormat(); - + // Строим компоненты Component emoji = buildEmojiComponent(); Component prefix = buildPrefix(); Component separator = buildSeparator(); Component playerPrefix = buildPlayerPrefix(source); Component playerSuffix = buildPlayerSuffix(source); - Component player = buildPlayerComponent(source); - + Component player = buildPlayerComponent(source, sourceDisplayName); + return buildFormattedMessage(format, Map.of( "emoji", emoji, "prefix", prefix, @@ -117,62 +114,62 @@ public class EnhancedChatRenderer implements ChatRenderer { private String channelColor() { return channel != null && channel.color() != null ? channel.color() : "&7"; } - + private Component buildFormattedMessage(String format, Map parts) { Component result = Component.empty(); java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\{([^}]+)\\}"); java.util.regex.Matcher matcher = pattern.matcher(format); 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); result = result.append(parts.getOrDefault(placeholder, Component.text("{" + placeholder + "}"))); - + lastEnd = matcher.end(); } - + if (lastEnd < format.length()) { result = result.append(Component.text(format.substring(lastEnd))); } - + return result; } - + private Component buildEmojiComponent() { String emoji = emojiFor(); return emoji.isEmpty() ? Component.empty() : Component.text(emoji); } - + 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 && gradientModule.isEnabled()) { String prefix = gradientModule.getPrefix(player); if (!prefix.isEmpty()) { return com.loki.lochat.utils.format.ChatFormatter.parse(prefix); } } - + return Component.empty(); } private Component buildPlayerSuffix(Player player) { com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; com.loki.lochat.gradient.GradientModule gradientModule = loChat.getGradientModule(); - + if (gradientModule != null && gradientModule.isEnabled()) { String suffix = gradientModule.getSuffix(player); if (!suffix.isEmpty()) { return com.loki.lochat.utils.format.ChatFormatter.parse(suffix); } } - + return Component.empty(); } @@ -196,11 +193,11 @@ public class EnhancedChatRenderer implements ChatRenderer { return Component.text(text, parseColor(color)); } - private Component buildPlayerComponent(Player player) { - Component displayName = player.displayName(); + private Component buildPlayerComponent(Player player, Component sourceDisplayName) { + Component displayName = sourceDisplayName != null ? sourceDisplayName : Component.text(player.getName()); com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig(); - + if (!appearanceCfg.isHoverEnabled()) { return displayName.clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " ")); } @@ -219,7 +216,7 @@ public class EnhancedChatRenderer implements ChatRenderer { "&#B798A8✦ &fНажми чтобы написать" ); } - + net.kyori.adventure.text.TextComponent.Builder hoverBuilder = Component.text(); for (int i = 0; i < hoverLines.size(); i++) { @@ -248,19 +245,24 @@ public class EnhancedChatRenderer implements ChatRenderer { } com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; TranslationService ts = loChat.getTranslationService(); - String viewerLang = viewer.locale().getLanguage(); - ts.translateAsync(plainText, viewerLang) + String localeStr = viewer.getLocale(); + String viewerLang = "en"; + if (localeStr != null && !localeStr.isBlank()) { + viewerLang = localeStr.split("[_\\-]")[0].toLowerCase(); + } + final String lang = viewerLang; + ts.translateAsync(plainText, lang) .thenAccept(translated -> { if (translated.equals(plainText)) { return; } - String langName = LANGUAGE_NAMES.getOrDefault(viewerLang, viewerLang.toUpperCase(java.util.Locale.ROOT)); + String langName = LANGUAGE_NAMES.getOrDefault(lang, lang.toUpperCase(java.util.Locale.ROOT)); Component component = Component.text() .append(com.loki.lochat.utils.format.ChatFormatter.parse( "&#AAAAAA[" + langName + "]&#FFFFFF ")) .append(Component.text(translated)) .build(); - viewer.sendActionBar(component); + Msg.actionBar(viewer, component); }); } diff --git a/src/main/java/com/loki/lochat/utils/format/ChatFormatter.java b/src/main/java/com/loki/lochat/utils/format/ChatFormatter.java index db76dfb..b81813b 100644 --- a/src/main/java/com/loki/lochat/utils/format/ChatFormatter.java +++ b/src/main/java/com/loki/lochat/utils/format/ChatFormatter.java @@ -76,7 +76,7 @@ public final class ChatFormatter { public static void sendMessage(Player player, Component component) { if (player != null && component != null) { - player.sendMessage(component); + com.loki.lochat.compat.Msg.send(player, component); } } diff --git a/src/main/java/com/loki/lochat/utils/platform/FoliaUtil.java b/src/main/java/com/loki/lochat/utils/platform/FoliaUtil.java index 35925f8..cae9300 100644 --- a/src/main/java/com/loki/lochat/utils/platform/FoliaUtil.java +++ b/src/main/java/com/loki/lochat/utils/platform/FoliaUtil.java @@ -5,41 +5,24 @@ import org.bukkit.entity.Entity; import org.bukkit.plugin.java.JavaPlugin; /** - * Утилита для совместимости с Folia + * Утилита для совместимости с Folia — на Spigot 1.20.1 всегда false. */ public class FoliaUtil { private FoliaUtil() { } - private static boolean isFolia; - - static { - try { - Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); - isFolia = true; - } catch (ClassNotFoundException e) { - isFolia = false; - } - } + private static final boolean IS_FOLIA = false; public static boolean isFolia() { - return isFolia; + return IS_FOLIA; } public static void runAsync(JavaPlugin plugin, Runnable task) { - if (isFolia) { - Bukkit.getAsyncScheduler().runNow(plugin, scheduledTask -> task.run()); - } else { - Bukkit.getScheduler().runTaskAsynchronously(plugin, task); - } + Bukkit.getScheduler().runTaskAsynchronously(plugin, task); } public static void runEntityTask(JavaPlugin plugin, Entity entity, Runnable task) { - if (isFolia) { - entity.getScheduler().run(plugin, scheduledTask -> task.run(), null); - } else { - Bukkit.getScheduler().runTask(plugin, task); - } + Bukkit.getScheduler().runTask(plugin, task); } /** @@ -51,17 +34,6 @@ public class FoliaUtil { * @param periodTicks период повторения (в тиках) */ public static void runTimerAsync(JavaPlugin plugin, Runnable task, long delayTicks, long periodTicks) { - if (isFolia) { - // В Folia используем AsyncScheduler для глобальных задач - long delayMs = delayTicks * 50; // 1 тик = 50мс - long periodMs = periodTicks * 50; - Bukkit.getAsyncScheduler().runAtFixedRate( - plugin, scheduledTask -> task.run(), delayMs, periodMs, - java.util.concurrent.TimeUnit.MILLISECONDS - ); - } else { - // В Paper/Spigot используем обычный scheduler - Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, task, delayTicks, periodTicks); - } + Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, task, delayTicks, periodTicks); } } From 83e980d1bb9e5cde127b45e3b87be4d29179ae8d Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 20 Sep 2026 16:44:39 +0200 Subject: [PATCH 08/10] =?UTF-8?q?trim:=20moderation=20=E2=86=92=20mute=20o?= =?UTF-8?q?nly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - remove ban/warn commands: BanCommand (/lban), UnbanCommand (/lunban), WarnCommand (/warn), SilentWarnCommand (silentwarn) and empty ban/warn dirs - plugin.yml: remove commands lban/lunban/warn/silentwarn and permissions lochat.ban/lochat.unban/lochat.warn/lochat.warn.silent/ lochat.warn.notify/lochat.bypass.warn/lochat.bypass.ban/lochat.ban.override; remove LibertyBans from softdepend - remove registration in CommandManager and PluginInitializer (LibertyBansHook, ModerationListener) - delete PunishmentService api+impl, BanRecord/WarnEntry models, ModerationListener, LibertyBansHook; strip ServiceRegistry/PluginInitializer/ LoChat reload and PlayerInfoCommand (warn/ban sections) - config: remove moderation.default-ban-reason/ban-kick-message from config.yml, ban/warn keys from config/messages.yml, escalation header Punishment->Mute in mute.yml - MessagesConfig.java: remove ban/warn getters (keep mute) - README: remove /lban /warn rows, update features to mute-only - preserve /lmute /lunmute /lmutelist /lmuteblame, MuteService, mute.yml, mutes.json format and HIGHEST chat handler --- README.md | 8 +- src/main/java/com/loki/lochat/LoChat.java | 10 +- .../lochat/api/service/PunishmentService.java | 39 ---- .../loki/lochat/commands/CommandManager.java | 8 - .../commands/moderation/ban/BanCommand.java | 159 -------------- .../commands/moderation/ban/UnbanCommand.java | 85 -------- .../moderation/warn/SilentWarnCommand.java | 92 -------- .../commands/moderation/warn/WarnCommand.java | 112 ---------- .../commands/nick/PlayerInfoCommand.java | 37 +--- .../loki/lochat/config/MessagesConfig.java | 61 +----- .../loki/lochat/core/PluginInitializer.java | 12 +- .../com/loki/lochat/core/PluginShutdown.java | 6 - .../lochat/core/registry/ServiceRegistry.java | 3 - .../core/service/PunishmentServiceImpl.java | 199 ------------------ .../com/loki/lochat/data/model/BanRecord.java | 39 ---- .../com/loki/lochat/data/model/WarnEntry.java | 21 -- .../lochat/integrations/LibertyBansHook.java | 39 ---- .../lochat/listener/ModerationListener.java | 34 --- src/main/resources/config.yml | 2 - src/main/resources/config/messages.yml | 15 -- src/main/resources/config/mute.yml | 7 +- src/main/resources/plugin.yml | 49 ----- 22 files changed, 7 insertions(+), 1030 deletions(-) delete mode 100644 src/main/java/com/loki/lochat/api/service/PunishmentService.java delete mode 100644 src/main/java/com/loki/lochat/commands/moderation/ban/BanCommand.java delete mode 100644 src/main/java/com/loki/lochat/commands/moderation/ban/UnbanCommand.java delete mode 100644 src/main/java/com/loki/lochat/commands/moderation/warn/SilentWarnCommand.java delete mode 100644 src/main/java/com/loki/lochat/commands/moderation/warn/WarnCommand.java delete mode 100644 src/main/java/com/loki/lochat/core/service/PunishmentServiceImpl.java delete mode 100644 src/main/java/com/loki/lochat/data/model/BanRecord.java delete mode 100644 src/main/java/com/loki/lochat/data/model/WarnEntry.java delete mode 100644 src/main/java/com/loki/lochat/integrations/LibertyBansHook.java delete mode 100644 src/main/java/com/loki/lochat/listener/ModerationListener.java diff --git a/README.md b/README.md index e71505a..7db2fa9 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ LoChat is a powerful Minecraft chat plugin for Paper 1.19.2+ and Folia servers. | Private messages | PM system with reply support and ignore list | | Gradient nicknames | Up to 7 colors with built-in presets (fire, ocean, rainbow) | | Mentions | @player, @everyone, @here, @role with sounds | -| Moderation | Mute, ban, warn with history, silent mode, escalation | +| Moderation | Mute with history, silent mode, escalation | | Chat filters | Swear, spam, flood, CAPS, URL/IP, hidden links | | RP commands | /me, /do, /try, /roll with configurable radius | | Custom join/quit/death | Messages with weapon name and hover events | @@ -55,8 +55,6 @@ LoChat is a powerful Minecraft chat plugin for Paper 1.19.2+ and Folia servers. | `/prefix ` | `gradient.prefix` | Set custom prefix | | `/lmute [time] [-s] [reason]` | `lochat.mute` | Mute player | | `/lunmute [-s]` | `lochat.unmute` | Unmute player | -| `/lban [time] [reason]` | `lochat.ban` | Ban player | -| `/warn [reason]` | `lochat.warn` | Warn player | | `/chatspy` | `chat.spy` | Toggle spy mode | | `/clearchat` | `chat.clear` | Clear chat | | `/lochat reload` | `lochat.admin` | Reload config | @@ -90,7 +88,7 @@ LoChat - мощный чат-плагин для Paper 1.19.2+ и Folia серв | Личные сообщения | PM с ответом и списком игнора | | Градиентные ники | До 7 цветов с пресетами (огонь, океан, радуга) | | Упоминания | @player, @everyone, @here, @role со звуками | -| Модерация | Мут, бан, варн с историей, тихий режим, эскалация | +| Модерация | Мут с историей, тихий режим, эскалация | | Фильтры чата | Мат, спам, флуд, CAPS, URL/IP, скрытые ссылки | | RP команды | /me, /do, /try, /roll с настраиваемым радиусом | | Кастомные вход/выход/смерть | Сообщения с названием оружия и hover-событиями | @@ -109,8 +107,6 @@ LoChat - мощный чат-плагин для Paper 1.19.2+ и Folia серв | `/msg <игрок> <сообщение>` | `chat.pm.use` | Личное сообщение | | `/color [hex2]` | `gradient.color` | Установить градиент | | `/lmute <игрок> [время] [-s] [причина]` | `lochat.mute` | Замутить | -| `/lban <игрок> [время] [причина]` | `lochat.ban` | Забанить | -| `/warn <игрок> [причина]` | `lochat.warn` | Выдать варн | | `/lochat reload` | `lochat.admin` | Перезагрузить конфиг | ### Зависимости diff --git a/src/main/java/com/loki/lochat/LoChat.java b/src/main/java/com/loki/lochat/LoChat.java index 90115f2..5b8b375 100644 --- a/src/main/java/com/loki/lochat/LoChat.java +++ b/src/main/java/com/loki/lochat/LoChat.java @@ -4,7 +4,6 @@ import com.loki.lochat.api.service.MessageService; import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.api.service.NickService; import com.loki.lochat.api.service.PlayerService; -import com.loki.lochat.api.service.PunishmentService; import com.loki.lochat.commands.CommandManager; import com.loki.lochat.config.ConfigManager; import com.loki.lochat.config.MessageConfig; @@ -83,8 +82,7 @@ public final class LoChat extends JavaPlugin { serviceRegistry.get(MessageService.class), serviceRegistry.get(PlayerService.class), serviceRegistry.get(MessagingService.class), - serviceRegistry.get(NickService.class), - serviceRegistry.get(PunishmentService.class)); + serviceRegistry.get(NickService.class)); initializer.registerPluginChannels(); // Автосообщения @@ -112,12 +110,6 @@ public final class LoChat extends JavaPlugin { if (discordIntegration != null) { discordIntegration.reload(); } - if (serviceRegistry != null) { - try { - serviceRegistry.get(PunishmentService.class).save(); - } catch (IllegalStateException ignored) { - } - } } // Геттеры diff --git a/src/main/java/com/loki/lochat/api/service/PunishmentService.java b/src/main/java/com/loki/lochat/api/service/PunishmentService.java deleted file mode 100644 index 6035c28..0000000 --- a/src/main/java/com/loki/lochat/api/service/PunishmentService.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.loki.lochat.api.service; - -import com.loki.lochat.data.model.BanRecord; -import com.loki.lochat.data.model.WarnEntry; - -import net.kyori.adventure.text.Component; - -import java.util.List; -import java.util.UUID; - -/** - * Варны и баны LoChat - */ -public interface PunishmentService { - - void addWarn(UUID uuid, String playerName, String moderator, String reason, boolean silent); - - List getWarns(UUID uuid); - - int getWarnCount(UUID uuid); - - /** - * @param durationMs длительность; 0 = навсегда - */ - void ban(UUID uuid, String playerName, long durationMs, String reason, String bannedBy); - - boolean unban(UUID uuid); - - boolean isBanned(UUID uuid); - - BanRecord getActiveBan(UUID uuid); - - /** Сообщение при кике (MiniMessage + legacy &#) */ - Component buildBanKickMessage(BanRecord ban); - - void save(); - - void reload(); -} diff --git a/src/main/java/com/loki/lochat/commands/CommandManager.java b/src/main/java/com/loki/lochat/commands/CommandManager.java index 998bdfd..bfaf9cf 100644 --- a/src/main/java/com/loki/lochat/commands/CommandManager.java +++ b/src/main/java/com/loki/lochat/commands/CommandManager.java @@ -18,15 +18,11 @@ import com.loki.lochat.commands.messaging.IgnoreListCommand; import com.loki.lochat.commands.messaging.MsgCommand; import com.loki.lochat.commands.messaging.ReplyCommand; import com.loki.lochat.commands.messaging.UnignoreCommand; -import com.loki.lochat.commands.moderation.ban.BanCommand; -import com.loki.lochat.commands.moderation.ban.UnbanCommand; import com.loki.lochat.commands.moderation.mute.MuteBlameCommand; import com.loki.lochat.commands.moderation.mute.MuteCommand; import com.loki.lochat.commands.moderation.mute.MuteHistoryCommand; import com.loki.lochat.commands.moderation.mute.MuteListCommand; import com.loki.lochat.commands.moderation.mute.UnmuteCommand; -import com.loki.lochat.commands.moderation.warn.SilentWarnCommand; -import com.loki.lochat.commands.moderation.warn.WarnCommand; import com.loki.lochat.commands.nick.NickCommand; import com.loki.lochat.commands.nick.PlayerInfoCommand; import com.loki.lochat.commands.rp.DoCommand; @@ -75,10 +71,6 @@ public class CommandManager { reg("lmutelist", new MuteListCommand(plugin)); regTab("lmutehistory", new MuteHistoryCommand(plugin)); regTab("lmuteblame", new MuteBlameCommand(plugin)); - regTab("warn", new WarnCommand(plugin)); - regTab("silentwarn", new SilentWarnCommand(plugin)); - regTab("lban", new BanCommand(plugin)); - regTab("lunban", new UnbanCommand(plugin)); // ── Ник / профиль ───────────────────────────────────────────────────── regTab("nick", new NickCommand(plugin)); diff --git a/src/main/java/com/loki/lochat/commands/moderation/ban/BanCommand.java b/src/main/java/com/loki/lochat/commands/moderation/ban/BanCommand.java deleted file mode 100644 index 85c8079..0000000 --- a/src/main/java/com/loki/lochat/commands/moderation/ban/BanCommand.java +++ /dev/null @@ -1,159 +0,0 @@ -package com.loki.lochat.commands.moderation.ban; - -import com.loki.lochat.compat.Msg; - -import com.loki.lochat.LoChat; -import com.loki.lochat.api.service.MuteService; -import com.loki.lochat.api.service.PunishmentService; -import com.loki.lochat.config.MessagesConfig; -import com.loki.lochat.data.model.BanRecord; -import com.loki.lochat.utils.format.ChatFormatter; -import com.loki.lochat.utils.player.PlayerUtil; - -import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -/** - * /lban — бан с красивым киком - */ -public class BanCommand implements CommandExecutor, TabCompleter { - - private final LoChat plugin; - private final PunishmentService punishmentService; - private final MuteService muteService; - - public BanCommand(LoChat plugin) { - this.plugin = plugin; - this.punishmentService = plugin.getServiceRegistry().get(PunishmentService.class); - this.muteService = plugin.getServiceRegistry().get(MuteService.class); - } - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, - @NotNull String label, @NotNull String[] args) { - MessagesConfig msg = plugin.getConfigManager().getMessagesConfig(); - if (!sender.hasPermission("lochat.ban")) { - Msg.send(sender, ChatFormatter.parse(msg.getNoPermission())); - return true; - } - if (args.length < 1) { - Msg.send(sender, ChatFormatter.parse(msg.getBanUsage())); - Msg.send(sender, ChatFormatter.parse(msg.getMuteTimeHelp())); - return true; - } - - String targetName = args[0]; - UUID targetUuid = PlayerUtil.findPlayerUUID(targetName); - if (targetUuid == null) { - Msg.send(sender, ChatFormatter.parse(msg.getPlayerNotFound())); - return true; - } - - if (sender instanceof Player p && p.getUniqueId().equals(targetUuid)) { - Msg.send(sender, ChatFormatter.parse("&#CF6679Нельзя забанить самого себя")); - return true; - } - - Player onlineTarget = Bukkit.getPlayer(targetUuid); - if (onlineTarget != null && onlineTarget.hasPermission("lochat.bypass.ban") && !sender.hasPermission("lochat.ban.override")) { - Msg.send(sender, ChatFormatter.parse("&#CF6679Нельзя забанить этого игрока")); - return true; - } - - if (punishmentService.isBanned(targetUuid)) { - Msg.send(sender, ChatFormatter.parse(msg.getAlreadyBanned())); - return true; - } - - DurationResult parsed = parseDuration(sender, args, msg); - if (!parsed.valid) { - return true; - } - - long durationMs = parsed.durationMs; - String reason = parsed.reason; - String finalName = PlayerUtil.getPlayerName(targetUuid); - if (finalName == null) { - finalName = targetName; - } - - punishmentService.ban(targetUuid, finalName, durationMs, reason, sender.getName()); - - String durationLabel = durationMs == 0 - ? msg.getModerationDurationPermanent() - : muteService.formatTime(durationMs); - Msg.send(sender, ChatFormatter.parse(msg.getBanConfirm().replace("{player}", finalName).replace("{duration}", durationLabel))); - - BanRecord ban = punishmentService.getActiveBan(targetUuid); - if (ban != null) { - Player kicked = Bukkit.getPlayer(targetUuid); - if (kicked != null && kicked.isOnline()) { - Msg.kick(kicked, punishmentService.buildBanKickMessage(ban)); - } - } - return true; - } - - private record DurationResult(long durationMs, String reason, boolean valid) { } - - private DurationResult parseDuration(CommandSender sender, String[] args, MessagesConfig msg) { - String timeStr = null; - StringBuilder reasonBuilder = new StringBuilder(); - for (int i = 1; i < args.length; i++) { - if (timeStr == null && isTimeFormat(args[i])) { - timeStr = args[i]; - } else { - if (reasonBuilder.length() > 0) { - reasonBuilder.append(" "); - } - reasonBuilder.append(args[i]); - } - } - - long durationMs; - if (timeStr == null || timeStr.isEmpty()) { - durationMs = 0; - } else if ("perm".equalsIgnoreCase(timeStr) || "0".equals(timeStr)) { - durationMs = 0; - } else { - durationMs = muteService.parseTime(timeStr); - if (durationMs < 0) { - Msg.send(sender, ChatFormatter.parse(msg.getInvalidTime())); - return new DurationResult(0, "", false); - } - } - - String reason = reasonBuilder.length() > 0 ? reasonBuilder.toString() : msg.getModerationDefaultReason(); - return new DurationResult(durationMs, reason, true); - } - - private boolean isTimeFormat(String s) { - return "perm".equalsIgnoreCase(s) || s.matches("\\d+[dhms]?"); - } - - @Override - public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, - @NotNull String alias, @NotNull String[] args) { - List c = new ArrayList<>(); - if (args.length == 1) { - for (Player p : Bukkit.getOnlinePlayers()) { - if (p.getName().toLowerCase().startsWith(args[0].toLowerCase())) { - c.add(p.getName()); - } - } - } else if (args.length == 2) { - c.addAll(List.of("1h", "1d", "7d", "30d", "perm")); - } - return c; - } -} diff --git a/src/main/java/com/loki/lochat/commands/moderation/ban/UnbanCommand.java b/src/main/java/com/loki/lochat/commands/moderation/ban/UnbanCommand.java deleted file mode 100644 index d575623..0000000 --- a/src/main/java/com/loki/lochat/commands/moderation/ban/UnbanCommand.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.loki.lochat.commands.moderation.ban; - -import com.loki.lochat.compat.Msg; - -import com.loki.lochat.LoChat; -import com.loki.lochat.api.service.PunishmentService; -import com.loki.lochat.config.MessagesConfig; -import com.loki.lochat.utils.format.ChatFormatter; -import com.loki.lochat.utils.player.PlayerUtil; - -import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -/** - * /lunban — снять бан - */ -public class UnbanCommand implements CommandExecutor, TabCompleter { - - private final LoChat plugin; - private final PunishmentService punishmentService; - - public UnbanCommand(LoChat plugin) { - this.plugin = plugin; - this.punishmentService = plugin.getServiceRegistry().get(PunishmentService.class); - } - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, - @NotNull String label, @NotNull String[] args) { - MessagesConfig msg = plugin.getConfigManager().getMessagesConfig(); - if (!sender.hasPermission("lochat.unban")) { - Msg.send(sender, ChatFormatter.parse(msg.getNoPermission())); - return true; - } - if (args.length < 1) { - Msg.send(sender, ChatFormatter.parse("&#B798A8Использование: ẲE9/lunban <ник>")); - return true; - } - - UUID uuid = PlayerUtil.findPlayerUUID(args[0]); - if (uuid == null) { - Msg.send(sender, ChatFormatter.parse(msg.getPlayerNotFound())); - return true; - } - - if (!punishmentService.isBanned(uuid)) { - Msg.send(sender, ChatFormatter.parse(msg.getNotBanned())); - return true; - } - - String name = PlayerUtil.getPlayerName(uuid); - if (name == null) { - name = args[0]; - } - - punishmentService.unban(uuid); - Msg.send(sender, ChatFormatter.parse(msg.getUnbanConfirm().replace("{player}", name))); - return true; - } - - @Override - public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, - @NotNull String alias, @NotNull String[] args) { - List out = new ArrayList<>(); - if (args.length == 1) { - String p = args[0].toLowerCase(); - for (Player pl : Bukkit.getOnlinePlayers()) { - if (pl.getName().toLowerCase().startsWith(p)) { - out.add(pl.getName()); - } - } - } - return out; - } -} diff --git a/src/main/java/com/loki/lochat/commands/moderation/warn/SilentWarnCommand.java b/src/main/java/com/loki/lochat/commands/moderation/warn/SilentWarnCommand.java deleted file mode 100644 index 9db2c9f..0000000 --- a/src/main/java/com/loki/lochat/commands/moderation/warn/SilentWarnCommand.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.loki.lochat.commands.moderation.warn; - -import com.loki.lochat.compat.Msg; - -import com.loki.lochat.LoChat; -import com.loki.lochat.api.service.PunishmentService; -import com.loki.lochat.config.MessagesConfig; -import com.loki.lochat.utils.format.ChatFormatter; -import com.loki.lochat.utils.player.PlayerUtil; - -import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -/** - * /silentwarn — варн в базе, цель не видит; модераторы с lochat.warn.notify видят - */ -public class SilentWarnCommand implements CommandExecutor, TabCompleter { - - private final LoChat plugin; - private final PunishmentService punishmentService; - - public SilentWarnCommand(LoChat plugin) { - this.plugin = plugin; - this.punishmentService = plugin.getServiceRegistry().get(PunishmentService.class); - } - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, - @NotNull String label, @NotNull String[] args) { - MessagesConfig msg = plugin.getConfigManager().getMessagesConfig(); - if (!sender.hasPermission("lochat.warn.silent")) { - Msg.send(sender, ChatFormatter.parse(msg.getNoPermission())); - return true; - } - if (args.length < 1) { - Msg.send(sender, ChatFormatter.parse(msg.getSilentWarnUsage())); - return true; - } - - UUID targetUuid = PlayerUtil.findPlayerUUID(args[0]); - if (targetUuid == null) { - Msg.send(sender, ChatFormatter.parse(msg.getPlayerNotFound())); - return true; - } - - String mod = sender.getName(); - String reason = args.length > 1 - ? String.join(" ", java.util.Arrays.copyOfRange(args, 1, args.length)) - : msg.getModerationDefaultReason(); - String name = PlayerUtil.getPlayerName(targetUuid); - if (name == null) { - name = args[0]; - } - - punishmentService.addWarn(targetUuid, name, mod, reason, true); - - Msg.send(sender, ChatFormatter.parse(msg.getWarnStaffConfirm().replace("{player}", name).replace("{reason}", reason))); - - String staffLine = msg.getSilentWarnStaff().replace("{player}", name).replace("{reason}", reason); - for (Player p : Bukkit.getOnlinePlayers()) { - if (p.hasPermission("lochat.warn.notify")) { - Msg.send(p, ChatFormatter.parse(staffLine)); - } - } - return true; - } - - @Override - public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, - @NotNull String alias, @NotNull String[] args) { - List out = new ArrayList<>(); - if (args.length == 1) { - String p = args[0].toLowerCase(); - for (Player pl : Bukkit.getOnlinePlayers()) { - if (pl.getName().toLowerCase().startsWith(p)) { - out.add(pl.getName()); - } - } - } - return out; - } -} diff --git a/src/main/java/com/loki/lochat/commands/moderation/warn/WarnCommand.java b/src/main/java/com/loki/lochat/commands/moderation/warn/WarnCommand.java deleted file mode 100644 index 8da20e0..0000000 --- a/src/main/java/com/loki/lochat/commands/moderation/warn/WarnCommand.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.loki.lochat.commands.moderation.warn; - -import com.loki.lochat.compat.Msg; - -import com.loki.lochat.LoChat; -import com.loki.lochat.api.service.PunishmentService; -import com.loki.lochat.config.MessagesConfig; -import com.loki.lochat.utils.format.ChatFormatter; -import com.loki.lochat.utils.player.PlayerUtil; - -import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -/** - * /warn — игрок видит предупреждение в чате - */ -public class WarnCommand implements CommandExecutor, TabCompleter { - - private final LoChat plugin; - private final PunishmentService punishmentService; - - public WarnCommand(LoChat plugin) { - this.plugin = plugin; - this.punishmentService = plugin.getServiceRegistry().get(PunishmentService.class); - } - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, - @NotNull String label, @NotNull String[] args) { - MessagesConfig msg = plugin.getConfigManager().getMessagesConfig(); - if (!sender.hasPermission("lochat.warn")) { - Msg.send(sender, ChatFormatter.parse(msg.getNoPermission())); - return true; - } - if (args.length < 1) { - Msg.send(sender, ChatFormatter.parse(msg.getWarnUsage())); - return true; - } - - UUID targetUuid = PlayerUtil.findPlayerUUID(args[0]); - if (targetUuid == null) { - Msg.send(sender, ChatFormatter.parse(msg.getPlayerNotFound())); - return true; - } - - if (sender instanceof Player p && p.getUniqueId().equals(targetUuid)) { - Msg.send(sender, ChatFormatter.parse("&#CF6679Нельзя выдать варн самому себе")); - return true; - } - - String mod = sender.getName(); - String reason = buildReason(msg, args, 1); - String name = PlayerUtil.getPlayerName(targetUuid); - if (name == null) { - name = args[0]; - } - - Player targetOnline = Bukkit.getPlayer(targetUuid); - if (targetOnline != null && targetOnline.hasPermission("lochat.bypass.warn")) { - Msg.send(sender, ChatFormatter.parse("&#CF6679У этого игрока защита от варнов")); - return true; - } - - punishmentService.addWarn(targetUuid, name, mod, reason, false); - - Msg.send(sender, ChatFormatter.parse(msg.getWarnStaffConfirm().replace("{player}", name).replace("{reason}", reason))); - - if (targetOnline != null && targetOnline.isOnline()) { - Msg.send(targetOnline, ChatFormatter.parse(msg.getWarnReceived().replace("{reason}", reason))); - } - return true; - } - - private static String buildReason(MessagesConfig msg, String[] args, int from) { - if (args.length <= from) { - return msg.getModerationDefaultReason(); - } - StringBuilder sb = new StringBuilder(); - for (int i = from; i < args.length; i++) { - if (sb.length() > 0) { - sb.append(" "); - } - sb.append(args[i]); - } - return sb.toString(); - } - - @Override - public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, - @NotNull String alias, @NotNull String[] args) { - List out = new ArrayList<>(); - if (args.length == 1) { - String p = args[0].toLowerCase(); - for (Player pl : Bukkit.getOnlinePlayers()) { - if (pl.getName().toLowerCase().startsWith(p)) { - out.add(pl.getName()); - } - } - } - return out; - } -} diff --git a/src/main/java/com/loki/lochat/commands/nick/PlayerInfoCommand.java b/src/main/java/com/loki/lochat/commands/nick/PlayerInfoCommand.java index 5ef3e01..82d6e18 100644 --- a/src/main/java/com/loki/lochat/commands/nick/PlayerInfoCommand.java +++ b/src/main/java/com/loki/lochat/commands/nick/PlayerInfoCommand.java @@ -4,10 +4,7 @@ import com.loki.lochat.compat.Msg; import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MuteService; -import com.loki.lochat.api.service.PunishmentService; -import com.loki.lochat.data.model.BanRecord; import com.loki.lochat.data.model.MuteData; -import com.loki.lochat.data.model.WarnEntry; import com.loki.lochat.utils.format.ChatFormatter; import com.loki.lochat.utils.player.PlayerUtil; @@ -25,18 +22,16 @@ import java.util.List; import java.util.UUID; /** - * /playerinfo — статистика варнов, банов и мутов + * /playerinfo — статистика мутов (mute-only) */ public class PlayerInfoCommand implements CommandExecutor, TabCompleter { private final LoChat plugin; private final MuteService muteService; - private final PunishmentService punishmentService; public PlayerInfoCommand(LoChat plugin) { this.plugin = plugin; this.muteService = plugin.getServiceRegistry().get(MuteService.class); - this.punishmentService = plugin.getServiceRegistry().get(PunishmentService.class); } @Override @@ -78,31 +73,6 @@ public class PlayerInfoCommand implements CommandExecutor, TabCompleter { Msg.send(sender, ChatFormatter.parse("⚖C9⚡ &fГрадиент: " + plugin.getGradientModule().getFormattedName(target))); } - int warns = punishmentService.getWarnCount(uuid); - Msg.send(sender, ChatFormatter.parse("&#CF6679⚠ &fВарнов всего: ẲE9" + warns)); - - List recent = punishmentService.getWarns(uuid); - int show = Math.min(5, recent.size()); - for (int i = recent.size() - show; i < recent.size(); i++) { - WarnEntry w = recent.get(i); - String tag = w.silent ? "&#B798A8[тихий] " : ""; - String reason = w.reason != null ? w.reason : "—"; - long t = w.time; - Msg.send(sender, ChatFormatter.parse( - tag + "⚖C9• &7" + formatTime(t) + " &#B798A8от ẲE9" + w.moderator + "⚖C9: &f" + reason)); - } - - BanRecord ban = punishmentService.getActiveBan(uuid); - if (ban != null) { - String until = ban.isPermanent() - ? "&#CF6679навсегда" - : "ẲE9" + muteService.formatTime(Math.max(0, ban.until - System.currentTimeMillis())); - Msg.send(sender, ChatFormatter.parse("&#CF6679🚫 &fБан: &cактивен &7(" + until + "&7)")); - Msg.send(sender, ChatFormatter.parse("&#B798A8Причина: &f" + (ban.reason != null ? ban.reason : "—"))); - } else { - Msg.send(sender, ChatFormatter.parse("⚖C9🚫 &fБан: &aнет")); - } - if (muteService.isMuted(uuid)) { MuteData md = muteService.getMuteData(uuid); long rem = muteService.getRemainingTime(uuid); @@ -123,11 +93,6 @@ public class PlayerInfoCommand implements CommandExecutor, TabCompleter { return true; } - private static String formatTime(long ms) { - java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm"); - return sdf.format(new java.util.Date(ms)); - } - @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/config/MessagesConfig.java b/src/main/java/com/loki/lochat/config/MessagesConfig.java index 46a4803..7053f45 100644 --- a/src/main/java/com/loki/lochat/config/MessagesConfig.java +++ b/src/main/java/com/loki/lochat/config/MessagesConfig.java @@ -157,7 +157,7 @@ public class MessagesConfig extends BaseConfig { return getConfig().getString("ignore.ignore_list_header", "&#B798A8Игнорируемые игроки⚖C9:"); } - // ── Модерация (варны / баны) ──────────────────────────────────────────────── + // ── Модерация ──────────────────────────────────────────────────────────────── public String getModerationDefaultReason() { return getConfig().getString("moderation.default_reason", "Без причины"); @@ -166,63 +166,4 @@ public class MessagesConfig extends BaseConfig { public String getModerationDurationPermanent() { return getConfig().getString("moderation.duration_permanent", "навсегда"); } - - /** - * MiniMessage + legacy &# цвета; плейсхолдеры {reason},{moderator},{duration},{player}; перевод строки \\n - */ - public String getBanKickMessage() { - return getConfig().getString("moderation.ban_kick_message", defaultBanKick()); - } - - private String defaultBanKick() { - return "═══════ ДОСТУП ЗАПРЕЩЁН ═══════\n" - + "Вы заблокированы на этом сервере.\n" - + "Причина: {reason}\n" - + "Модератор: {moderator}\n" - + "Срок: <#7858E9>{duration}\n" - + "════════════════════════"; - } - - public String getWarnReceived() { - return getConfig().getString("moderation.warn_received", "&#CF6679⚠ Вам выдано предупреждение! &#B798A8Причина: ẲE9{reason}"); - } - - public String getWarnStaffConfirm() { - return getConfig().getString("moderation.warn_staff_confirm", "⚖C9Игроку ẲE9{player} ⚖C9выдан" - + "варн. &#B798A8Причина: ẲE9{reason}"); - } - - public String getSilentWarnStaff() { - return getConfig().getString("moderation.silent_warn_staff", - "&#B798A8[⚖C9ТИХИЙ ВАРН&#B798A8] ẲE9{player}⚖C9: &#B798A8{reason}"); - } - - public String getBanUsage() { - return getConfig().getString("moderation.ban_usage", "&#B798A8Использование: ẲE9/lban <ник> [время|perm] [причина]"); - } - - public String getBanConfirm() { - return getConfig().getString("moderation.ban_confirm", "⚖C9Игрок ẲE9{player} ⚖C9забанен." - + "&#B798A8Срок: ẲE9{duration}"); - } - - public String getUnbanConfirm() { - return getConfig().getString("moderation.unban_confirm", "⚖C9Бан снят с ẲE9{player}"); - } - - public String getNotBanned() { - return getConfig().getString("moderation.not_banned", "&#CF6679Игрок не забанен!"); - } - - public String getAlreadyBanned() { - return getConfig().getString("moderation.already_banned", "&#CF6679Игрок уже забанен!"); - } - - public String getWarnUsage() { - return getConfig().getString("moderation.warn_usage", "&#B798A8Использование: ẲE9/warn <ник> [причина]"); - } - - public String getSilentWarnUsage() { - return getConfig().getString("moderation.silent_warn_usage", "&#B798A8Использование: ẲE9/silentwarn <ник> [причина]"); - } } diff --git a/src/main/java/com/loki/lochat/core/PluginInitializer.java b/src/main/java/com/loki/lochat/core/PluginInitializer.java index 81d275f..116e779 100644 --- a/src/main/java/com/loki/lochat/core/PluginInitializer.java +++ b/src/main/java/com/loki/lochat/core/PluginInitializer.java @@ -5,18 +5,15 @@ import com.loki.lochat.api.service.MessageService; import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.api.service.NickService; import com.loki.lochat.api.service.PlayerService; -import com.loki.lochat.api.service.PunishmentService; import com.loki.lochat.config.ConfigManager; import com.loki.lochat.config.MessageConfig; import com.loki.lochat.core.filter.AdvancedMessageFilter; import com.loki.lochat.core.registry.ServiceRegistry; import com.loki.lochat.gradient.GradientModule; import com.loki.lochat.integrations.DiscordIntegration; -import com.loki.lochat.integrations.LibertyBansHook; import com.loki.lochat.integrations.PlaceholderAPIHook; import com.loki.lochat.listener.ChatEventListener; import com.loki.lochat.listener.DiscordEventListener; -import com.loki.lochat.listener.ModerationListener; import com.loki.lochat.listener.PlayerEventListener; import com.loki.lochat.managers.AutoMessageManager; import com.loki.lochat.managers.CustomCommandManager; @@ -70,9 +67,6 @@ public class PluginInitializer { * Инициализировать интеграции */ public void initIntegrations(DiscordIntegration discordIntegration, GradientModule gradientModule) { - // LibertyBans - new LibertyBansHook(plugin); - // PlaceholderAPI if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { new PlaceholderAPIHook(plugin).register(); @@ -103,8 +97,7 @@ public class PluginInitializer { */ public AdvancedMessageFilter registerListeners(ServiceRegistry serviceRegistry, DiscordIntegration discordIntegration, MessageService messageService, PlayerService playerService, - MessagingService messagingService, NickService nickService, - PunishmentService punishmentService) { + MessagingService messagingService, NickService nickService) { AdvancedMessageFilter filter = new AdvancedMessageFilter(plugin.getConfig(), plugin); plugin.setChatFilter(filter); @@ -115,9 +108,6 @@ public class PluginInitializer { plugin.getServer().getPluginManager().registerEvents( new PlayerEventListener(plugin, playerService, messagingService, nickService, filter), plugin ); - plugin.getServer().getPluginManager().registerEvents( - new ModerationListener(punishmentService), plugin - ); // Discord listener if (discordIntegration.isEnabled()) { diff --git a/src/main/java/com/loki/lochat/core/PluginShutdown.java b/src/main/java/com/loki/lochat/core/PluginShutdown.java index ed76d09..02a7837 100644 --- a/src/main/java/com/loki/lochat/core/PluginShutdown.java +++ b/src/main/java/com/loki/lochat/core/PluginShutdown.java @@ -5,7 +5,6 @@ import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.api.service.MuteService; import com.loki.lochat.api.service.NickService; import com.loki.lochat.api.service.PlayerService; -import com.loki.lochat.api.service.PunishmentService; import com.loki.lochat.core.registry.ServiceRegistry; import com.loki.lochat.gradient.GradientModule; import com.loki.lochat.integrations.DiscordIntegration; @@ -82,11 +81,6 @@ public class PluginShutdown { muteService.save(); } - PunishmentService punishmentService = serviceRegistry.get(PunishmentService.class); - if (punishmentService != null) { - punishmentService.save(); - } - // Ники NickService nickService = serviceRegistry.get(NickService.class); if (nickService != null) { 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 7f3ca47..4b9cfb4 100644 --- a/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java +++ b/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java @@ -9,7 +9,6 @@ import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.api.service.MuteService; import com.loki.lochat.api.service.NickService; import com.loki.lochat.api.service.PlayerService; -import com.loki.lochat.api.service.PunishmentService; import com.loki.lochat.api.service.pm.PrivateMessageService; import com.loki.lochat.config.ConfigManager; import com.loki.lochat.config.MessageConfig; @@ -20,7 +19,6 @@ import com.loki.lochat.core.service.MentionServiceImpl; import com.loki.lochat.core.service.MessageServiceImpl; import com.loki.lochat.core.service.NickServiceImpl; import com.loki.lochat.core.service.PlayerServiceImpl; -import com.loki.lochat.core.service.PunishmentServiceImpl; import com.loki.lochat.core.service.messaging.PrivateMessageServiceImpl; import org.bukkit.plugin.java.JavaPlugin; @@ -64,7 +62,6 @@ public class ServiceRegistry { register(MentionService.class, new MentionServiceImpl(configManager)); register(NickService.class, new NickServiceImpl(plugin)); - register(PunishmentService.class, new PunishmentServiceImpl(plugin, configManager.getMessagesConfig())); } public void register(Class serviceClass, T implementation) { diff --git a/src/main/java/com/loki/lochat/core/service/PunishmentServiceImpl.java b/src/main/java/com/loki/lochat/core/service/PunishmentServiceImpl.java deleted file mode 100644 index ba33009..0000000 --- a/src/main/java/com/loki/lochat/core/service/PunishmentServiceImpl.java +++ /dev/null @@ -1,199 +0,0 @@ -package com.loki.lochat.core.service; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -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.utils.format.ChatFormatter; -import com.loki.lochat.utils.format.TimeFormatter; -import com.loki.lochat.utils.persistence.FilePersistence; -import com.loki.lochat.utils.platform.FoliaUtil; - -import net.kyori.adventure.text.Component; - -import org.bukkit.plugin.java.JavaPlugin; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Варны и баны в punishments.json - */ -public class PunishmentServiceImpl implements PunishmentService { - - private final JavaPlugin plugin; - private final MessagesConfig messages; - private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); - - private final Map> warns = new ConcurrentHashMap<>(); - private final Map bans = new ConcurrentHashMap<>(); - - public PunishmentServiceImpl(JavaPlugin plugin, MessagesConfig messagesConfig) { - this.plugin = plugin; - this.messages = messagesConfig; - load(); - } - - @Override - public void addWarn(UUID uuid, String playerName, String moderator, String reason, boolean silent) { - WarnEntry entry = new WarnEntry(System.currentTimeMillis(), moderator, reason, silent); - warns.computeIfAbsent(uuid, k -> Collections.synchronizedList(new ArrayList<>())).add(entry); - saveAsync(); - } - - @Override - public List getWarns(UUID uuid) { - List list = warns.get(uuid); - if (list == null) { - return List.of(); - } - synchronized (list) { - return List.copyOf(list); - } - } - - @Override - public int getWarnCount(UUID uuid) { - List list = warns.get(uuid); - return list == null ? 0 : list.size(); - } - - @Override - public void ban(UUID uuid, String playerName, long durationMs, String reason, String bannedBy) { - long until = durationMs <= 0 ? 0 : System.currentTimeMillis() + durationMs; - BanRecord record = new BanRecord(uuid, playerName, reason, bannedBy, until, System.currentTimeMillis()); - bans.put(uuid, record); - saveAsync(); - } - - @Override - public boolean unban(UUID uuid) { - BanRecord removed = bans.remove(uuid); - if (removed != null) { - saveAsync(); - return true; - } - return false; - } - - @Override - public boolean isBanned(UUID uuid) { - BanRecord b = bans.get(uuid); - if (b == null) { - return false; - } - if (b.isPermanent()) { - return true; - } - if (b.isExpired()) { - bans.remove(uuid, b); - return false; - } - return true; - } - - @Override - public BanRecord getActiveBan(UUID uuid) { - if (!isBanned(uuid)) { - return null; - } - return bans.get(uuid); - } - - @Override - public Component buildBanKickMessage(BanRecord ban) { - String reason = ban.reason != null ? ban.reason : messages.getModerationDefaultReason(); - String mod = ban.bannedBy != null ? ban.bannedBy : "?"; - String duration; - if (ban.isPermanent()) { - duration = messages.getModerationDurationPermanent(); - } else { - long left = Math.max(0, ban.until - System.currentTimeMillis()); - duration = TimeFormatter.format(left); - } - String template = messages.getBanKickMessage(); - String filled = template - .replace("{reason}", reason) - .replace("{moderator}", mod) - .replace("{duration}", duration) - .replace("{player}", ban.playerName != null ? ban.playerName : "?"); - return ChatFormatter.parse(filled); - } - - @Override - public void save() { - PunishmentSnapshot snap = new PunishmentSnapshot(); - for (Map.Entry> e : warns.entrySet()) { - List list = e.getValue(); - synchronized (list) { - snap.getWarns().put(e.getKey().toString(), new ArrayList<>(list)); - } - } - for (Map.Entry e : bans.entrySet()) { - snap.getBans().put(e.getKey().toString(), e.getValue()); - } - FilePersistence.saveJson(plugin, "punishments.json", snap, gson); - } - - @Override - public void reload() { - warns.clear(); - bans.clear(); - load(); - } - - private void saveAsync() { - FoliaUtil.runAsync(plugin, this::save); - } - - private void load() { - PunishmentSnapshot snap = FilePersistence.loadJson(plugin, "punishments.json", PunishmentSnapshot.class, gson); - if (snap == null) { - return; - } - if (snap.getWarns() != null) { - for (Map.Entry> e : snap.getWarns().entrySet()) { - try { - warns.put(UUID.fromString(e.getKey()), Collections.synchronizedList(new ArrayList<>(e.getValue()))); - } catch (IllegalArgumentException ignored) { - } - } - } - if (snap.getBans() != null) { - for (Map.Entry e : snap.getBans().entrySet()) { - try { - UUID id = UUID.fromString(e.getKey()); - BanRecord br = e.getValue(); - if (br != null) { - br.uuid = id; - if (!br.isPermanent() && br.isExpired()) { - continue; - } - bans.put(id, br); - } - } catch (IllegalArgumentException ignored) { - } - } - } - } - - private static class PunishmentSnapshot { - private Map> warns = new HashMap<>(); - private Map bans = new HashMap<>(); - - Map> getWarns() { - return warns; - } - - Map getBans() { - return bans; - } - } -} diff --git a/src/main/java/com/loki/lochat/data/model/BanRecord.java b/src/main/java/com/loki/lochat/data/model/BanRecord.java deleted file mode 100644 index f5471b6..0000000 --- a/src/main/java/com/loki/lochat/data/model/BanRecord.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.loki.lochat.data.model; - -import java.util.UUID; - -/** - * Активная блокировка - */ -public class BanRecord { - public UUID uuid; - public String playerName; - public String reason; - public String bannedBy; - /** 0 = перманент; иначе timestamp окончания бана */ - public long until; - public long createdAt; - - public BanRecord() { - } - - public BanRecord(UUID uuid, String playerName, String reason, String bannedBy, long until, long createdAt) { - this.uuid = uuid; - this.playerName = playerName; - this.reason = reason; - this.bannedBy = bannedBy; - this.until = until; - this.createdAt = createdAt; - } - - public boolean isPermanent() { - return until == 0; - } - - public boolean isExpired() { - if (isPermanent()) { - return false; - } - return System.currentTimeMillis() >= until; - } -} diff --git a/src/main/java/com/loki/lochat/data/model/WarnEntry.java b/src/main/java/com/loki/lochat/data/model/WarnEntry.java deleted file mode 100644 index 1858d08..0000000 --- a/src/main/java/com/loki/lochat/data/model/WarnEntry.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.loki.lochat.data.model; - -/** - * Запись о варне (для JSON) - */ -public class WarnEntry { - public long time; - public String moderator; - public String reason; - public boolean silent; - - public WarnEntry() { - } - - public WarnEntry(long time, String moderator, String reason, boolean silent) { - this.time = time; - this.moderator = moderator; - this.reason = reason; - this.silent = silent; - } -} diff --git a/src/main/java/com/loki/lochat/integrations/LibertyBansHook.java b/src/main/java/com/loki/lochat/integrations/LibertyBansHook.java deleted file mode 100644 index 6dc52ee..0000000 --- a/src/main/java/com/loki/lochat/integrations/LibertyBansHook.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.loki.lochat.integrations; - -import com.loki.lochat.LoChat; -import com.loki.lochat.api.service.MuteService; - -import org.bukkit.entity.Player; - -/** - * Заглушка для совместимости с LibertyBans - * Теперь используется встроенная система мутов LoChat - */ -public class LibertyBansHook { - - private final MuteService muteService; - - public LibertyBansHook(LoChat plugin) { - this.muteService = plugin.getServiceRegistry().get(MuteService.class); - - // Проверяем наличие LibertyBans - if (plugin.getServer().getPluginManager().getPlugin("LibertyBans") != null) { - plugin.getLogger().info("LibertyBans обнаружен. Используйте встроенную систему мутов LoChat (/lmute, /lunmute)"); - } - } - - /** - * Проверяет, замучен ли игрок через встроенную систему LoChat - */ - public boolean isMuted(Player player) { - return muteService.isMuted(player.getUniqueId()); - } - - public boolean isEnabled() { - return true; // Всегда используем встроенную систему - } - - public boolean isPluginPresent() { - return true; - } -} diff --git a/src/main/java/com/loki/lochat/listener/ModerationListener.java b/src/main/java/com/loki/lochat/listener/ModerationListener.java deleted file mode 100644 index 3b4f260..0000000 --- a/src/main/java/com/loki/lochat/listener/ModerationListener.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.loki.lochat.listener; - -import com.loki.lochat.api.service.PunishmentService; -import com.loki.lochat.compat.Msg; -import com.loki.lochat.data.model.BanRecord; - -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.player.AsyncPlayerPreLoginEvent; - -/** - * Блокировка входа забаненным игрокам - */ -public class ModerationListener implements Listener { - - private final PunishmentService punishmentService; - - public ModerationListener(PunishmentService punishmentService) { - this.punishmentService = punishmentService; - } - - @EventHandler(priority = EventPriority.LOWEST) - public void onPreLogin(AsyncPlayerPreLoginEvent event) { - if (!punishmentService.isBanned(event.getUniqueId())) { - return; - } - BanRecord ban = punishmentService.getActiveBan(event.getUniqueId()); - if (ban == null) { - return; - } - event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED, Msg.legacy(punishmentService.buildBanKickMessage(ban))); - } -} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index fedea1f..5e79e5d 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -171,9 +171,7 @@ filters: moderation: default-mute-duration: "7d" default-mute-reason: "No reason specified" - default-ban-reason: "No reason specified" voice-mute-console-command: "" - ban-kick-message: "&cYou have been banned!\n&7Reason: &f{reason}" # ── Sounds ───────────────────────────────────────────────────────────────────── diff --git a/src/main/resources/config/messages.yml b/src/main/resources/config/messages.yml index f7ff89a..0221640 100644 --- a/src/main/resources/config/messages.yml +++ b/src/main/resources/config/messages.yml @@ -106,18 +106,3 @@ general: moderation: default-reason: "No reason specified" duration-permanent: "permanent" - warn-received: "&6\u26A0 &cYou received a warning! &7Reason: &f{reason}" - warn-staff-confirm: "&aPlayer &f{player} &awarned. &7Reason: &f{reason}" - silent-warn-staff: "&8[&7SILENT&8] &f{player}&7: &7{reason}" - ban-usage: "&cUsage: &f/lban [time|perm] [reason]" - ban-confirm: "&aPlayer &f{player} &abanned. &7Duration: &f{duration}" - unban-confirm: "&aBan lifted for &f{player}" - not-banned: "&cPlayer is not banned!" - already-banned: "&cPlayer is already banned!" - warn-usage: "&cUsage: &f/warn [reason]" - silent-warn-usage: "&cUsage: &f/silentwarn [reason]" - ban-kick-message: | - You are banned from this server. - Reason: {reason} - Moderator: {moderator} - Duration: {duration} diff --git a/src/main/resources/config/mute.yml b/src/main/resources/config/mute.yml index 848fbc6..0226dec 100644 --- a/src/main/resources/config/mute.yml +++ b/src/main/resources/config/mute.yml @@ -132,7 +132,7 @@ auto-mute: duration: "1h" reason: "Auto-mute: advertising" -# ── Punishment Escalation ────────────────────────────────────────────────────── +# ── Mute Escalation ────────────────────────────────────────────────────────────── escalation: enabled: true @@ -238,11 +238,6 @@ features: - "say" - "tell" - warnings: - enabled: true - max-warnings: 3 - reset-time: 3600 - appeals: enabled: false command: "/appeal" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 82393c9..4ae43bb 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -9,7 +9,6 @@ softdepend: - PlaceholderAPI - PlayerPoints - LuckPerms - - LibertyBans - SkinsRestorer commands: @@ -163,30 +162,6 @@ 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 @@ -393,30 +368,6 @@ permissions: description: Can use /appeal command default: true - 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 From bd39118b146e7a5db93b2e08037ba6ddcde62915 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 20 Sep 2026 16:56:02 +0200 Subject: [PATCH 09/10] chore: add PMD 7.8.0 and fix warnings/antipatterns - build.gradle.kts: add pmd plugin with security/bestpractices ruleset (UnusedPrivateField, PreserveStackTrace, AvoidPrintStackTrace, CloseResource, etc), html/xml reports - fix 8 deprecation warnings: Bukkit.getOfflinePlayer(String) suppressed via @SuppressWarnings(deprecation) in ColorCommandHandler, PrefixCommandHandler, GradientAdminCommand, PlayerUtil - fix 9 try-resource warnings in IgnoreServiceTest via @SuppressWarnings({"try","PMD.UnusedLocalVariable"}) - PMD fixes: AvoidPrintStackTrace -> logger.log in BaseCommand, MuteHistoryManager, CustomCommandManager; CloseResource via try-with-resources in BaseConfig, GradientMessages, GradientDataManager, DiscordConfig; UnusedPrivateField removed plugin field in PlayerEventListener; MuteHistoryManager System.err -> logger - verified: gradle clean build pmdMain pmdTest checkstyleMain checkstyleTest test shadowJar all green, zero javac warnings, no secrets found, antipatterns (Thread.sleep in async webhook/rate-limiter kept isolated, printStackTrace removed) --- build.gradle.kts | 32 +++++++++++++++++++ .../lochat/commands/base/BaseCommand.java | 4 +-- .../com/loki/lochat/config/BaseConfig.java | 14 ++++---- .../core/service/mute/MuteHistoryManager.java | 6 ++-- .../commands/GradientAdminCommand.java | 1 + .../handlers/ColorCommandHandler.java | 3 ++ .../handlers/PrefixCommandHandler.java | 3 ++ .../gradient/config/GradientMessages.java | 13 +++++--- .../gradient/data/GradientDataManager.java | 23 ++++++------- .../integrations/discord/DiscordConfig.java | 4 +-- .../lochat/listener/PlayerEventListener.java | 2 -- .../lochat/managers/CustomCommandManager.java | 4 +-- .../loki/lochat/utils/player/PlayerUtil.java | 1 + .../service/messaging/IgnoreServiceTest.java | 9 ++++++ 14 files changed, 85 insertions(+), 34 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 3f97440..5d49805 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("com.gradleup.shadow") version "9.0.0" jacoco checkstyle + pmd } group = "com.loki" @@ -57,6 +58,37 @@ tasks.withType().configureEach { } } +pmd { + toolVersion = "7.8.0" + ruleSetConfig = resources.text.fromString( + """ + + + LoChat security + best practices (no style nit) + + + + + + + + + + """.trimIndent() + ) + isIgnoreFailures = false +} + +tasks.withType().configureEach { + reports { + xml.required.set(true) + html.required.set(true) + } +} + java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) diff --git a/src/main/java/com/loki/lochat/commands/base/BaseCommand.java b/src/main/java/com/loki/lochat/commands/base/BaseCommand.java index 53081e4..76e28d8 100644 --- a/src/main/java/com/loki/lochat/commands/base/BaseCommand.java +++ b/src/main/java/com/loki/lochat/commands/base/BaseCommand.java @@ -11,6 +11,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.logging.Level; /** * Базовый класс для всех команд плагина @@ -30,8 +31,7 @@ public abstract class BaseCommand implements CommandExecutor, TabCompleter { try { return executeCommand(sender, command, label, args); } catch (Exception e) { - plugin.getLogger().severe("Ошибка в команде " + command.getName() + ": " + e.getMessage()); - e.printStackTrace(); + plugin.getLogger().log(Level.WARNING, "Ошибка в команде " + command.getName() + ": " + e.getMessage(), e); sender.sendMessage("§cПроизошла ошибка при выполнении команды!"); return true; } diff --git a/src/main/java/com/loki/lochat/config/BaseConfig.java b/src/main/java/com/loki/lochat/config/BaseConfig.java index 2f6296d..22516a6 100644 --- a/src/main/java/com/loki/lochat/config/BaseConfig.java +++ b/src/main/java/com/loki/lochat/config/BaseConfig.java @@ -68,13 +68,13 @@ public abstract class BaseConfig { if (!configFile.exists()) { try { String resourcePath = inConfigFolder ? "config/" + fileName : fileName; - InputStream in = plugin.getResource(resourcePath); - if (in != null) { - Files.copy(in, configFile.toPath()); - in.close(); - plugin.getLogger().info("Created config file: " + (inConfigFolder ? "config/" : "") + fileName); - } else { - plugin.getLogger().warning("Resource not found: " + resourcePath); + try (InputStream in = plugin.getResource(resourcePath)) { + if (in != null) { + Files.copy(in, configFile.toPath()); + plugin.getLogger().info("Created config file: " + (inConfigFolder ? "config/" : "") + fileName); + } else { + plugin.getLogger().warning("Resource not found: " + resourcePath); + } } } catch (Exception e) { plugin.getLogger().warning("Failed to copy " + fileName + ": " + e.getMessage()); diff --git a/src/main/java/com/loki/lochat/core/service/mute/MuteHistoryManager.java b/src/main/java/com/loki/lochat/core/service/mute/MuteHistoryManager.java index 93932ee..3972c06 100644 --- a/src/main/java/com/loki/lochat/core/service/mute/MuteHistoryManager.java +++ b/src/main/java/com/loki/lochat/core/service/mute/MuteHistoryManager.java @@ -24,6 +24,8 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; public class MuteHistoryManager { @@ -112,7 +114,7 @@ public class MuteHistoryManager { } moveIntoPlace(tmp); } catch (IOException e) { - System.err.println("Failed to save mute history: " + e.getMessage()); + Logger.getLogger(MuteHistoryManager.class.getName()).log(Level.WARNING, "Failed to save mute history", e); } finally { lock.unlock(); } @@ -140,7 +142,7 @@ public class MuteHistoryManager { history.putAll(loaded); } } catch (IOException e) { - e.printStackTrace(); + Logger.getLogger(MuteHistoryManager.class.getName()).log(Level.WARNING, "Failed to load mute history", e); } } } 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 81e15a8..4f47ac5 100644 --- a/src/main/java/com/loki/lochat/gradient/commands/GradientAdminCommand.java +++ b/src/main/java/com/loki/lochat/gradient/commands/GradientAdminCommand.java @@ -95,6 +95,7 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter { sender.sendMessage("§e/aprefix updateall §7— обновить display names всех игроков"); } + @SuppressWarnings("deprecation") private void handleInfo(CommandSender sender, String playerName, GradientMessages msg) { OfflinePlayer target = Bukkit.getOfflinePlayer(playerName); if (!target.hasPlayedBefore() && !target.isOnline()) { 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 index d51c316..09c3405 100644 --- a/src/main/java/com/loki/lochat/gradient/commands/handlers/ColorCommandHandler.java +++ b/src/main/java/com/loki/lochat/gradient/commands/handlers/ColorCommandHandler.java @@ -29,6 +29,7 @@ public class ColorCommandHandler { /** * Установить цвет игроку */ + @SuppressWarnings("deprecation") public void handleSetColor(CommandSender sender, String[] args, GradientMessages msg) { if (args.length < 3) { sender.sendMessage("§cИспользование: /aprefix setcolor <игрок> <цвет1> [цвет2] ..."); @@ -69,6 +70,7 @@ public class ColorCommandHandler { /** * Включить/выключить цвет */ + @SuppressWarnings("deprecation") public void handleColorToggle(CommandSender sender, String[] args, boolean enable, GradientMessages msg) { if (args.length < 2) { sender.sendMessage("§cИспользование: /aprefix " + (enable ? "coloron" : "coloroff") + " <игрок>"); @@ -93,6 +95,7 @@ public class ColorCommandHandler { /** * Сбросить цвет */ + @SuppressWarnings("deprecation") public void handleResetColor(CommandSender sender, String[] args, GradientMessages msg) { if (args.length < 2) { sender.sendMessage("§cИспользование: /aprefix resetcolor <игрок>"); 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 index 32ecbbc..112751f 100644 --- a/src/main/java/com/loki/lochat/gradient/commands/handlers/PrefixCommandHandler.java +++ b/src/main/java/com/loki/lochat/gradient/commands/handlers/PrefixCommandHandler.java @@ -25,6 +25,7 @@ public class PrefixCommandHandler { /** * Установить префикс игроку */ + @SuppressWarnings("deprecation") public void handleSetPrefix(CommandSender sender, String[] args, GradientMessages msg) { if (args.length < 3) { sender.sendMessage("§cИспользование: /aprefix setprefix <игрок> <префикс>"); @@ -65,6 +66,7 @@ public class PrefixCommandHandler { /** * Включить/выключить префикс */ + @SuppressWarnings("deprecation") public void handlePrefixToggle(CommandSender sender, String[] args, boolean enable, GradientMessages msg) { if (args.length < 2) { sender.sendMessage("§cИспользование: /aprefix " + (enable ? "prefixon" : "prefixoff") + " <игрок>"); @@ -89,6 +91,7 @@ public class PrefixCommandHandler { /** * Сбросить префикс */ + @SuppressWarnings("deprecation") public void handleResetPrefix(CommandSender sender, String[] args, GradientMessages msg) { if (args.length < 2) { sender.sendMessage("§cИспользование: /aprefix resetprefix <игрок>"); diff --git a/src/main/java/com/loki/lochat/gradient/config/GradientMessages.java b/src/main/java/com/loki/lochat/gradient/config/GradientMessages.java index 72d7367..6640c63 100644 --- a/src/main/java/com/loki/lochat/gradient/config/GradientMessages.java +++ b/src/main/java/com/loki/lochat/gradient/config/GradientMessages.java @@ -37,11 +37,14 @@ public class GradientMessages { } messages = YamlConfiguration.loadConfiguration(messagesFile); - InputStream defStream = plugin.getResource("messages.yml"); - if (defStream != null) { - YamlConfiguration defConfig = YamlConfiguration.loadConfiguration( - new InputStreamReader(defStream, StandardCharsets.UTF_8)); - messages.setDefaults(defConfig); + try (InputStream defStream = plugin.getResource("messages.yml")) { + if (defStream != null) { + YamlConfiguration defConfig = YamlConfiguration.loadConfiguration( + new InputStreamReader(defStream, StandardCharsets.UTF_8)); + messages.setDefaults(defConfig); + } + } catch (java.io.IOException e) { + plugin.getLogger().warning("Failed to load default messages.yml: " + e.getMessage()); } } diff --git a/src/main/java/com/loki/lochat/gradient/data/GradientDataManager.java b/src/main/java/com/loki/lochat/gradient/data/GradientDataManager.java index 3cb5b6d..ff1f13d 100644 --- a/src/main/java/com/loki/lochat/gradient/data/GradientDataManager.java +++ b/src/main/java/com/loki/lochat/gradient/data/GradientDataManager.java @@ -104,18 +104,19 @@ public class GradientDataManager { "SELECT prefix, colors, color_enabled, prefix_enabled, " + "prefix_purchased, last_color_change, last_prefix_change FROM gradient_data WHERE uuid = ?")) { stmt.setString(1, uuid.toString()); - ResultSet rs = stmt.executeQuery(); - if (rs.next()) { - data.setPrefix(rs.getString("prefix")); - String colorsStr = rs.getString("colors"); - if (colorsStr != null && !colorsStr.isEmpty()) { - data.setColors(List.of(colorsStr.split(","))); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + data.setPrefix(rs.getString("prefix")); + String colorsStr = rs.getString("colors"); + if (colorsStr != null && !colorsStr.isEmpty()) { + data.setColors(List.of(colorsStr.split(","))); + } + data.setColorEnabled(rs.getBoolean("color_enabled")); + data.setPrefixEnabled(rs.getBoolean("prefix_enabled")); + data.setPrefixPurchased(rs.getBoolean("prefix_purchased")); + data.setLastColorChange(rs.getLong("last_color_change")); + data.setLastPrefixChange(rs.getLong("last_prefix_change")); } - data.setColorEnabled(rs.getBoolean("color_enabled")); - data.setPrefixEnabled(rs.getBoolean("prefix_enabled")); - data.setPrefixPurchased(rs.getBoolean("prefix_purchased")); - data.setLastColorChange(rs.getLong("last_color_change")); - data.setLastPrefixChange(rs.getLong("last_prefix_change")); } } catch (SQLException e) { plugin.getLogger().warning("Ошибка загрузки gradient данных: " + e.getMessage()); diff --git a/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java b/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java index e5a1e08..f75f4b0 100644 --- a/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java +++ b/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java @@ -25,11 +25,9 @@ public class DiscordConfig { File configFile = new File(configFolder, "discord.yml"); if (!configFile.exists()) { - try { - java.io.InputStream in = plugin.getResource("config/discord.yml"); + 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()); diff --git a/src/main/java/com/loki/lochat/listener/PlayerEventListener.java b/src/main/java/com/loki/lochat/listener/PlayerEventListener.java index 49b5c1f..d11bab4 100644 --- a/src/main/java/com/loki/lochat/listener/PlayerEventListener.java +++ b/src/main/java/com/loki/lochat/listener/PlayerEventListener.java @@ -23,7 +23,6 @@ import org.bukkit.inventory.ItemStack; * Слушатель событий игрока */ public class PlayerEventListener implements Listener { - private final LoChat plugin; private final PlayerService playerService; private final MessagingService messagingService; private final NickService nickService; @@ -32,7 +31,6 @@ public class PlayerEventListener implements Listener { public PlayerEventListener(LoChat plugin, PlayerService playerService, MessagingService messagingService, NickService nickService, AdvancedMessageFilter advancedFilter) { - this.plugin = plugin; this.playerService = playerService; this.messagingService = messagingService; this.nickService = nickService; diff --git a/src/main/java/com/loki/lochat/managers/CustomCommandManager.java b/src/main/java/com/loki/lochat/managers/CustomCommandManager.java index ac93b45..c96fffa 100644 --- a/src/main/java/com/loki/lochat/managers/CustomCommandManager.java +++ b/src/main/java/com/loki/lochat/managers/CustomCommandManager.java @@ -19,6 +19,7 @@ import java.io.File; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.logging.Level; public class CustomCommandManager { @@ -101,8 +102,7 @@ public class CustomCommandManager { } } catch (Exception e) { - plugin.getLogger().severe("Ошибка регистрации команды " + data.name() + ": " + e.getMessage()); - e.printStackTrace(); + plugin.getLogger().log(Level.WARNING, "Ошибка регистрации команды " + data.name() + ": " + e.getMessage(), e); } } diff --git a/src/main/java/com/loki/lochat/utils/player/PlayerUtil.java b/src/main/java/com/loki/lochat/utils/player/PlayerUtil.java index b64dd2c..a4f0303 100644 --- a/src/main/java/com/loki/lochat/utils/player/PlayerUtil.java +++ b/src/main/java/com/loki/lochat/utils/player/PlayerUtil.java @@ -54,6 +54,7 @@ public final class PlayerUtil { * @param name имя игрока * @return UUID игрока или null если не найден */ + @SuppressWarnings("deprecation") public static UUID findPlayerUUID(String name) { // Сначала ищем онлайн игрока Player onlinePlayer = Bukkit.getPlayer(name); diff --git a/src/test/java/com/loki/lochat/core/service/messaging/IgnoreServiceTest.java b/src/test/java/com/loki/lochat/core/service/messaging/IgnoreServiceTest.java index b1cbe93..18797d4 100644 --- a/src/test/java/com/loki/lochat/core/service/messaging/IgnoreServiceTest.java +++ b/src/test/java/com/loki/lochat/core/service/messaging/IgnoreServiceTest.java @@ -30,6 +30,7 @@ class IgnoreServiceTest { } @Test + @SuppressWarnings({"try", "PMD.UnusedLocalVariable"}) void isIgnoring_notIgnored() { try (MockedStatic ignored = Mockito.mockStatic(com.loki.lochat.utils.platform.FoliaUtil.class)) { UUID player = UUID.randomUUID(); @@ -39,6 +40,7 @@ class IgnoreServiceTest { } @Test + @SuppressWarnings({"try", "PMD.UnusedLocalVariable"}) void addIgnore_thenIsIgnoring() { try (MockedStatic fu = Mockito.mockStatic(com.loki.lochat.utils.platform.FoliaUtil.class)) { UUID player = UUID.randomUUID(); @@ -49,6 +51,7 @@ class IgnoreServiceTest { } @Test + @SuppressWarnings({"try", "PMD.UnusedLocalVariable"}) void addIgnore_duplicateReturnsFalse() { try (MockedStatic fu = Mockito.mockStatic(com.loki.lochat.utils.platform.FoliaUtil.class)) { UUID player = UUID.randomUUID(); @@ -59,6 +62,7 @@ class IgnoreServiceTest { } @Test + @SuppressWarnings({"try", "PMD.UnusedLocalVariable"}) void removeIgnore() { try (MockedStatic fu = Mockito.mockStatic(com.loki.lochat.utils.platform.FoliaUtil.class)) { UUID player = UUID.randomUUID(); @@ -70,6 +74,7 @@ class IgnoreServiceTest { } @Test + @SuppressWarnings({"try", "PMD.UnusedLocalVariable"}) void removeIgnore_notIgnored() { try (MockedStatic fu = Mockito.mockStatic(com.loki.lochat.utils.platform.FoliaUtil.class)) { assertFalse(ignoreService.removeIgnore(UUID.randomUUID(), UUID.randomUUID())); @@ -77,6 +82,7 @@ class IgnoreServiceTest { } @Test + @SuppressWarnings({"try", "PMD.UnusedLocalVariable"}) void getIgnoredCount() { try (MockedStatic fu = Mockito.mockStatic(com.loki.lochat.utils.platform.FoliaUtil.class)) { UUID player = UUID.randomUUID(); @@ -89,6 +95,7 @@ class IgnoreServiceTest { } @Test + @SuppressWarnings({"try", "PMD.UnusedLocalVariable"}) void getIgnoredPlayers_returnsSet() { try (MockedStatic fu = Mockito.mockStatic(com.loki.lochat.utils.platform.FoliaUtil.class)) { UUID player = UUID.randomUUID(); @@ -99,6 +106,7 @@ class IgnoreServiceTest { } @Test + @SuppressWarnings({"try", "PMD.UnusedLocalVariable"}) void clearIgnores() { try (MockedStatic fu = Mockito.mockStatic(com.loki.lochat.utils.platform.FoliaUtil.class)) { UUID player = UUID.randomUUID(); @@ -109,6 +117,7 @@ class IgnoreServiceTest { } @Test + @SuppressWarnings({"try", "PMD.UnusedLocalVariable"}) void multiplePlayersIndepedent() { try (MockedStatic fu = Mockito.mockStatic(com.loki.lochat.utils.platform.FoliaUtil.class)) { UUID a = UUID.randomUUID(); From 260986ff1134e011544080db1162bd409f03725a Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 20 Sep 2026 21:05:17 +0200 Subject: [PATCH 10/10] feat: hover translation (Google, Chrome endpoint with gtx fallback), LuckPerms prefixes in chat, restyled messages --- src/main/java/com/loki/lochat/LoChat.java | 2 + .../com/loki/lochat/config/ConfigManager.java | 20 +++ .../lochat/config/core/ConfigAccessor.java | 23 +++ .../lochat/listener/ChatEventListener.java | 32 +++++ .../lochat/renderer/EnhancedChatRenderer.java | 65 +++++++-- .../lochat/translate/TranslationService.java | 136 +++++++++++++++++- .../lochat/utils/format/LuckPermsAffixes.java | 67 +++++++++ src/main/resources/config/messages.yml | 42 +++--- src/main/resources/messages.yml | 90 ++++++------ 9 files changed, 396 insertions(+), 81 deletions(-) create mode 100644 src/main/java/com/loki/lochat/utils/format/LuckPermsAffixes.java diff --git a/src/main/java/com/loki/lochat/LoChat.java b/src/main/java/com/loki/lochat/LoChat.java index 5b8b375..2b10aa5 100644 --- a/src/main/java/com/loki/lochat/LoChat.java +++ b/src/main/java/com/loki/lochat/LoChat.java @@ -60,6 +60,8 @@ public final class LoChat extends JavaPlugin { configManager.getTranslationEndpoint(), configManager.getTranslationApiKey(), configManager.isTranslationEnabled(), + configManager.getTranslationProvider(), + configManager.getTranslationTimeoutMs(), getLogger() ); diff --git a/src/main/java/com/loki/lochat/config/ConfigManager.java b/src/main/java/com/loki/lochat/config/ConfigManager.java index 3454ecb..d03ac4b 100644 --- a/src/main/java/com/loki/lochat/config/ConfigManager.java +++ b/src/main/java/com/loki/lochat/config/ConfigManager.java @@ -249,6 +249,26 @@ public class ConfigManager { return accessor.getTranslationApiKey(); } + public String getTranslationProvider() { + return accessor.getTranslationProvider(); + } + + public String getTranslationMode() { + return accessor.getTranslationMode(); + } + + public int getTranslationTimeoutMs() { + return accessor.getTranslationTimeoutMs(); + } + + public java.util.List getTranslationHoverFormat() { + return accessor.getTranslationHoverFormat(); + } + + public String getTranslationBypassPermission() { + return accessor.getTranslationBypassPermission(); + } + // ========== LoPreff integration ========== public boolean isLopreffEnabled() { diff --git a/src/main/java/com/loki/lochat/config/core/ConfigAccessor.java b/src/main/java/com/loki/lochat/config/core/ConfigAccessor.java index 96db60e..1531c67 100644 --- a/src/main/java/com/loki/lochat/config/core/ConfigAccessor.java +++ b/src/main/java/com/loki/lochat/config/core/ConfigAccessor.java @@ -150,6 +150,29 @@ public class ConfigAccessor { return config.getString("chat.translation.api-key", ""); } + /** "libretranslate" (needs endpoint and usually a key) or "google" (free, no key). */ + public String getTranslationProvider() { + return config.getString("chat.translation.provider", "libretranslate"); + } + + /** "hover" shows the translation when hovering the message, "actionbar" shows it above the hotbar. */ + public String getTranslationMode() { + return config.getString("chat.translation.mode", "hover"); + } + + public int getTranslationTimeoutMs() { + return config.getInt("chat.translation.timeout-ms", 3000); + } + + public java.util.List getTranslationHoverFormat() { + java.util.List lines = config.getStringList("chat.translation.hover-format"); + return lines.isEmpty() ? java.util.List.of("&7Translation:", "&f{translation}") : lines; + } + + public String getTranslationBypassPermission() { + return config.getString("chat.translation.bypass-permission", "lochat.translate.bypass"); + } + // ========== LoPreff integration ========== public boolean isLopreffEnabled() { diff --git a/src/main/java/com/loki/lochat/listener/ChatEventListener.java b/src/main/java/com/loki/lochat/listener/ChatEventListener.java index 3f0f6e8..89ae9b3 100644 --- a/src/main/java/com/loki/lochat/listener/ChatEventListener.java +++ b/src/main/java/com/loki/lochat/listener/ChatEventListener.java @@ -9,7 +9,9 @@ import com.loki.lochat.compat.Msg; import com.loki.lochat.core.filter.AdvancedMessageFilter; import com.loki.lochat.core.filter.FilterResult; import com.loki.lochat.renderer.EnhancedChatRenderer; +import com.loki.lochat.translate.TranslationService; import com.loki.lochat.utils.format.ChatFormatter; +import com.loki.lochat.utils.format.LuckPermsAffixes; import net.kyori.adventure.text.Component; @@ -80,6 +82,7 @@ public class ChatEventListener implements Listener { Component displayName = buildDisplayName(sender); Set recipients = new HashSet<>(event.getRecipients()); event.setCancelled(true); + prefetchTranslation(sender, messageComponent, recipients); for (Player viewer : recipients) { Component rendered = renderer.render(sender, displayName, messageComponent, viewer); Msg.send(viewer, rendered); @@ -132,6 +135,7 @@ public class ChatEventListener implements Listener { Component displayName = buildDisplayName(sender); Set recipients = new HashSet<>(event.getRecipients()); event.setCancelled(true); + prefetchTranslation(sender, messageComponent, recipients); for (Player viewer : recipients) { Component rendered = renderer.render(sender, displayName, messageComponent, viewer); Msg.send(viewer, rendered); @@ -226,9 +230,37 @@ public class ChatEventListener implements Listener { if (legacyName == null || legacyName.isBlank()) { return Component.text(sender.getName()); } + // LuckPerms prefix/suffix are added separately ({player_prefix}/{player_suffix}), do not show them twice + legacyName = LuckPermsAffixes.withoutAffixes(legacyName, LuckPermsAffixes.prefix(sender), + LuckPermsAffixes.suffix(sender)); return ChatFormatter.parse(legacyName); } + /** Translates the message for every viewer language before rendering (runs off the main thread only). */ + private void prefetchTranslation(Player sender, Component messageComponent, Set recipients) { + try { + LoChat loChat = (LoChat) plugin; + var cfg = loChat.getConfigManager(); + TranslationService ts = loChat.getTranslationService(); + if (!cfg.isTranslationEnabled() || !ts.isEnabled() || Bukkit.isPrimaryThread() + || "actionbar".equalsIgnoreCase(cfg.getTranslationMode()) + || sender.hasPermission(cfg.getTranslationBypassPermission())) { + return; + } + Set languages = new HashSet<>(); + for (Player viewer : recipients) { + if (!viewer.equals(sender)) { + languages.add(TranslationService.localeToLanguageCode(viewer.getLocale())); + } + } + if (!languages.isEmpty()) { + ts.prefetch(Msg.plain(messageComponent), languages, cfg.getTranslationTimeoutMs()); + } + } catch (Exception e) { + plugin.getLogger().fine("Translation prefetch failed: " + e.getMessage()); + } + } + private void recordStatistics(LoChat loChat, Player sender, boolean isGlobal) { playerService.recordMessage(sender.getUniqueId(), isGlobal ? "global" : "local"); } diff --git a/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java b/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java index 416bef0..fa7f830 100644 --- a/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java +++ b/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java @@ -64,12 +64,7 @@ public class EnhancedChatRenderer { if (viewer != null) { processed = mentionHandler.processMentions(processed, source, viewer); - if (cfg.isTranslationEnabled()) { - com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; - if (loChat.getTranslationService().isEnabled()) { - sendTranslationAsync(viewer, PlainTextComponentSerializer.plainText().serialize(processed)); - } - } + processed = applyTranslation(source, viewer, message, processed); } // Получаем формат из конфига @@ -124,7 +119,7 @@ public class EnhancedChatRenderer { while (matcher.find()) { if (matcher.start() > lastEnd) { - result = result.append(Component.text(format.substring(lastEnd, matcher.start()))); + result = result.append(ChatFormatter.parse(format.substring(lastEnd, matcher.start()))); } String placeholder = matcher.group(1); @@ -134,7 +129,7 @@ public class EnhancedChatRenderer { } if (lastEnd < format.length()) { - result = result.append(Component.text(format.substring(lastEnd))); + result = result.append(ChatFormatter.parse(format.substring(lastEnd))); } return result; @@ -156,6 +151,11 @@ public class EnhancedChatRenderer { } } + String luckPermsPrefix = com.loki.lochat.utils.format.LuckPermsAffixes.prefix(player); + if (!luckPermsPrefix.isEmpty()) { + return com.loki.lochat.utils.format.ChatFormatter.parse(luckPermsPrefix); + } + return Component.empty(); } @@ -170,6 +170,11 @@ public class EnhancedChatRenderer { } } + String luckPermsSuffix = com.loki.lochat.utils.format.LuckPermsAffixes.suffix(player); + if (!luckPermsSuffix.isEmpty()) { + return com.loki.lochat.utils.format.ChatFormatter.parse(luckPermsSuffix); + } + return Component.empty(); } @@ -239,6 +244,50 @@ public class EnhancedChatRenderer { .clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " ")); } + /** + * Adds the translation into the viewer's client language: as hover text over the message (default) + * or in the action bar. The translation must already be cached (see ChatEventListener#prefetchTranslation). + */ + private Component applyTranslation(Player source, Player viewer, Component original, Component processed) { + if (!cfg.isTranslationEnabled() || viewer.equals(source)) { + return processed; + } + com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; + TranslationService ts = loChat.getTranslationService(); + if (!ts.isEnabled() || source.hasPermission(cfg.getTranslationBypassPermission())) { + return processed; + } + String plain = PlainTextComponentSerializer.plainText().serialize(original); + if (plain.isBlank()) { + return processed; + } + if ("actionbar".equalsIgnoreCase(cfg.getTranslationMode())) { + sendTranslationAsync(viewer, plain); + return processed; + } + String translated = ts.getCachedTranslation(plain, viewer.getLocale()); + if (translated == null || translated.isBlank() || translated.equalsIgnoreCase(plain)) { + return processed; + } + Component hover = Component.empty(); + boolean first = true; + for (String line : cfg.getTranslationHoverFormat()) { + if (!first) { + hover = hover.append(Component.newline()); + } + first = false; + int idx = line.indexOf("{translation}"); + if (idx < 0) { + hover = hover.append(ChatFormatter.parse(line)); + } else { + hover = hover.append(ChatFormatter.parse(line.substring(0, idx))) + .append(Component.text(translated)) + .append(ChatFormatter.parse(line.substring(idx + "{translation}".length()))); + } + } + return processed.hoverEvent(HoverEvent.showText(hover)); + } + private void sendTranslationAsync(Player viewer, String plainText) { if (plainText.isBlank()) { return; diff --git a/src/main/java/com/loki/lochat/translate/TranslationService.java b/src/main/java/com/loki/lochat/translate/TranslationService.java index 7eb6b60..264a395 100644 --- a/src/main/java/com/loki/lochat/translate/TranslationService.java +++ b/src/main/java/com/loki/lochat/translate/TranslationService.java @@ -33,11 +33,20 @@ public class TranslationService { private final String endpoint; private final String apiKey; private final boolean enabled; + private final String provider; + private final int timeoutMs; public TranslationService(String endpoint, String apiKey, boolean enabled, Logger logger) { + this(endpoint, apiKey, enabled, "libretranslate", 3000, logger); + } + + public TranslationService(String endpoint, String apiKey, boolean enabled, String provider, int timeoutMs, + Logger logger) { this.endpoint = endpoint != null ? endpoint.replaceAll("/+$", "") : ""; this.apiKey = apiKey; this.enabled = enabled; + this.provider = provider == null ? "libretranslate" : provider.toLowerCase(Locale.ROOT); + this.timeoutMs = Math.max(500, timeoutMs); this.logger = logger; this.httpClient = HttpClient.newBuilder() .connectTimeout(TIMEOUT) @@ -59,8 +68,37 @@ public class TranslationService { }); } + private boolean isGoogle() { + return "google".equals(provider); + } + public boolean isEnabled() { - return enabled && !endpoint.isBlank(); + return enabled && (isGoogle() || !endpoint.isBlank()); + } + + /** Cached translation for the given language, or null if it has not been translated yet. */ + public String getCachedTranslation(String text, String language) { + return getCached(text, localeToLanguageCode(language)); + } + + /** + * Translates the text into every given language in parallel and waits up to timeoutMs. + * Call it off the main thread; the results end up in the cache. + */ + public void prefetch(String text, java.util.Collection languages, long timeoutMs) { + if (!isEnabled() || text == null || text.isBlank()) { + return; + } + java.util.List> futures = new java.util.ArrayList<>(); + for (String language : languages) { + futures.add(translateAsync(text, language)); + } + try { + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .get(timeoutMs, java.util.concurrent.TimeUnit.MILLISECONDS); + } catch (Exception e) { + // timeout or failure: the message is simply sent without a translation + } } public CompletableFuture translateAsync(String text, String targetLanguage) { @@ -89,7 +127,91 @@ public class TranslationService { } } + private volatile long gtxBlockedUntil; + private volatile long chromeBlockedUntil; + + /** + * The gtx endpoint gets IP-blocked (HTTP 429 / captcha page) very quickly, so the Chrome dictionary + * endpoint is tried first and gtx is only a fallback; a blocked endpoint is skipped for a minute. + */ + private String doFetchGoogle(String text, String targetLang) throws Exception { + long now = System.currentTimeMillis(); + String q = java.net.URLEncoder.encode(text, StandardCharsets.UTF_8); + String tl = java.net.URLEncoder.encode(targetLang, StandardCharsets.UTF_8); + Exception last = null; + if (now >= chromeBlockedUntil) { + try { + return parseChromeResponse(get("https://clients5.google.com/translate_a/t?client=dict-chrome-ex&sl=auto&tl=" + + tl + "&q=" + q)); + } catch (Exception e) { + chromeBlockedUntil = now + 60_000; + last = e; + } + } + if (now >= gtxBlockedUntil) { + try { + return parseGoogleResponse(get("https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&dt=t&tl=" + + tl + "&q=" + q)); + } catch (Exception e) { + gtxBlockedUntil = now + 60_000; + last = e; + } + } + throw last != null ? last : new RuntimeException("all translation endpoints are on cooldown"); + } + + private String get(String url) throws Exception { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofMillis(timeoutMs)) + .header("User-Agent", "Mozilla/5.0") + .GET() + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new RuntimeException("Google returned " + response.statusCode()); + } + return response.body(); + } + + /** Chrome dictionary response: [["translated","detected-language"], ...]. */ + static String parseChromeResponse(String body) { + com.google.gson.JsonArray root = JsonParser.parseString(body).getAsJsonArray(); + StringBuilder sb = new StringBuilder(); + for (com.google.gson.JsonElement segment : root) { + if (segment.isJsonArray()) { + com.google.gson.JsonArray parts = segment.getAsJsonArray(); + if (parts.size() > 0 && !parts.get(0).isJsonNull()) { + sb.append(parts.get(0).getAsString()); + } + } else if (segment.isJsonPrimitive()) { + sb.append(segment.getAsString()); + break; + } + } + if (sb.length() == 0) { + throw new RuntimeException("empty translation response"); + } + return sb.toString(); + } + + /** Google response: [[["translated","original",...], ...], null, "detected-language"]. */ + static String parseGoogleResponse(String body) { + com.google.gson.JsonArray root = JsonParser.parseString(body).getAsJsonArray(); + StringBuilder sb = new StringBuilder(); + for (com.google.gson.JsonElement segment : root.get(0).getAsJsonArray()) { + com.google.gson.JsonArray parts = segment.getAsJsonArray(); + if (parts.size() > 0 && !parts.get(0).isJsonNull()) { + sb.append(parts.get(0).getAsString()); + } + } + return sb.toString(); + } + private String doFetch(String text, String targetLang) throws Exception { + if (isGoogle()) { + return doFetchGoogle(text, targetLang); + } JsonObject body = new JsonObject(); body.addProperty("q", text); body.addProperty("source", "auto"); @@ -141,12 +263,12 @@ public class TranslationService { if (locale == null || locale.isBlank()) { return "en"; } - String lang = locale.split("_")[0].toLowerCase(Locale.ROOT); - return switch (lang) { - case "zh" -> "zh-CN"; - case "zh-tw", "zh-hk" -> "zh-TW"; - default -> lang; - }; + String normalized = locale.toLowerCase(Locale.ROOT).replace('-', '_'); + if (normalized.startsWith("zh_tw") || normalized.startsWith("zh_hk")) { + return "zh-TW"; + } + String lang = normalized.split("_")[0]; + return "zh".equals(lang) ? "zh-CN" : lang; } public void clearCache() { diff --git a/src/main/java/com/loki/lochat/utils/format/LuckPermsAffixes.java b/src/main/java/com/loki/lochat/utils/format/LuckPermsAffixes.java new file mode 100644 index 0000000..35b2a5f --- /dev/null +++ b/src/main/java/com/loki/lochat/utils/format/LuckPermsAffixes.java @@ -0,0 +1,67 @@ +package com.loki.lochat.utils.format; + +import me.clip.placeholderapi.PlaceholderAPI; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.regex.Pattern; + +/** + * LuckPerms prefix and suffix through PlaceholderAPI (%luckperms_prefix% / %luckperms_suffix%). + * Returns an empty string when PlaceholderAPI or its LuckPerms expansion is not available. + */ +public final class LuckPermsAffixes { + + private static final Pattern COLOR = Pattern.compile("(?i)(&#[0-9a-f]{6}|[&\u00A7][0-9a-fk-orx])"); + + private LuckPermsAffixes() { + } + + public static String prefix(Player player) { + return resolve(player, "%luckperms_prefix%"); + } + + public static String suffix(Player player) { + return resolve(player, "%luckperms_suffix%"); + } + + private static String resolve(Player player, String placeholder) { + if (player == null || !Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { + return ""; + } + try { + String result = PlaceholderAPI.setPlaceholders(player, placeholder); + if (result == null || result.contains("%")) { + return ""; + } + return result; + } catch (Exception | LinkageError e) { + return ""; + } + } + + /** Text without color codes. */ + public static String strip(String text) { + return text == null ? "" : COLOR.matcher(text).replaceAll(""); + } + + /** + * Removes the LuckPerms prefix/suffix from a display name that already contains them + * (for example when another plugin put them into the nickname), so they are not shown twice. + * Returns the original text if nothing was removed. + */ + public static String withoutAffixes(String displayName, String prefix, String suffix) { + String plain = strip(displayName).trim(); + String pp = strip(prefix).trim(); + String sp = strip(suffix).trim(); + String result = plain; + if (!pp.isEmpty() && result.startsWith(pp)) { + result = result.substring(pp.length()).trim(); + } + if (!sp.isEmpty() && result.endsWith(sp)) { + result = result.substring(0, result.length() - sp.length()).trim(); + } + return result.equals(plain) || result.isEmpty() ? displayName : result; + } +} diff --git a/src/main/resources/config/messages.yml b/src/main/resources/config/messages.yml index 0221640..809b282 100644 --- a/src/main/resources/config/messages.yml +++ b/src/main/resources/config/messages.yml @@ -5,25 +5,25 @@ commands: mute: - usage: "&cUsage: &f/mute [time] [-s] [reason]" - time-help: "&7Time format: &f1d&7, &f2h&7, &f30m&7, &f60s&7, &fperm" - silent-help: "&7-s &7= silent mute (requires &flochat.mute.silent&7)" + usage: "&cUsage: &6/lmute [time] [-s] [reason]" + time-help: "&7Time format: &61d&7, &62h&7, &630m&7, &660s&7, &6perm" + silent-help: "&7Use &6-s &7for a silent mute (needs &6lochat.mute.silent&7)" no-silent-permission: "&cNo permission for silent mute" default-reason: "No reason specified" default-duration: "7d" cooldown: - message: "&cPlease wait &f{remaining} &cseconds" + message: "&cPlease wait &6{remaining} &cseconds" clear-chat: - lines-cleared: "&aChat cleared &7({lines} lines)" + lines-cleared: "&fChat cleared &7({lines} lines)" no-permission: "&cNo permission to clear chat" # ── Local Chat ───────────────────────────────────────────────────────────────── local-chat: - nobody-heard: "&7Nobody heard you — no players nearby" - no-players-nearby: "&7No players nearby" + nobody-heard: "&7Nobody heard you. Start your message with &6! &7to be heard everywhere." + no-players-nearby: "&7No players nearby. Start your message with &6! &7to write to everyone." # ── Filters ──────────────────────────────────────────────────────────────────── @@ -42,13 +42,13 @@ filters: # ── Mute Messages ────────────────────────────────────────────────────────────── mute: - muted-message: "&cYou are muted! &7Reason: &f{reason}&7. Remaining: &f{time}" - muted-permanent: "&cYou are permanently muted! &7Reason: &f{reason}" - player-muted: "&aPlayer &f{player} &amuted for &f{time}&a. Reason: &7{reason}" - player-muted-permanent: "&aPlayer &f{player} &amuted permanently. Reason: &7{reason}" - player-muted-silent: "&8[&7SILENT&8] &f{player} &7muted for &f{time}" - player-unmuted: "&aPlayer &f{player} &aunmuted" - not-muted: "&7Player &f{player} &7is not muted" + muted-message: "&cYou are muted! &7Reason: &6{reason}&7. Remaining: &6{time}" + muted-permanent: "&cYou are permanently muted! &7Reason: &6{reason}" + player-muted: "&fPlayer &6{player} &fmuted for &6{time}&f. Reason: &7{reason}" + player-muted-permanent: "&fPlayer &6{player} &fmuted permanently. Reason: &7{reason}" + player-muted-silent: "&8[&7SILENT&8] &6{player} &7muted for &6{time}" + player-unmuted: "&fPlayer &6{player} &funmuted" + not-muted: "&7Player &6{player} &7is not muted" player-not-found: "&cPlayer not found!" invalid-time: "&cInvalid time format!" already-muted: "&cPlayer is already muted!" @@ -66,17 +66,17 @@ gamemodes: private-messages: no-reply-target: "&cNobody to reply to" - message-sent: "&aMessage sent to &f{player}" - player-offline: "&cPlayer &f{player} &cis not online" + message-sent: "&fMessage sent to &6{player}" + player-offline: "&cPlayer &6{player} &cis not online" cannot-message-self: "&cYou cannot message yourself" # ── Ignore ───────────────────────────────────────────────────────────────────── ignore: - player-ignored: "&aPlayer &f{player} &ais now ignored" - player-unignored: "&aPlayer &f{player} &ais no longer ignored" - already-ignored: "&7Player &f{player} &7is already ignored" - not-ignored: "&7Player &f{player} &7is not ignored" + player-ignored: "&fPlayer &6{player} &fis now ignored" + player-unignored: "&fPlayer &6{player} &fis no longer ignored" + already-ignored: "&7Player &6{player} &7is already ignored" + not-ignored: "&7Player &6{player} &7is not ignored" cannot-ignore-self: "&cYou cannot ignore yourself" ignore-list-empty: "&7Your ignore list is empty" ignore-list-header: "&7Ignored players:" @@ -97,7 +97,7 @@ general: days: "d" permanent: "permanent" enabled: "&aEnabled" - disabled: "&7Disabled" + disabled: "&cDisabled" online: "&aOnline" offline: "&7Offline" diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 2a1c53f..a9271b0 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -1,20 +1,20 @@ # LoChat player messages # All messages sent to players. Supports & color codes and placeholders. -prefix: "&8[&3LoChat&8]" +prefix: "&6Star Wars &7|" # ── Global Chat ──────────────────────────────────────────────────────────────── global: - toggled-on: "&aGlobal chat enabled" + toggled-on: "&fGlobal chat enabled" toggled-off: "&cGlobal chat disabled" disabled: "&cGlobal chat is disabled on this server" # ── Local Chat ───────────────────────────────────────────────────────────────── local: - no-players: "&cNo players nearby" - nobody-heard: "&7Nobody heard you — no players nearby" + no-players: "&7No players nearby. Start your message with &6! &7to write to everyone." + nobody-heard: "&7Nobody heard you. Start your message with &6! &7to be heard everywhere." # ── Private Messages ─────────────────────────────────────────────────────────── @@ -29,19 +29,19 @@ pm: # ── Ignore List ──────────────────────────────────────────────────────────────── ignore: - added: "&aPlayer &f{player} &ais now ignored" - removed: "&aPlayer &f{player} &ais no longer ignored" - already-ignored: "&7Player &f{player} &7is already ignored" - not-ignored: "&7Player &f{player} &7is not ignored" + added: "&fPlayer &6{player} &fis now ignored" + removed: "&fPlayer &6{player} &fis no longer ignored" + already-ignored: "&7Player &6{player} &7is already ignored" + not-ignored: "&7Player &6{player} &7is not ignored" self: "&cYou cannot ignore yourself" list-empty: "&7Your ignore list is empty" list-header: "&8\u2500\u2500\u2500\u2500\u2500 Ignored Players \u2500\u2500\u2500\u2500\u2500" - list-footer: "&7Total: &f{count} &7players" + list-footer: "&7Total: &6{count} &7players" # ── Announcements ────────────────────────────────────────────────────────────── announce: - reloaded: "&aConfiguration reloaded" + reloaded: "&fConfiguration reloaded" # ── Chat Spy ─────────────────────────────────────────────────────────────────── @@ -60,7 +60,7 @@ filter: # ── Cooldowns ────────────────────────────────────────────────────────────────── cooldown: - wait: "&cPlease wait &f{time} &cseconds" + wait: "&cPlease wait &6{time} &cseconds" # ── Anti-Spam ────────────────────────────────────────────────────────────────── @@ -72,23 +72,23 @@ antispam: # ── Clear Chat ───────────────────────────────────────────────────────────────── clearchat: - cleared: "&aChat cleared by &f{player}" + cleared: "&fChat cleared by &6{player}" # ── Errors ───────────────────────────────────────────────────────────────────── errors: no-permission: "&cNo permission" player-not-found: "&cPlayer not found" - invalid-usage: "&cUsage: &f{usage}" + invalid-usage: "&cUsage: &6{usage}" # ── General ──────────────────────────────────────────────────────────────────── general: no-permission: "&cYou don't have permission!" players-only: "&cThis command is for players only!" - config-reloaded: "&aConfiguration reloaded!" - invalid-player: "&cPlayer &f{player} &cnot found!" - cooldown-active: "&cPlease wait &f{time}&cs" + config-reloaded: "&fConfiguration reloaded!" + invalid-player: "&cPlayer &6{player} &cnot found!" + cooldown-active: "&cPlease wait &6{time}&cs" command-disabled: "&cCommand disabled!" # ── Hub ──────────────────────────────────────────────────────────────────────── @@ -99,55 +99,55 @@ hub: # ── Mute ─────────────────────────────────────────────────────────────────────── mute: - muted: "&aPlayer &f{player} &amuted for &f{time}" - unmuted: "&aPlayer &f{player} &aunmuted" - already-muted: "&7Player &f{player} &7is already muted" - not-muted: "&7Player &f{player} &7is not muted" - you-muted: "&cYou are muted! &7Remaining: &f{time}" + muted: "&fPlayer &6{player} &fmuted for &6{time}" + unmuted: "&fPlayer &6{player} &funmuted" + already-muted: "&7Player &6{player} &7is already muted" + not-muted: "&7Player &6{player} &7is not muted" + you-muted: "&cYou are muted! &7Remaining: &6{time}" permanent: "&cYou are permanently muted!" # ── Gradients ────────────────────────────────────────────────────────────────── gradient: - color-set: "&aGradient set!" - color-enabled: "&aGradient nickname enabled" + color-set: "&fGradient set!" + color-enabled: "&fGradient nickname enabled" color-disabled: "&7Gradient nickname disabled" color-reset: "&7Gradient reset" prefix-set: "&aPrefix set!" prefix-reset: "&7Prefix reset" - prefix-enabled: "&aPrefix enabled" + prefix-enabled: "&fPrefix enabled" prefix-disabled: "&7Prefix disabled" - invalid-hex: "&cInvalid hex color: &f{color}" - invalid-color: "&cInvalid color: &f{color}" - too-many-colors: "&cToo many colors! Maximum: &f{max}" - too-few-colors: "&cToo few colors! Minimum: &f{min}" - cooldown: "&cPlease wait &f{time} &cseconds" - not-enough-points: "&cNot enough points! Need: &f{price}" + invalid-hex: "&cInvalid hex color: &6{color}" + invalid-color: "&cInvalid color: &6{color}" + too-many-colors: "&cToo many colors! Maximum: &6{max}" + too-few-colors: "&cToo few colors! Minimum: &6{min}" + cooldown: "&cPlease wait &6{time} &cseconds" + not-enough-points: "&cNot enough points! Need: &6{price}" player-not-found: "&cPlayer not found" - color-help: "&cUsage: &f/color ..." - color-success: "&aGradient applied!" + color-help: "&cUsage: &6/color ..." + color-success: "&fGradient applied!" color-no-colors: "&7You have no colors set" - prefix-help: "&cUsage: &f/prefix " + prefix-help: "&cUsage: &6/prefix " prefix-success: "&aPrefix set!" prefix-success-free: "&aPrefix set!" prefix-no-prefix: "&7You have no prefix" - prefix-too-long: "&cPrefix too long! Max: &f{max}" + prefix-too-long: "&cPrefix too long! Max: &6{max}" prefix-no-colors: "&cPrefix cannot contain color codes" only-player: "&cPlayers only!" no-permission: "&cNo permission!" - reload-success: "&aConfiguration reloaded!" - info-header: "&8\u2500\u2500\u2500\u2500\u2500 &f{player} &8\u2500\u2500\u2500\u2500\u2500" - info-prefix: "&7Prefix: &f{prefix}" - info-colors: "&7Colors: &f{colors}" + reload-success: "&fConfiguration reloaded!" + info-header: "&8\u2500\u2500\u2500\u2500\u2500 &6{player} &8\u2500\u2500\u2500\u2500\u2500" + info-prefix: "&7Prefix: &6{prefix}" + info-colors: "&7Colors: &6{colors}" # ── Custom Nicknames ─────────────────────────────────────────────────────────── nick: - current: "&7Your nickname: &f{nick}" - usage: "&cUsage: &f/nick &7or &f/nick reset" + current: "&7Your nickname: &6{nick}" + usage: "&cUsage: &6/nick &7or &6/nick reset" no-nick: "&7You have no custom nickname" - reset-success: "&aNickname reset!" - set-success: "&aNickname changed to: &f{nick}" + reset-success: "&fNickname reset!" + set-success: "&fNickname changed to: &6{nick}" invalid: "&cInvalid nickname!" taken: "&cThis nickname is already taken!" error: "&cFailed to set nickname!" @@ -156,11 +156,11 @@ nick: rp: me: "&7* {player} {action}" - try-format: "&7* {player} tries {action}... &f{result}" + try-format: "&7* {player} tries {action}... &6{result}" try-success: "&a[Success!]" try-fail: "&c[Fail!]" - do-format: "&8[Scene] &f{description} &7({player})" - roll-format: "&7* {player} rolls &f{result} &7out of &f{max}" + do-format: "&8[Scene] &6{description} &7({player})" + roll-format: "&7* {player} rolls &6{result} &7out of &6{max}" roll-min-error: "&cMinimum dice value: 2" roll-max-error: "&cMaximum dice value: 1000000" roll-invalid-number: "&cPlease specify a number: /roll [max]"