This commit is contained in:
loki5512344 2026-03-16 12:43:52 +01:00
parent 5088e3ad1c
commit d1d2b93742
82 changed files with 4008 additions and 3032 deletions

View file

@ -11,6 +11,7 @@ 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.core.filter.AdvancedMessageFilter;
import com.loki.lochat.listener.ChatEventListener;
import com.loki.lochat.listener.DiscordEventListener;
import com.loki.lochat.listener.PlayerEventListener;
@ -33,6 +34,8 @@ public final class LoChat extends JavaPlugin {
private AutoMessageManager autoMessageManager;
private CustomCommandManager customCommandManager;
private DiscordIntegration discordIntegration;
private CommandManager commandManager;
private AdvancedMessageFilter advancedMessageFilter;
// Геттеры
public static LoChat getInstance() {
@ -60,7 +63,8 @@ public final class LoChat extends JavaPlugin {
initIntegrations();
// Команды и слушатели
registerCommands();
commandManager = new CommandManager(this);
commandManager.registerCommands();
registerListeners();
registerPluginChannels();
@ -74,6 +78,10 @@ public final class LoChat extends JavaPlugin {
public void onDisable() {
if (autoMessageManager != null) autoMessageManager.stop();
// Сохраняем статистику
com.loki.lochat.api.service.PlayerDataService pds = serviceRegistry.get(com.loki.lochat.api.service.PlayerDataService.class);
if (pds != null) pds.saveAll();
// Сохранение данных через сервисы
IgnoreService ignoreService = serviceRegistry.get(IgnoreService.class);
if (ignoreService != null) ignoreService.save();
@ -84,6 +92,9 @@ public final class LoChat extends JavaPlugin {
NickService nickService = serviceRegistry.get(NickService.class);
if (nickService != null) nickService.save();
// Останавливаем Discord executor
if (discordIntegration != null) discordIntegration.shutdown();
if (gradientModule != null) gradientModule.shutdown();
getLogger().info("LoChat отключен!");
}
@ -108,62 +119,18 @@ public final class LoChat extends JavaPlugin {
}
}
// Старый метод регистрации команд - заменен на CommandManager
/*
private void registerCommands() {
getCommand("g").setExecutor(new GlobalChatCommand(this));
getCommand("l").setExecutor(new LocalChatCommand(this));
getCommand("msg").setExecutor(new MsgCommand(this));
getCommand("reply").setExecutor(new ReplyCommand(this));
getCommand("ignore").setExecutor(new IgnoreCommand(this));
getCommand("unignore").setExecutor(new UnignoreCommand(this));
getCommand("ignorelist").setExecutor(new IgnoreListCommand(this));
getCommand("announce").setExecutor(new AnnounceCommand(this));
getCommand("chatspy").setExecutor(new ChatSpyCommand(this));
getCommand("clearchat").setExecutor(new ClearChatCommand(this));
NickCommand nickCmd = new NickCommand(this);
getCommand("nick").setExecutor(nickCmd);
getCommand("nick").setTabCompleter(nickCmd);
LoChatCommand loChatCmd = new LoChatCommand(this);
getCommand("lochat").setExecutor(loChatCmd);
getCommand("lochat").setTabCompleter(loChatCmd);
MuteCommand muteCmd = new MuteCommand(this);
getCommand("lmute").setExecutor(muteCmd);
getCommand("lmute").setTabCompleter(muteCmd);
UnmuteCommand unmuteCmd = new UnmuteCommand(this);
getCommand("lunmute").setExecutor(unmuteCmd);
getCommand("lunmute").setTabCompleter(unmuteCmd);
getCommand("lmutelist").setExecutor(new MuteListCommand(this));
MuteHistoryCommand historyCmd = new MuteHistoryCommand(this);
getCommand("lmutehistory").setExecutor(historyCmd);
getCommand("lmutehistory").setTabCompleter(historyCmd);
MuteBlameCommand blameCmd = new MuteBlameCommand(this);
getCommand("lmuteblame").setExecutor(blameCmd);
getCommand("lmuteblame").setTabCompleter(blameCmd);
ClearChatConfigCommand clearCfgCmd = new ClearChatConfigCommand(this);
getCommand("clearchatconfig").setExecutor(clearCfgCmd);
getCommand("clearchatconfig").setTabCompleter(clearCfgCmd);
// Custom commands управление
CustomCommandsCommand customCommandsCmd = new CustomCommandsCommand(this, customCommandManager);
getCommand("customcommands").setExecutor(customCommandsCmd);
getCommand("customcommands").setTabCompleter(customCommandsCmd);
// Discord admin команда (отдельно от кастомной /discord)
DiscordCommand discordAdminCmd = new DiscordCommand(this, discordIntegration);
getCommand("discordadmin").setExecutor(discordAdminCmd);
getCommand("discordadmin").setTabCompleter(discordAdminCmd);
// Код перенесен в CommandManager для лучшей организации
}
*/
private void registerListeners() {
getServer().getPluginManager().registerEvents(new ChatEventListener(this, serviceRegistry), this);
getServer().getPluginManager().registerEvents(new PlayerEventListener(serviceRegistry), this);
// Создаём один экземпляр фильтра и передаём в оба листенера — flood/spam трекеры общие
advancedMessageFilter = new AdvancedMessageFilter(getConfig(), this);
getServer().getPluginManager().registerEvents(new ChatEventListener(this, serviceRegistry, advancedMessageFilter), this);
getServer().getPluginManager().registerEvents(new PlayerEventListener(serviceRegistry, advancedMessageFilter), this);
// Discord listener
if (discordIntegration.isEnabled()) {
@ -208,6 +175,10 @@ public final class LoChat extends JavaPlugin {
return customCommandManager;
}
public CommandManager getCommandManager() {
return commandManager;
}
public DiscordIntegration getDiscordIntegration() {
return discordIntegration;
}

View file

@ -1,89 +0,0 @@
package com.loki.lochat.commands;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
/**
* Команда для настройки сообщения очистки чата
* /clearchatconfig <enable|disable|message> [текст]
*/
public class ClearChatConfigCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
public ClearChatConfigCommand(LoChat plugin) {
this.plugin = plugin;
}
@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"));
return true;
}
if (args.length == 0) {
sender.sendMessage(ChatFormatter.parse("&6Использование:"));
sender.sendMessage(ChatFormatter.parse("&e/clearchatconfig enable &7- включить сообщение"));
sender.sendMessage(ChatFormatter.parse("&e/clearchatconfig disable &7- отключить сообщение"));
sender.sendMessage(ChatFormatter.parse("&e/clearchatconfig message <текст> &7- установить сообщение"));
sender.sendMessage(ChatFormatter.parse("&e/clearchatconfig status &7- показать статус"));
return true;
}
String action = args[0].toLowerCase();
switch (action) {
case "enable" -> {
plugin.getConfigManager().setClearChatMessageEnabled(true);
sender.sendMessage(ChatFormatter.parse("&aСообщение очистки чата включено"));
}
case "disable" -> {
plugin.getConfigManager().setClearChatMessageEnabled(false);
sender.sendMessage(ChatFormatter.parse("&cСообщение очистки чата отключено"));
}
case "message" -> {
if (args.length < 2) {
sender.sendMessage(ChatFormatter.parse("&cУкажите текст сообщения!"));
return true;
}
String message = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
plugin.getConfigManager().setClearChatMessage(message);
sender.sendMessage(ChatFormatter.parse("&aСообщение установлено: &f" + message));
}
case "status" -> {
boolean enabled = plugin.getConfigManager().isClearChatMessageEnabled();
String message = plugin.getConfigManager().getClearChatMessage();
sender.sendMessage(ChatFormatter.parse("&6Статус сообщения очистки чата:"));
sender.sendMessage(ChatFormatter.parse("&eВключено: " + (enabled ? "&aДа" : "&cНет")));
sender.sendMessage(ChatFormatter.parse("&eСообщение: &f" + message));
}
default -> {
sender.sendMessage(ChatFormatter.parse("&cНеизвестная команда! Используйте: enable, disable, message, status"));
}
}
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
if (args.length == 1) {
return Arrays.asList("enable", "disable", "message", "status").stream()
.filter(s -> s.toLowerCase().startsWith(args[0].toLowerCase()))
.toList();
}
return null;
}
}

View file

@ -0,0 +1,106 @@
package com.loki.lochat.commands;
import com.loki.lochat.LoChat;
import com.loki.lochat.commands.admin.*;
import com.loki.lochat.commands.chat.*;
import com.loki.lochat.commands.messaging.*;
import com.loki.lochat.commands.moderation.*;
import com.loki.lochat.commands.nick.*;
import com.loki.lochat.commands.rp.*;
import org.bukkit.command.PluginCommand;
import java.util.HashMap;
import java.util.Map;
/**
* Централизованная регистрация всех команд плагина.
*/
public class CommandManager {
private final LoChat plugin;
private final Map<String, com.loki.lochat.commands.base.BaseCommand> commands = new HashMap<>();
public CommandManager(LoChat plugin) {
this.plugin = plugin;
}
public void registerCommands() {
// ── Чат ──────────────────────────────────────────────────────────────
reg("g", new GlobalChatCommand(plugin));
reg("l", new LocalChatCommand(plugin));
// ── Личные сообщения / игнор ──────────────────────────────────────────
reg("msg", new MsgCommand(plugin));
reg("reply", new ReplyCommand(plugin));
reg("ignore", new IgnoreCommand(plugin));
reg("unignore", new UnignoreCommand(plugin));
reg("ignorelist", new IgnoreListCommand(plugin));
// ── Модерация ─────────────────────────────────────────────────────────
regTab("lmute", new MuteCommand(plugin));
regTab("lunmute", new UnmuteCommand(plugin));
reg("lmutelist", new MuteListCommand(plugin));
regTab("lmutehistory", new MuteHistoryCommand(plugin));
regTab("lmuteblame", new MuteBlameCommand(plugin));
// ── Ник / профиль ─────────────────────────────────────────────────────
regTab("nick", new NickCommand(plugin));
reg("playerinfo", new PlayerInfoCommand(plugin));
// ── Админ ─────────────────────────────────────────────────────────────
reg("announce", new AnnounceCommand(plugin));
reg("chatspy", new ChatSpyCommand(plugin));
reg("clearchat", new ClearChatCommand(plugin));
regTab("clearchatconfig", new ClearChatConfigCommand(plugin));
regTab("lochat", new LoChatCommand(plugin));
reg("lochatreload", new ReloadConfigCommand(plugin));
regTab("discordadmin", new DiscordCommand(plugin, plugin.getDiscordIntegration()));
// ── RP ────────────────────────────────────────────────────────────────
registerBaseCommand("me", new MeCommand(plugin));
registerBaseCommand("try", new TryCommand(plugin));
registerBaseCommand("do", new DoCommand(plugin));
regTab("roll", new RollCommand(plugin));
// ── Кастомные команды ─────────────────────────────────────────────────
regTab("customcommands", new CustomCommandsCommand(plugin, plugin.getCustomCommandManager()));
plugin.getLogger().info("[LoChat] Команды зарегистрированы.");
}
// ── Вспомогательные методы ────────────────────────────────────────────────
/** Регистрирует команду только как CommandExecutor */
private void reg(String name, org.bukkit.command.CommandExecutor executor) {
PluginCommand cmd = plugin.getCommand(name);
if (cmd != null) {
cmd.setExecutor(executor);
} else {
plugin.getLogger().warning("[LoChat] Команда '" + name + "' не найдена в plugin.yml!");
}
}
/** Регистрирует команду как CommandExecutor + TabCompleter */
private void regTab(String name, Object executor) {
PluginCommand cmd = plugin.getCommand(name);
if (cmd != null) {
cmd.setExecutor((org.bukkit.command.CommandExecutor) executor);
if (executor instanceof org.bukkit.command.TabCompleter tc) cmd.setTabCompleter(tc);
} else {
plugin.getLogger().warning("[LoChat] Команда '" + name + "' не найдена в plugin.yml!");
}
}
/** Регистрирует BaseCommand (executor + tab-completer из одного класса) */
private void registerBaseCommand(String name, com.loki.lochat.commands.base.BaseCommand command) {
PluginCommand cmd = plugin.getCommand(name);
if (cmd != null) {
cmd.setExecutor(command);
cmd.setTabCompleter(command);
commands.put(name, command);
} else {
plugin.getLogger().warning("[LoChat] Команда '" + name + "' не найдена в plugin.yml!");
}
}
}

View file

@ -1,106 +0,0 @@
package com.loki.lochat.commands;
import com.loki.lochat.LoChat;
import com.loki.lochat.commands.custom.*;
import org.bukkit.command.CommandExecutor;
import java.util.HashMap;
import java.util.Map;
/**
* Менеджер кастомных команд
*/
public class CustomCommandManager {
private final LoChat plugin;
private final Map<String, CommandExecutor> commands;
public CustomCommandManager(LoChat plugin) {
this.plugin = plugin;
this.commands = new HashMap<>();
registerCommands();
}
/**
* Регистрирует все кастомные команды
*/
private void registerCommands() {
// Информационные команды
commands.put("help", new HelpCommand(plugin));
commands.put("rules", new RulesCommand(plugin));
// Утилиты
commands.put("coords", new CoordsCommand(plugin));
commands.put("ping", new PingCommand(plugin));
commands.put("stats", new StatsCommand(plugin));
// Чат команды
commands.put("me", new MeCommand(plugin));
commands.put("sounds", new SoundsCommand(plugin));
// TODO: Добавить остальные команды
// commands.put("website", new WebsiteCommand(plugin));
// commands.put("time", new TimeCommand(plugin));
// commands.put("list", new ListCommand(plugin));
// commands.put("top", new TopCommand(plugin));
// commands.put("afk", new AfkCommand(plugin));
// commands.put("back", new BackCommand(plugin));
// commands.put("broadcast", new BroadcastCommand(plugin));
// commands.put("maintenance", new MaintenanceCommand(plugin));
// commands.put("motd", new MotdCommand(plugin));
// commands.put("tps", new TpsCommand(plugin));
// commands.put("memory", new MemoryCommand(plugin));
// commands.put("version", new VersionCommand(plugin));
// commands.put("reload", new ReloadCommand(plugin));
// commands.put("automessage", new AutoMessageCommand(plugin));
}
/**
* Регистрирует команды в Bukkit
*/
public void registerBukkitCommands() {
for (Map.Entry<String, CommandExecutor> entry : commands.entrySet()) {
String commandName = entry.getKey();
CommandExecutor executor = entry.getValue();
if (plugin.getCommand(commandName) != null) {
plugin.getCommand(commandName).setExecutor(executor);
plugin.getLogger().info("Зарегистрирована кастомная команда: /" + commandName);
} else {
plugin.getLogger().warning("Не удалось зарегистрировать команду: /" + commandName +
" (не найдена в plugin.yml)");
}
}
}
/**
* Получить исполнителя команды
*/
public CommandExecutor getCommandExecutor(String command) {
return commands.get(command.toLowerCase());
}
/**
* Проверить, является ли команда кастомной
*/
public boolean isCustomCommand(String command) {
return commands.containsKey(command.toLowerCase());
}
/**
* Получить все зарегистрированные команды
*/
public Map<String, CommandExecutor> getCommands() {
return new HashMap<>(commands);
}
/**
* Перезагрузить команды
*/
public void reload() {
commands.clear();
registerCommands();
registerBukkitCommands();
plugin.getLogger().info("Кастомные команды перезагружены!");
}
}

View file

@ -1,131 +0,0 @@
package com.loki.lochat.commands;
import com.loki.lochat.LoChat;
import com.loki.lochat.integrations.DiscordIntegration;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
/**
* Команда для управления Discord интеграцией
*/
public class DiscordCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final DiscordIntegration discord;
public DiscordCommand(LoChat plugin, DiscordIntegration discord) {
this.plugin = plugin;
this.discord = discord;
}
@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")));
return true;
}
if (args.length == 0) {
sendHelp(sender);
return true;
}
switch (args[0].toLowerCase()) {
case "status":
handleStatus(sender);
break;
case "reload":
handleReload(sender);
break;
case "test":
handleTest(sender, args);
break;
default:
sendHelp(sender);
break;
}
return true;
}
private void sendHelp(CommandSender sender) {
sender.sendMessage(ChatFormatter.parse("&6&l=== Discord Integration ==="));
sender.sendMessage(ChatFormatter.parse("&e/discord status &7- Показать статус интеграции"));
sender.sendMessage(ChatFormatter.parse("&e/discord reload &7- Перезагрузить конфигурацию"));
sender.sendMessage(ChatFormatter.parse("&e/discord test [message] &7- Отправить тестовое сообщение"));
}
private void handleStatus(CommandSender sender) {
if (discord.isEnabled()) {
sender.sendMessage(ChatFormatter.parse("&a✓ Discord интеграция включена"));
} else {
sender.sendMessage(ChatFormatter.parse("&c✗ Discord интеграция отключена"));
}
}
private void handleReload(CommandSender sender) {
try {
discord.reload();
sender.sendMessage(ChatFormatter.parse("&a✓ Discord конфигурация перезагружена"));
} catch (Exception e) {
sender.sendMessage(ChatFormatter.parse("&c✗ Ошибка при перезагрузке: " + e.getMessage()));
plugin.getLogger().severe("Error reloading Discord config: " + e.getMessage());
}
}
private void handleTest(CommandSender sender, String[] args) {
if (!discord.isEnabled()) {
sender.sendMessage(ChatFormatter.parse("&c✗ Discord интеграция отключена"));
return;
}
String message = args.length > 1 ? String.join(" ", Arrays.copyOfRange(args, 1, args.length)) : "Тестовое сообщение от " + sender.getName();
sender.sendMessage(ChatFormatter.parse("&e⏳ Отправка тестового сообщения..."));
// Используем CompletableFuture для асинхронной отправки (Folia-совместимо)
java.util.concurrent.CompletableFuture.runAsync(() -> {
try {
discord.sendTestMessage(message, sender.getName());
// Возвращаемся в главный поток для отправки сообщения
if (sender instanceof org.bukkit.entity.Player player) {
player.getScheduler().run(plugin, (task) ->
sender.sendMessage(ChatFormatter.parse("&a✓ Тестовое сообщение отправлено в Discord")), null
);
} else {
// Для консоли можно отправить сразу
sender.sendMessage(ChatFormatter.parse("&a✓ Тестовое сообщение отправлено в Discord"));
}
} catch (Exception e) {
if (sender instanceof org.bukkit.entity.Player player) {
player.getScheduler().run(plugin, (task) ->
sender.sendMessage(ChatFormatter.parse("&c✗ Ошибка отправки: " + e.getMessage())), null
);
} else {
sender.sendMessage(ChatFormatter.parse("&c✗ Ошибка отправки: " + e.getMessage()));
}
plugin.getLogger().severe("Discord test error: " + e.getMessage());
}
});
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
if (!sender.hasPermission("lochat.discord.admin")) {
return null;
}
if (args.length == 1) {
return Arrays.asList("status", "reload", "test");
}
return null;
}
}

View file

@ -1,62 +0,0 @@
package com.loki.lochat.commands;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Команда для добавления игрока в список игнорируемых
* /ignore <ник>
*/
public class IgnoreCommand implements CommandExecutor {
private final LoChat plugin;
private final IgnoreService ignoreService;
public IgnoreCommand(LoChat plugin) {
this.plugin = plugin;
this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Только для игроков!");
return true;
}
if (args.length == 0) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/ignore <ник>")));
return true;
}
Player target = Bukkit.getPlayer(args[0]);
if (target == null) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getPlayerNotFound()));
return true;
}
if (target.equals(player)) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.self")));
return true;
}
if (ignoreService.isIgnoring(player.getUniqueId(), target.getUniqueId())) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.already-ignored")));
return true;
}
ignoreService.addIgnore(player.getUniqueId(), target.getUniqueId());
player.sendMessage(ChatFormatter.parse(
plugin.getMessageConfig().get("ignore.added", "{player}", target.getName())
));
return true;
}
}

View file

@ -1,60 +0,0 @@
package com.loki.lochat.commands;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
import java.util.UUID;
/**
* Команда /ignorelist - показывает список игнорируемых игроков
*/
public class IgnoreListCommand implements CommandExecutor {
private final LoChat plugin;
private final IgnoreService ignoreService;
public IgnoreListCommand(LoChat plugin) {
this.plugin = plugin;
this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Только для игроков!");
return true;
}
Set<UUID> ignoredPlayers = ignoreService.getIgnoredPlayers(player.getUniqueId());
if (ignoredPlayers.isEmpty()) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-empty")));
return true;
}
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-header")));
for (UUID ignoredUuid : ignoredPlayers) {
String playerName = com.loki.lochat.utils.PlayerUtil.getPlayerName(ignoredUuid);
if (playerName != null) {
player.sendMessage(ChatFormatter.parse("&#87CEEB• &#FFFFFF" + playerName));
}
}
player.sendMessage(ChatFormatter.parse(
plugin.getMessageConfig().get("ignore.list-footer")
.replace("{count}", String.valueOf(ignoredPlayers.size()))
));
return true;
}
}

View file

@ -1,99 +0,0 @@
package com.loki.lochat.commands;
import com.loki.lochat.LoChat;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
public class LoChatCommand implements CommandExecutor, TabCompleter {
private static final MiniMessage MM = MiniMessage.miniMessage();
private final LoChat plugin;
public LoChatCommand(LoChat plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("lochat.admin")) {
sender.sendMessage(MM.deserialize(plugin.getMessageConfig().getNoPermission()));
return true;
}
if (args.length == 0) {
sender.sendMessage(MM.deserialize("<#FFD700>LoChat v" + plugin.getPluginMeta().getVersion()));
sender.sendMessage(MM.deserialize("<#808080>Команды:"));
sender.sendMessage(MM.deserialize("<#FFFF00>/lochat reload</color> <#808080>- Перезагрузить конфиги"));
sender.sendMessage(MM.deserialize("<#FFFF00>/lochat commands</color> <#808080>- Список кастомных команд"));
sender.sendMessage(MM.deserialize("<#FFFF00>/lochat commands reload</color> <#808080>- Перезагрузить кастомные команды"));
return true;
}
switch (args[0].toLowerCase()) {
case "reload" -> {
plugin.reload();
sender.sendMessage(MM.deserialize("<#00FF00>Конфигурация перезагружена!"));
}
case "commands" -> {
if (args.length > 1 && args[1].equalsIgnoreCase("reload")) {
plugin.getCustomCommandManager().reload();
sender.sendMessage(MM.deserialize("<#00FF00>Кастомные команды перезагружены!"));
} else {
var commands = plugin.getCustomCommandManager().getCommands();
sender.sendMessage(MM.deserialize("<#FFD700>Кастомные команды (" + commands.size() + "):"));
for (var cmd : commands.values()) {
sender.sendMessage(MM.deserialize("<#FFEB3B>/" + cmd.name() + "</color> <#808080>(" + cmd.type() + ")"));
if (!cmd.aliases().isEmpty()) {
sender.sendMessage(MM.deserialize("<#808080> Алиасы: " + String.join(", ", cmd.aliases())));
}
}
}
}
default ->
sender.sendMessage(MM.deserialize("<#FF0000>Неизвестная команда. Используйте /lochat для помощи."));
}
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
if (!sender.hasPermission("lochat.admin")) {
return new ArrayList<>();
}
if (args.length == 1) {
List<String> completions = new ArrayList<>();
String input = args[0].toLowerCase();
for (String cmd : List.of("reload", "commands")) {
if (cmd.startsWith(input)) {
completions.add(cmd);
}
}
return completions;
}
if (args.length == 2 && args[0].equalsIgnoreCase("commands")) {
List<String> completions = new ArrayList<>();
String input = args[1].toLowerCase();
if ("reload".startsWith(input)) {
completions.add("reload");
}
return completions;
}
return new ArrayList<>();
}
}

View file

@ -1,113 +0,0 @@
package com.loki.lochat.commands;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.api.service.PMService;
import com.loki.lochat.api.service.SpyService;
import com.loki.lochat.utils.ChatFormatter;
import com.loki.lochat.utils.PlayerUtil;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Команда для отправки личных сообщений
* /msg <ник> <сообщение>
*/
public class MsgCommand implements CommandExecutor {
private final LoChat plugin;
private final PMService pmService;
private final IgnoreService ignoreService;
private final SpyService spyService;
public MsgCommand(LoChat plugin) {
this.plugin = plugin;
this.pmService = plugin.getServiceRegistry().get(PMService.class);
this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class);
this.spyService = plugin.getServiceRegistry().get(SpyService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Только для игроков!");
return true;
}
if (!plugin.getConfigManager().isPmEnabled()) {
return true;
}
if (args.length < 2) {
player.sendMessage(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")));
return true;
}
if (target.equals(player)) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.self")));
return true;
}
// Проверка игнора
if (ignoreService.isIgnoring(target.getUniqueId(), player.getUniqueId())) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored")));
return true;
}
// Собираем сообщение
StringBuilder messageBuilder = new StringBuilder();
for (int i = 1; i < args.length; i++) {
if (i > 1) messageBuilder.append(" ");
messageBuilder.append(args[i]);
}
String message = messageBuilder.toString();
// Отправляем сообщения с градиентными именами
player.sendMessage(ChatFormatter.formatPmSentNew(
plugin.getMessageConfig().getPmFormatSent(),
player,
target,
message
));
target.sendMessage(ChatFormatter.formatPmReceivedNew(
plugin.getMessageConfig().getPmFormatReceived(),
player,
target,
message
));
// Звук для получателя
if (plugin.getConfigManager().isPmSoundEnabled()) {
Sound sound = PlayerUtil.parseSound(plugin.getConfigManager().getPmSoundType(), null);
if (sound != null) {
target.playSound(target.getLocation(), sound, 1.0f, 1.0f);
}
}
// Шпион
spyService.broadcastPM(player, target, message);
// Сохраняем последнего собеседника
pmService.setLastConversation(player.getUniqueId(), target.getUniqueId());
pmService.setLastConversation(target.getUniqueId(), player.getUniqueId());
// Логирование
if (plugin.getConfigManager().isPmLogEnabled()) {
plugin.getLogger().info("[PM] " + player.getName() + " -> " + target.getName() + ": " + message);
}
return true;
}
}

View file

@ -1,118 +0,0 @@
package com.loki.lochat.commands;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.data.model.MuteData;
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.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Команда /lmuteblame <модератор> - история мутов выданных модератором
*/
public class MuteBlameCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final MuteService muteService;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
public MuteBlameCommand(LoChat plugin) {
this.plugin = plugin;
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
}
@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"));
return true;
}
if (args.length < 1) {
sender.sendMessage("§cИспользование: /lmuteblame <модератор>");
return true;
}
String operatorName = args[0];
List<MuteData.MuteHistoryEntry> mutes = muteService.getMutesByOperator(operatorName);
if (mutes.isEmpty()) {
sender.sendMessage("§aМодератор §e" + operatorName + " §aне выдавал мутов");
return true;
}
sender.sendMessage("§6§l=== Муты выданные " + operatorName + " (" + mutes.size() + ") ===");
int index = 1;
for (MuteData.MuteHistoryEntry entry : mutes) {
// Статус
String status;
if (entry.unmuted) {
status = "§aРазмучен";
} else {
if (entry.duration == 0) {
status = "§cАктивен (навсегда)";
} else {
long endTime = entry.mutedAt + entry.duration;
if (System.currentTimeMillis() > endTime) {
status = "§7Истёк";
} else {
long remaining = endTime - System.currentTimeMillis();
status = "§cАктивен (" + muteService.formatTime(remaining) + ")";
}
}
}
String durationStr = entry.duration == 0 ? "Навсегда" : muteService.formatTime(entry.duration);
String playerName = entry.playerName != null ? entry.playerName : "???";
sender.sendMessage("§7" + index + ". §f" + playerName + " §7- " + status);
sender.sendMessage(" §7Дата: §f" + dateFormat.format(new Date(entry.mutedAt)));
sender.sendMessage(" §7Длительность: §f" + durationStr);
sender.sendMessage(" §7Причина: §f" + (entry.reason != null ? entry.reason : "Не указана"));
if (entry.unmuted && entry.unmutedBy != null) {
sender.sendMessage(" §7Размутил: §f" + entry.unmutedBy);
}
index++;
if (index > 15) {
sender.sendMessage("§7... и ещё " + (mutes.size() - 15) + " записей");
break;
}
}
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
List<String> completions = new ArrayList<>();
if (args.length == 1) {
String prefix = args[0].toLowerCase();
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getName().toLowerCase().startsWith(prefix)) {
completions.add(player.getName());
}
}
}
return completions;
}
}

View file

@ -1,200 +0,0 @@
package com.loki.lochat.commands;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.utils.ChatFormatter;
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;
/**
* Команда /mute nick [time] [-s] [причина]
* Поддержка -s в любом месте до причины
*/
public class MuteCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final MuteService muteService;
public MuteCommand(LoChat plugin) {
this.plugin = plugin;
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("lochat.mute")) {
sender.sendMessage(plugin.getMessageConfig().getComponent("errors.no-permission"));
return true;
}
if (args.length < 1) {
sender.sendMessage("§cИспользование: /mute <ник> [время] [-s] [причина]");
sender.sendMessage("§7Время: 1d, 2h, 30m, 60s или 0/perm для перманентного");
sender.sendMessage("§7-s - тихий мут (требует право lochat.mute.silent)");
return true;
}
// Парсим аргументы
String targetName = null;
String timeStr = null;
boolean silent = false;
StringBuilder reasonBuilder = new StringBuilder();
for (String arg : args) {
if (arg.equalsIgnoreCase("-s")) {
silent = true;
} else if (targetName == null) {
targetName = arg;
} else if (timeStr == null && isTimeFormat(arg)) {
timeStr = arg;
} else {
if (reasonBuilder.length() > 0) reasonBuilder.append(" ");
reasonBuilder.append(arg);
}
}
// Проверка права на тихий мут
if (silent && !sender.hasPermission("lochat.mute.silent")) {
sender.sendMessage("§cУ вас нет права на тихий мут!");
return true;
}
// Получаем настройки из конфига
String defaultReason = plugin.getConfigManager().getString("mute.default-reason", "Нету.");
String defaultDuration = plugin.getConfigManager().getString("mute.default-duration", "7d");
String reason = reasonBuilder.length() > 0 ? reasonBuilder.toString() : defaultReason;
// Определяем длительность
long duration;
if (timeStr == null || timeStr.isEmpty()) {
if (sender instanceof Player player) {
duration = muteService.getMaxDuration(player);
if (duration == -1) {
duration = muteService.parseTime(defaultDuration);
}
} else {
duration = 0; // Консоль = перманентный
}
} else if (timeStr.equalsIgnoreCase("perm") || timeStr.equals("0")) {
duration = 0; // Перманентный
} else {
duration = muteService.parseTime(timeStr);
if (duration < 0) {
sender.sendMessage("§cНеверный формат времени! Используйте: 1d, 2h, 30m, 60s, perm");
return true;
}
}
// Проверяем право на длительность
if (sender instanceof Player player) {
if (!muteService.canMuteForDuration(player, duration)) {
long maxDur = muteService.getMaxDuration(player);
String maxStr = maxDur == 0 ? "перманентный" : muteService.formatTime(maxDur);
sender.sendMessage("§cВы не можете мутить на такой срок! Максимум: " + maxStr);
return true;
}
}
// Ищем игрока
UUID targetUUID = com.loki.lochat.utils.PlayerUtil.findPlayerUUID(targetName);
if (targetUUID == null) {
sender.sendMessage(plugin.getMessageConfig().getComponent("errors.player-not-found"));
return true;
}
Player target = Bukkit.getPlayer(targetUUID);
String finalTargetName = com.loki.lochat.utils.PlayerUtil.getPlayerName(targetUUID);
// Проверяем, не замучен ли уже
if (muteService.isMuted(targetUUID)) {
sender.sendMessage("§cИгрок уже замучен!");
return true;
}
// Мутим игрока
String operatorName = sender.getName();
muteService.mute(targetUUID, finalTargetName, duration, reason, operatorName);
String timeDisplay = duration == 0 ? "навсегда" : muteService.formatTime(duration);
// Уведомляем отправителя
sender.sendMessage("§aИгрок §e" + finalTargetName + " §aзамучен на §e" + timeDisplay);
sender.sendMessage("§7Причина: §f" + reason);
// Уведомляем замученного (если онлайн)
if (target != null) {
String msgKey = duration == 0 ? "mute.messages.you-muted-permanent" : "mute.messages.you-muted";
String msg = plugin.getConfigManager().getString(msgKey,
duration == 0 ? "§cВы замучены навсегда! Причина: %reason%" : "§cВы замучены на %duration%! Причина: %reason%");
msg = muteService.formatMessage(msg, finalTargetName, operatorName, timeDisplay, reason);
target.sendMessage(ChatFormatter.parse(msg));
}
// Broadcast
if (silent) {
String silentMsg = plugin.getConfigManager().getString("mute.messages.silent-muted",
"§8[Тихо] §c%player% §7замучен на §c%duration% §7(%operator%). Причина: %reason%");
silentMsg = muteService.formatMessage(silentMsg, finalTargetName, operatorName, timeDisplay, reason);
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.hasPermission("lochat.mute.see-silent")) {
p.sendMessage(ChatFormatter.parse(silentMsg));
}
}
} else {
String broadcastMsg = plugin.getConfigManager().getString("mute.messages.muted",
"§c%player% §7был замучен на §c%duration% §7модератором §c%operator%§7. Причина: §c%reason%");
broadcastMsg = muteService.formatMessage(broadcastMsg, finalTargetName, operatorName, timeDisplay, reason);
Bukkit.broadcast(ChatFormatter.parse(broadcastMsg));
}
return true;
}
private boolean isTimeFormat(String str) {
if (str.equalsIgnoreCase("perm")) return true;
return str.matches("\\d+[dhms]?");
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
List<String> completions = new ArrayList<>();
if (args.length == 1) {
String prefix = args[0].toLowerCase();
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getName().toLowerCase().startsWith(prefix)) {
completions.add(player.getName());
}
}
} else if (args.length == 2) {
completions.add("10m");
completions.add("30m");
completions.add("1h");
completions.add("6h");
completions.add("12h");
completions.add("1d");
completions.add("7d");
completions.add("30d");
completions.add("perm");
completions.add("-s");
} else if (args.length == 3) {
completions.add("-s");
}
return completions;
}
}

View file

@ -1,135 +0,0 @@
package com.loki.lochat.commands;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.data.model.MuteData;
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.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* Команда /lmutehistory [ник] - история мутов игрока
*/
public class MuteHistoryCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final MuteService muteService;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
public MuteHistoryCommand(LoChat plugin) {
this.plugin = plugin;
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
}
@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"));
return true;
}
if (args.length < 1) {
sender.sendMessage("§cИспользование: /lmutehistory <ник>");
return true;
}
String targetName = args[0];
// Ищем игрока
UUID targetUUID = com.loki.lochat.utils.PlayerUtil.findPlayerUUID(targetName);
if (targetUUID == null) {
// Пробуем найти по имени в истории
targetUUID = muteService.getUUIDByName(targetName);
}
if (targetUUID == null) {
sender.sendMessage(plugin.getMessageConfig().getComponent("errors.player-not-found"));
return true;
}
List<MuteData.MuteHistoryEntry> history = muteService.getPlayerHistory(targetUUID);
if (history.isEmpty()) {
sender.sendMessage("§aУ игрока §e" + targetName + " §aнет истории мутов");
return true;
}
sender.sendMessage("§6§l=== История мутов " + targetName + " (" + history.size() + ") ===");
int index = 1;
// Выводим от новых к старым
for (int i = history.size() - 1; i >= 0; i--) {
MuteData.MuteHistoryEntry entry = history.get(i);
// Статус
String status;
if (entry.unmuted) {
status = "§aРазмучен";
} else {
// Проверяем, активен ли ещё мут
if (entry.duration == 0) {
status = "§cАктивен (навсегда)";
} else {
long endTime = entry.mutedAt + entry.duration;
if (System.currentTimeMillis() > endTime) {
status = "§7Истёк";
} else {
long remaining = endTime - System.currentTimeMillis();
status = "§cАктивен (" + muteService.formatTime(remaining) + ")";
}
}
}
// Длительность
String durationStr = entry.duration == 0 ? "Навсегда" : muteService.formatTime(entry.duration);
sender.sendMessage("§7" + index + ". §f" + dateFormat.format(new Date(entry.mutedAt)) + " §7- " + status);
sender.sendMessage(" §7Длительность: §f" + durationStr);
sender.sendMessage(" §7Причина: §f" + (entry.reason != null ? entry.reason : "Не указана"));
sender.sendMessage(" §7Выдал: §f" + entry.mutedBy);
if (entry.unmuted && entry.unmutedBy != null) {
sender.sendMessage(" §7Размутил: §f" + entry.unmutedBy + " §7(" + dateFormat.format(new Date(entry.unmutedAt)) + ")");
}
index++;
// Ограничиваем вывод
if (index > 10) {
sender.sendMessage("§7... и ещё " + (history.size() - 10) + " записей");
break;
}
}
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
List<String> completions = new ArrayList<>();
if (args.length == 1) {
String prefix = args[0].toLowerCase();
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getName().toLowerCase().startsWith(prefix)) {
completions.add(player.getName());
}
}
}
return completions;
}
}

View file

@ -1,79 +0,0 @@
package com.loki.lochat.commands;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.data.model.MuteData;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
/**
* Команда /lmutelist - список активных мутов
*/
public class MuteListCommand implements CommandExecutor {
private final LoChat plugin;
private final MuteService muteService;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
public MuteListCommand(LoChat plugin) {
this.plugin = plugin;
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
}
@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"));
return true;
}
Map<UUID, MuteData> activeMutes = muteService.getActiveMutes();
if (activeMutes.isEmpty()) {
sender.sendMessage("§aНет активных мутов");
return true;
}
sender.sendMessage("§6§l=== Активные муты (" + activeMutes.size() + ") ===");
int index = 1;
for (Map.Entry<UUID, MuteData> entry : activeMutes.entrySet()) {
UUID uuid = entry.getKey();
MuteData data = entry.getValue();
// Получаем имя игрока
String playerName = data.getPlayerName();
if (playerName == null) {
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid);
playerName = offlinePlayer.getName() != null ? offlinePlayer.getName() : uuid.toString().substring(0, 8);
}
// Форматируем время
String timeStr;
if (data.isPermanent()) {
timeStr = "§4Навсегда";
} else {
long remaining = data.getEndTime() - System.currentTimeMillis();
timeStr = "§e" + muteService.formatTime(remaining);
}
sender.sendMessage("§7" + index + ". §f" + playerName + " §7- " + timeStr);
sender.sendMessage(" §7Причина: §f" + (data.getReason() != null ? data.getReason() : "Не указана"));
sender.sendMessage(" §7Выдал: §f" + data.getMutedBy() + " §7(" + dateFormat.format(new Date(data.getMutedAt())) + ")");
index++;
}
return true;
}
}

View file

@ -1,139 +0,0 @@
package com.loki.lochat.commands;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.NickService;
import com.loki.lochat.utils.ChatFormatter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
public class NickCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final NickService nickService;
public NickCommand(LoChat plugin) {
this.plugin = plugin;
this.nickService = plugin.getServiceRegistry().get(NickService.class);
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("Эта команда только для игроков!", NamedTextColor.RED));
return true;
}
if (!player.hasPermission("lochat.nick")) {
player.sendMessage(Component.text("У вас нет прав на использование этой команды!", NamedTextColor.RED));
return true;
}
if (args.length == 0) {
return showCurrentNick(player);
}
if (args[0].equalsIgnoreCase("reset")) {
return resetNick(player);
}
String nickname = String.join(" ", args);
return setNick(player, nickname);
}
private boolean showCurrentNick(Player player) {
var currentNick = nickService.getNickname(player.getUniqueId());
if (currentNick.isPresent()) {
Component nickComponent = ChatFormatter.parse(currentNick.get());
player.sendMessage(Component.text("Ваш текущий ник: ", NamedTextColor.GRAY)
.append(nickComponent));
player.sendMessage(Component.text("Используйте ", NamedTextColor.GRAY)
.append(Component.text("/nick reset", NamedTextColor.YELLOW))
.append(Component.text(" для сброса", NamedTextColor.GRAY)));
} else {
player.sendMessage(Component.text("У вас нет кастомного ника", NamedTextColor.GRAY));
player.sendMessage(Component.text("Используйте ", NamedTextColor.GRAY)
.append(Component.text("/nick <ник>", NamedTextColor.YELLOW))
.append(Component.text(" для установки", NamedTextColor.GRAY)));
}
return true;
}
private boolean resetNick(Player player) {
var currentNick = nickService.getNickname(player.getUniqueId());
if (currentNick.isEmpty()) {
player.sendMessage(Component.text("У вас нет кастомного ника!", NamedTextColor.RED));
return true;
}
nickService.resetNickname(player.getUniqueId());
player.sendMessage(Component.text("Ваш ник сброшен!", NamedTextColor.GREEN));
return true;
}
private boolean setNick(Player player, String nickname) {
if (!nickService.isValidNickname(nickname)) {
player.sendMessage(Component.text("Невалидный ник!", NamedTextColor.RED));
player.sendMessage(Component.text("Требования:", NamedTextColor.GRAY));
player.sendMessage(Component.text("• Длина: 3-16 символов", NamedTextColor.GRAY));
player.sendMessage(Component.text("• Разрешены: буквы, цифры, _, русские буквы", NamedTextColor.GRAY));
player.sendMessage(Component.text("• Поддержка цветов: &#FF0000текст, <gradient:&#FF0000:&#00FF00>текст</gradient>", NamedTextColor.GRAY));
return true;
}
if (nickService.isNicknameTaken(nickname)) {
var currentNick = nickService.getNickname(player.getUniqueId());
if (currentNick.isEmpty() || !currentNick.get().equals(nickname)) {
player.sendMessage(Component.text("Этот ник уже занят!", NamedTextColor.RED));
return true;
}
}
boolean success = nickService.setNickname(player.getUniqueId(), nickname);
if (success) {
Component nickComponent = ChatFormatter.parse(nickname);
player.sendMessage(Component.text("Ваш ник изменён на: ", NamedTextColor.GREEN)
.append(nickComponent));
} else {
player.sendMessage(Component.text("Не удалось установить ник!", NamedTextColor.RED));
}
return true;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
List<String> completions = new ArrayList<>();
if (args.length == 1) {
String input = args[0].toLowerCase();
if ("reset".startsWith(input)) {
completions.add("reset");
}
if (sender instanceof Player player) {
var currentNick = nickService.getNickname(player.getUniqueId());
if (currentNick.isPresent()) {
completions.add(currentNick.get());
}
}
completions.add("&#FF0000Красный");
completions.add("&#00FF00Зелёный");
completions.add("Русский_Ник");
}
return completions;
}
}

View file

@ -1,53 +0,0 @@
package com.loki.lochat.commands;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Команда для показа информации об игроке (используется при клике на голову)
*/
public class PlayerInfoCommand implements CommandExecutor {
private final LoChat plugin;
public PlayerInfoCommand(LoChat plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (args.length < 2 || !args[0].equals("playerinfo")) {
return false;
}
String targetName = args[1];
Player target = Bukkit.getPlayer(targetName);
if (target == null || !target.isOnline()) {
sender.sendMessage(ChatFormatter.parse("§cИгрок не найден или не в сети"));
return true;
}
// Показываем информацию об игроке
sender.sendMessage(ChatFormatter.parse("§6=== Информация об игроке ==="));
sender.sendMessage(ChatFormatter.parse("§eИмя: §f" + target.getName()));
sender.sendMessage(ChatFormatter.parse("§eОтображаемое имя: §f" + target.displayName()));
sender.sendMessage(ChatFormatter.parse("§eУровень: §f" + target.getLevel()));
sender.sendMessage(ChatFormatter.parse("§eЗдоровье: §f" + Math.round(target.getHealth()) + "/20"));
sender.sendMessage(ChatFormatter.parse("§eМир: §f" + target.getWorld().getName()));
// Если есть градиентный модуль, показываем градиентное имя
if (plugin.getGradientModule() != null) {
String gradientName = plugin.getGradientModule().getFormattedName(target);
sender.sendMessage(ChatFormatter.parse("§eГрадиентное имя: " + gradientName));
}
return true;
}
}

View file

@ -1,126 +0,0 @@
package com.loki.lochat.commands;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.utils.ChatFormatter;
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;
/**
* Команда /unmute nick [-s]
*/
public class UnmuteCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final MuteService muteService;
public UnmuteCommand(LoChat plugin) {
this.plugin = plugin;
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
}
@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"));
return true;
}
if (args.length < 1) {
sender.sendMessage("§cИспользование: /unmute <ник> [-s]");
return true;
}
// Парсим аргументы
String targetName = null;
boolean silent = false;
for (String arg : args) {
if (arg.equalsIgnoreCase("-s")) {
silent = true;
} else if (targetName == null) {
targetName = arg;
}
}
// Проверка права на тихий размут
if (silent && !sender.hasPermission("lochat.mute.silent")) {
sender.sendMessage("§cУ вас нет права на тихий размут!");
return true;
}
// Ищем игрока
UUID targetUUID = com.loki.lochat.utils.PlayerUtil.findPlayerUUID(targetName);
if (targetUUID == null) {
sender.sendMessage(plugin.getMessageConfig().getComponent("errors.player-not-found"));
return true;
}
Player target = Bukkit.getPlayer(targetUUID);
String finalTargetName = com.loki.lochat.utils.PlayerUtil.getPlayerName(targetUUID);
String operatorName = sender.getName();
// Размучиваем
if (muteService.unmute(targetUUID, operatorName)) {
sender.sendMessage("§aИгрок §e" + finalTargetName + " §aразмучен");
// Уведомляем игрока (если онлайн)
if (target != null) {
target.sendMessage("§aВы были размучены!");
}
// Broadcast
if (silent) {
String silentMsg = plugin.getConfigManager().getString("mute.messages.silent-unmuted",
"§8[Тихо] §a%player% §7размучен (%operator%)");
silentMsg = muteService.formatMessage(silentMsg, finalTargetName, operatorName, null, null);
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.hasPermission("lochat.mute.see-silent")) {
p.sendMessage(ChatFormatter.parse(silentMsg));
}
}
} else {
String broadcastMsg = plugin.getConfigManager().getString("mute.messages.unmuted",
"§a%player% §7был размучен модератором §a%operator%");
broadcastMsg = muteService.formatMessage(broadcastMsg, finalTargetName, operatorName, null, null);
Bukkit.broadcast(ChatFormatter.parse(broadcastMsg));
}
} else {
sender.sendMessage("§cИгрок не замучен!");
}
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
List<String> completions = new ArrayList<>();
if (args.length == 1) {
String prefix = args[0].toLowerCase();
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getName().toLowerCase().startsWith(prefix)) {
completions.add(player.getName());
}
}
} else if (args.length == 2) {
completions.add("-s");
}
return completions;
}
}

View file

@ -1,4 +1,4 @@
package com.loki.lochat.commands;
package com.loki.lochat.commands.admin;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.ChatFormatter;
@ -27,8 +27,6 @@ public class AnnounceCommand implements CommandExecutor {
sender.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/announce <сообщение>")));
return true;
}
// /announce reload
if (args[0].equalsIgnoreCase("reload") && sender.hasPermission("chat.announce.reload")) {
plugin.reload();
sender.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("announce.reloaded")));
@ -36,33 +34,21 @@ public class AnnounceCommand implements CommandExecutor {
}
String message = String.join(" ", args);
String format = plugin.getConfigManager().getAnnouncementFormat();
Component formatted = ChatFormatter.formatAnnouncement(format, message);
Component formatted = ChatFormatter.formatAnnouncement(plugin.getConfigManager().getAnnouncementFormat(), message);
// Отправка в чат
for (Player player : Bukkit.getOnlinePlayers()) {
player.sendMessage(formatted);
// ActionBar (если включен)
if (plugin.getConfigManager().isAnnouncementActionBarEnabled()) {
if (plugin.getConfigManager().isAnnouncementActionBarEnabled())
player.sendActionBar(formatted);
}
// Title (если включен)
if (plugin.getConfigManager().isAnnouncementTitleEnabled()) {
Title title = Title.title(
player.showTitle(Title.title(
ChatFormatter.parse(plugin.getConfigManager().getAnnouncementTitleHeader()),
ChatFormatter.parse(message),
Title.Times.times(
Duration.ofMillis(500),
Title.Times.times(Duration.ofMillis(500),
Duration.ofSeconds(plugin.getConfigManager().getAnnouncementTitleDuration()),
Duration.ofMillis(500)
)
);
player.showTitle(title);
Duration.ofMillis(500))));
}
}
plugin.getLogger().info("[ANNOUNCE] " + sender.getName() + ": " + ChatFormatter.stripTags(message));
return true;
}

View file

@ -1,4 +1,4 @@
package com.loki.lochat.commands;
package com.loki.lochat.commands.admin;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.SpyService;
@ -9,10 +9,6 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Команда для переключения режима шпиона
* /chatspy
*/
public class ChatSpyCommand implements CommandExecutor {
private final LoChat plugin;
@ -26,14 +22,11 @@ public class ChatSpyCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Только для игроков!");
sender.sendMessage(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly());
return true;
}
boolean enabled = spyService.toggleSpy(player.getUniqueId());
String msgKey = enabled ? "spy.enabled" : "spy.disabled";
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get(msgKey)));
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get(enabled ? "spy.enabled" : "spy.disabled")));
return true;
}
}

View file

@ -1,4 +1,4 @@
package com.loki.lochat.commands;
package com.loki.lochat.commands.admin;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.ChatFormatter;
@ -19,19 +19,14 @@ public class ClearChatCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
// Отправляем пустые строки всем игрокам для очистки чата
int lines = plugin.getConfigManager().getAppearanceConfig().getClearLines();
for (Player player : Bukkit.getOnlinePlayers()) {
for (int i = 0; i < com.loki.lochat.utils.Constants.CLEAR_CHAT_LINES; i++) {
player.sendMessage("");
}
// Уведомление (если включено)
for (int i = 0; i < lines; i++) player.sendMessage("");
if (plugin.getConfigManager().isClearChatMessageEnabled()) {
String message = plugin.getConfigManager().getClearChatMessage();
player.sendMessage(ChatFormatter.parse(message.replace("{player}", sender.getName())));
player.sendMessage(ChatFormatter.parse(
plugin.getConfigManager().getClearChatMessage().replace("{player}", sender.getName())));
}
}
return true;
}
}

View file

@ -0,0 +1,57 @@
package com.loki.lochat.commands.admin;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
public class ClearChatConfigCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
public ClearChatConfigCommand(LoChat plugin) {
this.plugin = plugin;
}
@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"));
return true;
}
if (args.length == 0) {
sender.sendMessage(ChatFormatter.parse("&#B798A8Использование: &#7858E9/clearchatconfig <enable|disable|message|status>"));
return true;
}
switch (args[0].toLowerCase()) {
case "enable" -> { plugin.getConfigManager().setClearChatMessageEnabled(true); sender.sendMessage(ChatFormatter.parse("&#9878C9Сообщение очистки &#7858E9включено")); }
case "disable" -> { plugin.getConfigManager().setClearChatMessageEnabled(false); sender.sendMessage(ChatFormatter.parse("&#9878C9Сообщение очистки &#B798A8отключено")); }
case "message" -> {
if (args.length < 2) { sender.sendMessage(ChatFormatter.parse("&#CF6679Укажите текст!")); return true; }
String msg = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
plugin.getConfigManager().setClearChatMessage(msg);
sender.sendMessage(ChatFormatter.parse("&#9878C9Сообщение установлено: &f" + msg));
}
case "status" -> {
sender.sendMessage(ChatFormatter.parse("&#B798A8Включено: " + (plugin.getConfigManager().isClearChatMessageEnabled() ? "&#7858E9да" : "&#CF6679нет")));
sender.sendMessage(ChatFormatter.parse("&#B798A8Текст: &f" + plugin.getConfigManager().getClearChatMessage()));
}
default -> sender.sendMessage(ChatFormatter.parse("&#CF6679Неизвестный аргумент"));
}
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
if (args.length == 1) return List.of("enable", "disable", "message", "status").stream()
.filter(s -> s.startsWith(args[0].toLowerCase())).toList();
return null;
}
}

View file

@ -0,0 +1,65 @@
package com.loki.lochat.commands.admin;
import com.loki.lochat.LoChat;
import com.loki.lochat.integrations.DiscordIntegration;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
public class DiscordCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final DiscordIntegration discord;
public DiscordCommand(LoChat plugin, DiscordIntegration discord) {
this.plugin = plugin;
this.discord = discord;
}
@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")));
return true;
}
if (args.length == 0) { sendHelp(sender); return true; }
switch (args[0].toLowerCase()) {
case "status" -> sender.sendMessage(ChatFormatter.parse(discord.isEnabled() ? "&#9878C9Discord &#7858E9включён" : "&#9878C9Discord &#CF6679отключён"));
case "reload" -> {
try { discord.reload(); sender.sendMessage(ChatFormatter.parse("&#9878C9Discord &#7858E9перезагружен")); }
catch (Exception e) { sender.sendMessage(ChatFormatter.parse("&#CF6679Ошибка: " + e.getMessage())); }
}
case "test" -> {
if (!discord.isEnabled()) { sender.sendMessage(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Отправка..."));
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("&#9878C9Отправлено &#7858E9✓")), null);
else sender.sendMessage(ChatFormatter.parse("&#9878C9Отправлено &#7858E9✓"));
});
}
default -> sendHelp(sender);
}
return true;
}
private void sendHelp(CommandSender s) {
s.sendMessage(ChatFormatter.parse("&#B798A8/discordadmin status|reload|test"));
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
if (!sender.hasPermission("lochat.discord.admin")) return null;
if (args.length == 1) return List.of("status", "reload", "test");
return null;
}
}

View file

@ -1,4 +1,4 @@
package com.loki.lochat.commands;
package com.loki.lochat.commands.admin;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
@ -23,30 +23,16 @@ public class HubCommand implements CommandExecutor {
sender.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("general.players-only")));
return true;
}
// Check if hub command is enabled
if (!plugin.getConfigManager().getBoolean("hub_command.enabled", true)) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("general.command-disabled")));
return true;
}
String hubServer = plugin.getConfigManager().getString("hub_command.server", "hub");
// Send player to hub server via BungeeCord
sendToServer(player, hubServer);
String message = plugin.getMessageConfig().get("hub.connecting")
.replace("%server%", hubServer);
player.sendMessage(ChatFormatter.parse(message));
return true;
}
private void sendToServer(Player player, String server) {
String server = plugin.getConfigManager().getString("hub_command.server", "hub");
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("Connect");
out.writeUTF(server);
player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray());
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("hub.connecting").replace("%server%", server)));
return true;
}
}
}

View file

@ -0,0 +1,62 @@
package com.loki.lochat.commands.admin;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
public class LoChatCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
public LoChatCommand(LoChat plugin) {
this.plugin = plugin;
}
@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()));
return true;
}
if (args.length == 0) {
sender.sendMessage(ChatFormatter.parse("&#7858E9LoChat &#B798A8v" + plugin.getPluginMeta().getVersion()));
sender.sendMessage(ChatFormatter.parse("&#9878C9/lochat reload &#B798A8— перезагрузить конфиги"));
sender.sendMessage(ChatFormatter.parse("&#9878C9/lochat commands [reload] &#B798A8— кастомные команды"));
return true;
}
switch (args[0].toLowerCase()) {
case "reload" -> {
plugin.reload();
sender.sendMessage(ChatFormatter.parse("&#9878C9Конфигурация &#7858E9перезагружена!"));
}
case "commands" -> {
if (args.length > 1 && args[1].equalsIgnoreCase("reload")) {
plugin.getCustomCommandManager().reload();
sender.sendMessage(ChatFormatter.parse("&#9878C9Кастомные команды &#7858E9перезагружены!"));
} else {
var cmds = plugin.getCustomCommandManager().getCommands();
sender.sendMessage(ChatFormatter.parse("&#B798A8Кастомные команды &#9878C9(" + cmds.size() + "&#9878C9):"));
cmds.values().forEach(c -> sender.sendMessage(ChatFormatter.parse("&#7858E9/" + c.name() + " &#B798A8(" + c.type() + ")")));
}
}
default -> sender.sendMessage(ChatFormatter.parse("&#CF6679Неизвестная команда"));
}
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
if (!sender.hasPermission("lochat.admin")) return new ArrayList<>();
if (args.length == 1) return List.of("reload", "commands").stream().filter(s -> s.startsWith(args[0].toLowerCase())).toList();
if (args.length == 2 && args[0].equalsIgnoreCase("commands")) return List.of("reload");
return new ArrayList<>();
}
}

View file

@ -0,0 +1,33 @@
package com.loki.lochat.commands.admin;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class ReloadConfigCommand implements CommandExecutor {
private final LoChat plugin;
public ReloadConfigCommand(LoChat plugin) {
this.plugin = plugin;
}
@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().getHardcodedMessages().getNoPermission()));
return true;
}
try {
plugin.reload();
sender.sendMessage(ChatFormatter.parse("&#9878C9Все конфигурации &#7858E9успешно перезагружены!"));
} catch (Exception e) {
sender.sendMessage(ChatFormatter.parse("&#CF6679Ошибка при перезагрузке: " + e.getMessage()));
plugin.getLogger().severe("Ошибка при перезагрузке: " + e.getMessage());
}
return true;
}
}

View file

@ -0,0 +1,35 @@
package com.loki.lochat.commands.base;
import com.loki.lochat.LoChat;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
/**
* Базовый класс для административных команд
*/
public abstract class AdminCommand extends BaseCommand {
private final String requiredPermission;
public AdminCommand(LoChat plugin, String requiredPermission) {
super(plugin);
this.requiredPermission = requiredPermission;
}
@Override
protected final boolean executeCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (!requirePermission(sender, requiredPermission)) {
return true;
}
return executeAdminCommand(sender, command, label, args);
}
/**
* Выполнить административную команду
*/
protected abstract boolean executeAdminCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args);
}

View file

@ -0,0 +1,101 @@
package com.loki.lochat.commands.base;
import com.loki.lochat.LoChat;
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.List;
/**
* Базовый класс для всех команд плагина
* Реализует общую логику и предоставляет утилиты
*/
public abstract class BaseCommand implements CommandExecutor, TabCompleter {
protected final LoChat plugin;
public BaseCommand(LoChat plugin) {
this.plugin = plugin;
}
@Override
public final boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
try {
return executeCommand(sender, command, label, args);
} catch (Exception e) {
plugin.getLogger().severe("Ошибка в команде " + command.getName() + ": " + e.getMessage());
e.printStackTrace();
sender.sendMessage("§cПроизошла ошибка при выполнении команды!");
return true;
}
}
/**
* Выполнить команду (переопределяется в наследниках)
*/
protected abstract boolean executeCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args);
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
return getTabCompletions(sender, command, alias, args);
}
/**
* Получить автодополнения (переопределяется в наследниках при необходимости)
*/
protected @Nullable List<String> getTabCompletions(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
return null;
}
/**
* Проверить права доступа
*/
protected boolean hasPermission(CommandSender sender, String permission) {
return sender.hasPermission(permission);
}
/**
* Проверить что отправитель - игрок
*/
protected boolean requirePlayer(CommandSender sender) {
if (!(sender instanceof Player)) {
sender.sendMessage(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly());
return false;
}
return true;
}
/**
* Проверить права и отправить сообщение об ошибке если нет прав
*/
protected boolean requirePermission(CommandSender sender, String permission) {
if (!hasPermission(sender, permission)) {
sender.sendMessage(plugin.getConfigManager().getHardcodedMessages().getNoPermission());
return false;
}
return true;
}
/**
* Получить игрока из CommandSender (с проверкой)
*/
protected Player getPlayer(CommandSender sender) {
return sender instanceof Player ? (Player) sender : null;
}
/**
* Отправить сообщение об использовании команды
*/
protected void sendUsage(CommandSender sender, String usage) {
sender.sendMessage("§cИспользование: " + usage);
}
}

View file

@ -0,0 +1,34 @@
package com.loki.lochat.commands.base;
import com.loki.lochat.LoChat;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Базовый класс для команд, доступных только игрокам
*/
public abstract class PlayerCommand extends BaseCommand {
public PlayerCommand(LoChat plugin) {
super(plugin);
}
@Override
protected final boolean executeCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (!requirePlayer(sender)) {
return true;
}
Player player = (Player) sender;
return executePlayerCommand(player, command, label, args);
}
/**
* Выполнить команду для игрока
*/
protected abstract boolean executePlayerCommand(@NotNull Player player, @NotNull Command command,
@NotNull String label, @NotNull String[] args);
}

View file

@ -1,4 +1,4 @@
package com.loki.lochat.commands;
package com.loki.lochat.commands.chat;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.ChatService;
@ -11,11 +11,6 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Команда для глобального чата
* /g <сообщение> - отправить сообщение в глобальный чат
* /globalchat toggle - переключить режим глобального чата
*/
public class GlobalChatCommand implements CommandExecutor {
private static final MiniMessage MM = MiniMessage.miniMessage();
@ -30,34 +25,26 @@ public class GlobalChatCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Только для игроков!");
sender.sendMessage(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly());
return true;
}
// /globalchat toggle
if (command.getName().equalsIgnoreCase("globalchat")) {
if (args.length > 0 && args[0].equalsIgnoreCase("toggle")) {
boolean enabled = chatService.toggleGlobalChat(player.getUniqueId());
String msgKey = enabled ? "global.toggled-on" : "global.toggled-off";
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get(msgKey)));
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get(enabled ? "global.toggled-on" : "global.toggled-off")));
} else {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/globalchat toggle")));
}
return true;
}
// /g <message>
if (args.length == 0) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/g <сообщение>")));
return true;
}
String message = String.join(" ", args);
// Конвертируем String в Component для новой сигнатуры ChatService
Component messageComponent = MM.deserialize(message);
chatService.sendGlobalMessage(player, messageComponent);
chatService.sendGlobalMessage(player, MM.deserialize(String.join(" ", args)));
return true;
}
}

View file

@ -1,9 +1,8 @@
package com.loki.lochat.commands;
package com.loki.lochat.commands.chat;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.ChatService;
import com.loki.lochat.utils.ChatFormatter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
@ -11,10 +10,6 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Команда для локального чата
* /l <сообщение> - отправить сообщение в локальный чат
*/
public class LocalChatCommand implements CommandExecutor {
private static final MiniMessage MM = MiniMessage.miniMessage();
@ -29,7 +24,7 @@ public class LocalChatCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Только для игроков!");
sender.sendMessage(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly());
return true;
}
@ -38,12 +33,7 @@ public class LocalChatCommand implements CommandExecutor {
return true;
}
String message = String.join(" ", args);
// Конвертируем String в Component для новой сигнатуры ChatService
Component messageComponent = MM.deserialize(message);
chatService.sendLocalMessage(player, messageComponent);
chatService.sendLocalMessage(player, MM.deserialize(String.join(" ", args)));
return true;
}
}

View file

@ -1,45 +0,0 @@
package com.loki.lochat.commands.custom;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Команда координат игрока
*/
public class CoordsCommand implements CommandExecutor {
private final LoChat plugin;
public CoordsCommand(LoChat plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(ChatFormatter.parse("<red>Эта команда только для игроков!</red>"));
return true;
}
int x = (int) player.getLocation().getX();
int y = (int) player.getLocation().getY();
int z = (int) player.getLocation().getZ();
String world = player.getWorld().getName();
String message = String.format("""
<gradient:#FFA726:#FF9800>Ваши координаты:</gradient>
<gradient:#87CEEB:#4682B4>X:</gradient> <white>%d</white>
<gradient:#87CEEB:#4682B4>Y:</gradient> <white>%d</white>
<gradient:#87CEEB:#4682B4>Z:</gradient> <white>%d</white>
<gradient:#87CEEB:#4682B4>Мир:</gradient> <white>%s</white>
""", x, y, z, world);
player.sendMessage(ChatFormatter.parse(message));
return true;
}
}

View file

@ -1,41 +0,0 @@
package com.loki.lochat.commands.custom;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
/**
* Команда Discord сервера
*/
public class DiscordCommand implements CommandExecutor {
private final LoChat plugin;
public DiscordCommand(LoChat plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
String discordLink = plugin.getConfig().getString("discord.invite-link", "discord.gg/example");
String message = String.format("""
<gradient:#7289DA:#5865F2>═══════ Наш Discord ═══════</gradient>
<gray>Присоединяйтесь к нашему сообществу!</gray>
<click:open_url:'https://%s'><gradient:#87CEEB:#4682B4>🔗 %s</gradient></click>
<gray>В Discord вы можете:</gray>
<white>•</white> <gray>Общаться с игроками</gray>
<white>•</white> <gray>Получать новости сервера</gray>
<white>•</white> <gray>Участвовать в событиях</gray>
<white>•</white> <gray>Получать поддержку</gray>
""", discordLink, discordLink);
sender.sendMessage(ChatFormatter.parse(message));
return true;
}
}

View file

@ -1,44 +0,0 @@
package com.loki.lochat.commands.custom;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Команда помощи сервера
*/
public class HelpCommand implements CommandExecutor {
private final LoChat plugin;
public HelpCommand(LoChat plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
String message = """
<gradient:#FFD700:#FFA500>═══════ Помощь по серверу ═══════</gradient>
<gradient:#87CEEB:#4682B4>/rules</gradient> <gray>- правила сервера</gray>
<gradient:#87CEEB:#4682B4>/discord</gradient> <gray>- наш Discord</gray>
<gradient:#87CEEB:#4682B4>/website</gradient> <gray>- официальный сайт</gray>
<gradient:#87CEEB:#4682B4>/coords</gradient> <gray>- ваши координаты</gray>
<gradient:#87CEEB:#4682B4>/time</gradient> <gray>- время на сервере</gray>
<gradient:#87CEEB:#4682B4>/ping</gradient> <gray>- ваш пинг</gray>
<gradient:#87CEEB:#4682B4>/list</gradient> <gray>- список игроков</gray>
<gradient:#87CEEB:#4682B4>/me <действие></gradient> <gray>- действие от третьего лица</gray>
<gradient:#87CEEB:#4682B4>/msg <игрок> <сообщение></gradient> <gray>- личное сообщение</gray>
<gradient:#87CEEB:#4682B4>/reply <сообщение></gradient> <gray>- ответить на ЛС</gray>
<gradient:#87CEEB:#4682B4>/ignore <игрок></gradient> <gray>- игнорировать игрока</gray>
<gradient:#87CEEB:#4682B4>/color <цвета></gradient> <gray>- градиентный ник</gray>
<gradient:#87CEEB:#4682B4>/prefix <текст></gradient> <gray>- кастомный префикс</gray>
""";
sender.sendMessage(ChatFormatter.parse(message));
return true;
}
}

View file

@ -1,81 +0,0 @@
package com.loki.lochat.commands.custom;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Команда действия от третьего лица
*/
public class MeCommand implements CommandExecutor {
private final LoChat plugin;
public MeCommand(LoChat plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(ChatFormatter.parse("<red>Эта команда только для игроков!</red>"));
return true;
}
if (!player.hasPermission("chat.me")) {
player.sendMessage(ChatFormatter.parse("<red>У вас нет прав для использования этой команды!</red>"));
return true;
}
if (args.length == 0) {
player.sendMessage(ChatFormatter.parse("<red>Использование: /me <действие></red>"));
return true;
}
// Проверяем мут через LoChat
if (plugin.getServiceRegistry() != null) {
try {
MuteService muteService = plugin.getServiceRegistry().get(MuteService.class);
if (muteService.isMuted(player.getUniqueId())) {
player.sendMessage(ChatFormatter.parse("<red>Вы замучены!</red>"));
return true;
}
} catch (Exception e) {
}
}
// Собираем действие из аргументов
String action = String.join(" ", args);
// Получаем отображаемое имя игрока (с градиентом если есть)
String displayName = player.getName();
if (plugin.getGradientModule() != null) {
displayName = plugin.getGradientModule().getFormattedName(player);
}
// Форматируем сообщение действия
String message = String.format("<gray>* %s %s</gray>", displayName, action);
// Отправляем всем игрокам в радиусе (как локальный чат)
int radius = plugin.getConfig().getInt("chat.local.radius", 100);
for (Player onlinePlayer : Bukkit.getOnlinePlayers()) {
if (onlinePlayer.getWorld().equals(player.getWorld()) &&
onlinePlayer.getLocation().distance(player.getLocation()) <= radius) {
onlinePlayer.sendMessage(ChatFormatter.parse(message));
}
}
// Логируем действие
plugin.getLogger().info(String.format("[ME] %s: %s", player.getName(), action));
return true;
}
}

View file

@ -1,74 +0,0 @@
package com.loki.lochat.commands.custom;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Команда пинга игрока
*/
public class PingCommand implements CommandExecutor {
private final LoChat plugin;
public PingCommand(LoChat plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(ChatFormatter.parse("<red>Эта команда только для игроков!</red>"));
return true;
}
// Если указан аргумент - показать пинг другого игрока
if (args.length > 0) {
if (!player.hasPermission("lochat.ping.others")) {
player.sendMessage(ChatFormatter.parse("<red>У вас нет прав для просмотра пинга других игроков!</red>"));
return true;
}
Player target = Bukkit.getPlayer(args[0]);
if (target == null || !target.isOnline()) {
player.sendMessage(ChatFormatter.parse("<red>Игрок не найден или не в сети!</red>"));
return true;
}
int ping = target.getPing();
String color = getPingColor(ping);
String message = String.format(
"<gradient:#87CEEB:#4682B4>Пинг игрока %s:</gradient> <%s>%dмс</%s>",
target.getName(), color, ping, color
);
player.sendMessage(ChatFormatter.parse(message));
} else {
// Показать свой пинг
int ping = player.getPing();
String color = getPingColor(ping);
String message = String.format(
"<gradient:#87CEEB:#4682B4>Ваш пинг:</gradient> <%s>%dмс</%s>",
color, ping, color
);
player.sendMessage(ChatFormatter.parse(message));
}
return true;
}
private String getPingColor(int ping) {
if (ping < 50) return "green";
if (ping < 100) return "yellow";
if (ping < 200) return "gold";
return "red";
}
}

View file

@ -1,41 +0,0 @@
package com.loki.lochat.commands.custom;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
/**
* Команда правил сервера
*/
public class RulesCommand implements CommandExecutor {
private final LoChat plugin;
public RulesCommand(LoChat plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
String message = """
<gradient:#FF6B6B:#FF4757>═══════ Правила сервера ═══════</gradient>
<white>1.</white> <gray>Не используйте читы и моды</gray>
<white>2.</white> <gray>Уважайте других игроков</gray>
<white>3.</white> <gray>Не спамьте в чате</gray>
<white>4.</white> <gray>Не стройте рядом с чужими постройками</gray>
<white>5.</white> <gray>Не используйте мат и оскорбления</gray>
<white>6.</white> <gray>Не рекламируйте другие серверы</gray>
<white>7.</white> <gray>Слушайтесь администрацию</gray>
<white>8.</white> <gray>Не злоупотребляйте багами</gray>
<gradient:#FFA726:#FF9800>Нарушение правил карается мутом или баном!</gradient>
<gradient:#87CEEB:#4682B4>За помощью обращайтесь к администрации</gradient>
""";
sender.sendMessage(ChatFormatter.parse(message));
return true;
}
}

View file

@ -1,80 +0,0 @@
package com.loki.lochat.commands.custom;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Команда управления звуками
*/
public class SoundsCommand implements CommandExecutor {
private final LoChat plugin;
public SoundsCommand(LoChat plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(ChatFormatter.parse("<red>Эта команда только для игроков!</red>"));
return true;
}
String playerUUID = player.getUniqueId().toString();
// Используем конфиг плагина для хранения настроек
boolean currentState = plugin.getConfig().getBoolean("player-settings." + playerUUID + ".sounds-enabled", true);
if (args.length == 0) {
// Показать текущее состояние
String status = currentState ? "<green>включены</green>" : "<red>отключены</red>";
String message = String.format(
"<gradient:#87CEEB:#4682B4>Звуки сейчас %s</gradient>\n" +
"<gray>Используйте /sounds on или /sounds off для изменения</gray>",
status
);
player.sendMessage(ChatFormatter.parse(message));
return true;
}
String action = args[0].toLowerCase();
boolean newState;
switch (action) {
case "on", "включить", "вкл" -> {
newState = true;
player.sendMessage(ChatFormatter.parse("<green>Звуки включены!</green>"));
}
case "off", "отключить", "выкл" -> {
newState = false;
player.sendMessage(ChatFormatter.parse("<red>Звуки отключены!</red>"));
}
case "toggle", "переключить" -> {
newState = !currentState;
String status = newState ? "<green>включены</green>" : "<red>отключены</red>";
player.sendMessage(ChatFormatter.parse(String.format("Звуки %s!", status)));
}
default -> {
player.sendMessage(ChatFormatter.parse("""
<red>Неверное использование!</red>
<gray>Доступные команды:</gray>
<white>/sounds on</white> <gray>- включить звуки</gray>
<white>/sounds off</white> <gray>- отключить звуки</gray>
<white>/sounds toggle</white> <gray>- переключить</gray>
"""));
return true;
}
}
// Сохраняем настройку
plugin.getConfig().set("player-settings." + playerUUID + ".sounds-enabled", newState);
plugin.saveConfig();
return true;
}
}

View file

@ -1,125 +0,0 @@
package com.loki.lochat.commands.custom;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Команда статистики чата
*/
public class StatsCommand implements CommandExecutor {
private final LoChat plugin;
public StatsCommand(LoChat plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("lochat.stats")) {
sender.sendMessage(ChatFormatter.parse("<red>У вас нет прав для просмотра статистики!</red>"));
return true;
}
if (args.length > 0) {
// Статистика конкретного игрока
Player target = Bukkit.getPlayer(args[0]);
if (target == null) {
sender.sendMessage(ChatFormatter.parse("<red>Игрок не найден!</red>"));
return true;
}
showPlayerStats(sender, target);
} else {
// Общая статистика сервера
showServerStats(sender);
}
return true;
}
private void showPlayerStats(CommandSender sender, Player target) {
// Получаем статистику игрока из конфига
String uuid = target.getUniqueId().toString();
int messagesSent = plugin.getConfig().getInt("player-stats." + uuid + ".messages-sent", 0);
int commandsUsed = plugin.getConfig().getInt("player-stats." + uuid + ".commands-used", 0);
long playtime = plugin.getConfig().getLong("player-stats." + uuid + ".total-playtime", 0);
String playtimeFormatted = formatTime(playtime);
String message = String.format("""
<gradient:#FFD700:#FFA500>═══ Статистика игрока %s ═══</gradient>
<gradient:#87CEEB:#4682B4>Сообщений отправлено:</gradient> <white>%d</white>
<gradient:#87CEEB:#4682B4>Команд использовано:</gradient> <white>%d</white>
<gradient:#87CEEB:#4682B4>Время в игре:</gradient> <white>%s</white>
<gradient:#87CEEB:#4682B4>Статус:</gradient> <green>В сети</green>
<gradient:#87CEEB:#4682B4>Пинг:</gradient> <yellow>%dмс</yellow>
""", target.getName(), messagesSent, commandsUsed, playtimeFormatted, target.getPing());
sender.sendMessage(ChatFormatter.parse(message));
}
private void showServerStats(CommandSender sender) {
// Получаем общую статистику сервера
int totalMessages = plugin.getConfig().getInt("server-stats.total-messages", 0);
int messagesToday = plugin.getConfig().getInt("server-stats.messages-today", 0);
int onlinePlayers = Bukkit.getOnlinePlayers().size();
int maxPlayers = Bukkit.getMaxPlayers();
String message = String.format("""
<gradient:#FFD700:#FFA500>═══ Статистика сервера ═══</gradient>
<gradient:#87CEEB:#4682B4>Всего сообщений:</gradient> <white>%d</white>
<gradient:#87CEEB:#4682B4>Сообщений сегодня:</gradient> <white>%d</white>
<gradient:#87CEEB:#4682B4>Игроков онлайн:</gradient> <white>%d/%d</white>
<gradient:#87CEEB:#4682B4>Активных мутов:</gradient> <white>%d</white>
<gradient:#87CEEB:#4682B4>TPS:</gradient> <green>%.1f</green>
""", totalMessages, messagesToday, onlinePlayers, maxPlayers,
getActiveMutesCount(), getCurrentTPS());
sender.sendMessage(ChatFormatter.parse(message));
}
private String formatTime(long milliseconds) {
long seconds = milliseconds / 1000;
long minutes = seconds / 60;
long hours = minutes / 60;
long days = hours / 24;
if (days > 0) {
return String.format("%dд %dч %dм", days, hours % 24, minutes % 60);
} else if (hours > 0) {
return String.format("%dч %dм", hours, minutes % 60);
} else {
return String.format("%dм", minutes);
}
}
private int getActiveMutesCount() {
// Получаем количество активных мутов через LoChat
if (plugin.getServiceRegistry() != null) {
try {
MuteService muteService = plugin.getServiceRegistry().get(MuteService.class);
return (int) Bukkit.getOnlinePlayers().stream()
.filter(p -> muteService.isMuted(p.getUniqueId()))
.count();
} catch (Exception e) {
}
}
return 0;
}
private double getCurrentTPS() {
try {
return Bukkit.getTPS()[0];
} catch (Exception e) {
return 20.0;
}
}
}

View file

@ -0,0 +1,37 @@
package com.loki.lochat.commands.messaging;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
public class IgnoreCommand implements CommandExecutor {
private final LoChat plugin;
private final IgnoreService ignoreService;
public IgnoreCommand(LoChat plugin) {
this.plugin = plugin;
this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) { sender.sendMessage(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly()); return true; }
if (args.length == 0) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/ignore <ник>"))); return true; }
Player target = Bukkit.getPlayer(args[0]);
if (target == null) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getPlayerNotFound())); return true; }
if (target.equals(player)) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.self"))); return true; }
if (ignoreService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.already-ignored"))); return true; }
ignoreService.addIgnore(player.getUniqueId(), target.getUniqueId());
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.added", "{player}", target.getName())));
return true;
}
}

View file

@ -0,0 +1,41 @@
package com.loki.lochat.commands.messaging;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.utils.ChatFormatter;
import com.loki.lochat.utils.PlayerUtil;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
import java.util.UUID;
public class IgnoreListCommand implements CommandExecutor {
private final LoChat plugin;
private final IgnoreService ignoreService;
public IgnoreListCommand(LoChat plugin) {
this.plugin = plugin;
this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) { sender.sendMessage(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly()); return true; }
Set<UUID> ignored = ignoreService.getIgnoredPlayers(player.getUniqueId());
if (ignored.isEmpty()) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-empty"))); return true; }
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-header")));
for (UUID uuid : ignored) {
String name = PlayerUtil.getPlayerName(uuid);
if (name != null) player.sendMessage(ChatFormatter.parse("&#9878C9◆ &f" + name));
}
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-footer").replace("{count}", String.valueOf(ignored.size()))));
return true;
}
}

View file

@ -0,0 +1,67 @@
package com.loki.lochat.commands.messaging;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.api.service.PMService;
import com.loki.lochat.api.service.SpyService;
import com.loki.lochat.utils.ChatFormatter;
import com.loki.lochat.utils.PlayerUtil;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
public class MsgCommand implements CommandExecutor {
private final LoChat plugin;
private final PMService pmService;
private final IgnoreService ignoreService;
private final SpyService spyService;
public MsgCommand(LoChat plugin) {
this.plugin = plugin;
this.pmService = plugin.getServiceRegistry().get(PMService.class);
this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class);
this.spyService = plugin.getServiceRegistry().get(SpyService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly());
return true;
}
if (!plugin.getConfigManager().isPmEnabled()) return true;
if (args.length < 2) {
player.sendMessage(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"))); return true; }
if (target.equals(player)) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.self"))); return true; }
if (ignoreService.isIgnoring(target.getUniqueId(), player.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); return true; }
if (ignoreService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); return true; }
String message = String.join(" ", java.util.Arrays.copyOfRange(args, 1, args.length));
sendPm(player, target, message);
return true;
}
private void sendPm(Player sender, Player target, String message) {
sender.sendMessage(ChatFormatter.formatPmSentNew(plugin.getMessageConfig().getPmFormatSent(), sender, target, message));
target.sendMessage(ChatFormatter.formatPmReceivedNew(plugin.getMessageConfig().getPmFormatReceived(), sender, target, message));
if (plugin.getConfigManager().isPmSoundEnabled()) {
Sound sound = PlayerUtil.parseSound(plugin.getConfigManager().getPmSoundType(), null);
if (sound != null) target.playSound(target.getLocation(), sound, 1.0f, 1.0f);
}
spyService.broadcastPM(sender, target, message);
pmService.setLastConversation(sender.getUniqueId(), target.getUniqueId());
pmService.setLastConversation(target.getUniqueId(), sender.getUniqueId());
if (plugin.getConfigManager().isPmLogEnabled())
plugin.getLogger().info("[PM] " + sender.getName() + " -> " + target.getName() + ": " + message);
}
}

View file

@ -1,4 +1,4 @@
package com.loki.lochat.commands;
package com.loki.lochat.commands.messaging;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.IgnoreService;
@ -17,10 +17,6 @@ import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.UUID;
/**
* Команда для ответа на последнее личное сообщение
* /reply <сообщение>
*/
public class ReplyCommand implements CommandExecutor {
private final LoChat plugin;
@ -38,74 +34,33 @@ public class ReplyCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Только для игроков!");
sender.sendMessage(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly());
return true;
}
if (!plugin.getConfigManager().isPmEnabled()) {
return true;
}
if (!plugin.getConfigManager().isPmEnabled()) return true;
if (args.length == 0) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/reply <сообщение>")));
return true;
}
Optional<UUID> lastUuidOpt = pmService.getLastConversation(player.getUniqueId());
if (lastUuidOpt.isEmpty()) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.no-reply")));
return true;
}
Optional<UUID> lastUuid = pmService.getLastConversation(player.getUniqueId());
if (lastUuid.isEmpty()) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.no-reply"))); return true; }
UUID lastUuid = lastUuidOpt.get();
Player target = Bukkit.getPlayer(lastUuid);
if (target == null) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.player-offline")));
return true;
}
// Проверка игнора
if (ignoreService.isIgnoring(target.getUniqueId(), player.getUniqueId())) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored")));
return true;
}
Player target = Bukkit.getPlayer(lastUuid.get());
if (target == null) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.player-offline"))); return true; }
if (ignoreService.isIgnoring(target.getUniqueId(), player.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); return true; }
String message = String.join(" ", args);
// Отправляем сообщения с градиентными именами
player.sendMessage(ChatFormatter.formatPmSentNew(
plugin.getMessageConfig().getPmFormatSent(),
player,
target,
message
));
target.sendMessage(ChatFormatter.formatPmReceivedNew(
plugin.getMessageConfig().getPmFormatReceived(),
player,
target,
message
));
// Звук для получателя
player.sendMessage(ChatFormatter.formatPmSentNew(plugin.getMessageConfig().getPmFormatSent(), player, target, message));
target.sendMessage(ChatFormatter.formatPmReceivedNew(plugin.getMessageConfig().getPmFormatReceived(), player, target, message));
if (plugin.getConfigManager().isPmSoundEnabled()) {
Sound sound = PlayerUtil.parseSound(plugin.getConfigManager().getPmSoundType(), null);
if (sound != null) {
target.playSound(target.getLocation(), sound, 1.0f, 1.0f);
}
if (sound != null) target.playSound(target.getLocation(), sound, 1.0f, 1.0f);
}
// Шпион
spyService.broadcastPM(player, target, message);
// Обновляем последнего собеседника
pmService.setLastConversation(target.getUniqueId(), player.getUniqueId());
// Логирование
if (plugin.getConfigManager().isPmLogEnabled()) {
if (plugin.getConfigManager().isPmLogEnabled())
plugin.getLogger().info("[PM] " + player.getName() + " -> " + target.getName() + ": " + message);
}
return true;
}
}

View file

@ -1,4 +1,4 @@
package com.loki.lochat.commands;
package com.loki.lochat.commands.messaging;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.IgnoreService;
@ -10,10 +10,6 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Команда для удаления игрока из списка игнорируемых
* /unignore <ник>
*/
public class UnignoreCommand implements CommandExecutor {
private final LoChat plugin;
@ -26,32 +22,15 @@ public class UnignoreCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Только для игроков!");
return true;
}
if (args.length == 0) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/unignore <ник>")));
return true;
}
if (!(sender instanceof Player player)) { sender.sendMessage(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly()); return true; }
if (args.length == 0) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/unignore <ник>"))); return true; }
Player target = Bukkit.getPlayer(args[0]);
if (target == null) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getPlayerNotFound()));
return true;
}
if (!ignoreService.isIgnoring(player.getUniqueId(), target.getUniqueId())) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.not-ignored")));
return true;
}
if (target == null) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getPlayerNotFound())); return true; }
if (!ignoreService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.not-ignored"))); return true; }
ignoreService.removeIgnore(player.getUniqueId(), target.getUniqueId());
player.sendMessage(ChatFormatter.parse(
plugin.getMessageConfig().get("ignore.removed", "{player}", target.getName())
));
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.removed", "{player}", target.getName())));
return true;
}
}

View file

@ -0,0 +1,59 @@
package com.loki.lochat.commands.moderation;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.data.model.MuteData;
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.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class MuteBlameCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final MuteService muteService;
private final SimpleDateFormat fmt = new SimpleDateFormat("dd.MM.yyyy HH:mm");
public MuteBlameCommand(LoChat plugin) {
this.plugin = plugin;
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
}
@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")); return true; }
if (args.length < 1) { sender.sendMessage("&#CF6679Использование: /lmuteblame <модератор>"); return true; }
List<MuteData.MuteHistoryEntry> mutes = muteService.getMutesByOperator(args[0]);
if (mutes.isEmpty()) { sender.sendMessage("&#9878C9Модератор &#7858E9" + args[0] + " &#9878C9не выдавал мутов"); return true; }
sender.sendMessage("&#7858E9▬▬▬▬▬ &#B798A8Муты от &#9878C9" + args[0] + " &#9878C9(" + mutes.size() + "&#9878C9) &#7858E9▬▬▬▬▬");
int idx = 1;
for (MuteData.MuteHistoryEntry e : mutes) {
if (idx > 15) { sender.sendMessage("&#B798A8...и ещё " + (mutes.size() - 15) + " записей"); break; }
String status = e.unmuted ? "&#9878C9Размучен" : (e.duration == 0 ? "&#CF6679Активен (навсегда)" :
System.currentTimeMillis() > e.mutedAt + e.duration ? "&#B798A8Истёк" : "&#CF6679Активен");
sender.sendMessage("&#9878C9" + idx + ". &f" + (e.playerName != null ? e.playerName : "???") + " &#B798A8— " + status);
sender.sendMessage(" &f" + fmt.format(new Date(e.mutedAt)) + " &#B798A8Длительность: &f" + (e.duration == 0 ? "навсегда" : muteService.formatTime(e.duration)));
sender.sendMessage(" &#B798A8Причина: &f" + (e.reason != null ? e.reason : "—"));
idx++;
}
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
List<String> c = new ArrayList<>();
if (args.length == 1) for (Player p : Bukkit.getOnlinePlayers()) if (p.getName().toLowerCase().startsWith(args[0].toLowerCase())) c.add(p.getName());
return c;
}
}

View file

@ -0,0 +1,147 @@
package com.loki.lochat.commands.moderation;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.config.HardcodedMessages;
import com.loki.lochat.utils.ChatFormatter;
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;
/**
* /mute <ник> [время] [-s] [причина]
*/
public class MuteCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final MuteService muteService;
public MuteCommand(LoChat plugin) {
this.plugin = plugin;
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
HardcodedMessages hm = plugin.getConfigManager().getHardcodedMessages();
if (!sender.hasPermission("lochat.mute")) {
sender.sendMessage(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()));
return true;
}
// Парсим аргументы
String targetName = null;
String timeStr = null;
boolean silent = false;
StringBuilder reasonBuilder = new StringBuilder();
for (String arg : args) {
if (arg.equalsIgnoreCase("-s")) {
silent = true;
} else if (targetName == null) {
targetName = arg;
} else if (timeStr == null && isTimeFormat(arg)) {
timeStr = arg;
} else {
if (reasonBuilder.length() > 0) reasonBuilder.append(" ");
reasonBuilder.append(arg);
}
}
if (silent && !sender.hasPermission("lochat.mute.silent")) {
sender.sendMessage(ChatFormatter.parse(hm.getNoSilentPermission()));
return true;
}
String reason = reasonBuilder.length() > 0 ? reasonBuilder.toString() : hm.getDefaultMuteReason();
// Длительность
long duration;
if (timeStr == null || timeStr.isEmpty()) {
duration = sender instanceof Player p ? muteService.getMaxDuration(p) : 0;
if (duration == -1) duration = muteService.parseTime(hm.getDefaultMuteDuration());
} else if (timeStr.equalsIgnoreCase("perm") || timeStr.equals("0")) {
duration = 0;
} else {
duration = muteService.parseTime(timeStr);
if (duration < 0) { sender.sendMessage(ChatFormatter.parse(hm.getMuteTimeHelp())); return true; }
}
if (sender instanceof Player p && !muteService.canMuteForDuration(p, duration)) {
long max = muteService.getMaxDuration(p);
sender.sendMessage(ChatFormatter.parse("&#CF6679Нельзя мутить на такой срок! &#B798A8Максимум: &#7858E9" +
(max == 0 ? "навсегда" : muteService.formatTime(max))));
return true;
}
UUID targetUUID = com.loki.lochat.utils.PlayerUtil.findPlayerUUID(targetName);
if (targetUUID == null) { sender.sendMessage(plugin.getMessageConfig().getComponent("errors.player-not-found")); return true; }
if (muteService.isMuted(targetUUID)) { sender.sendMessage(ChatFormatter.parse("&#CF6679Игрок уже замучен!")); return true; }
Player target = Bukkit.getPlayer(targetUUID);
String finalName = com.loki.lochat.utils.PlayerUtil.getPlayerName(targetUUID);
String op = sender.getName();
muteService.mute(targetUUID, finalName, duration, reason, op);
String timeDisplay = duration == 0 ? "навсегда" : muteService.formatTime(duration);
sender.sendMessage(ChatFormatter.parse("&#9878C9Игрок &#7858E9" + finalName + " &#9878C9замучен на &#7858E9" + timeDisplay));
sender.sendMessage(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));
}
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));
} else {
String broadcastMsg = (duration == 0 ? hm.getPlayerMutedPermanent() : hm.getPlayerMuted())
.replace("{player}", finalName).replace("{time}", timeDisplay).replace("{reason}", reason);
Bukkit.broadcast(ChatFormatter.parse(broadcastMsg));
}
return true;
}
private boolean isTimeFormat(String s) {
return s.equalsIgnoreCase("perm") || s.matches("\\d+[dhms]?");
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
List<String> 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("10m", "30m", "1h", "6h", "12h", "1d", "7d", "30d", "perm", "-s"));
} else if (args.length == 3) {
c.add("-s");
}
return c;
}
}

View file

@ -0,0 +1,66 @@
package com.loki.lochat.commands.moderation;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.data.model.MuteData;
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.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
public class MuteHistoryCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final MuteService muteService;
private final SimpleDateFormat fmt = new SimpleDateFormat("dd.MM.yyyy HH:mm");
public MuteHistoryCommand(LoChat plugin) {
this.plugin = plugin;
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
}
@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")); return true; }
if (args.length < 1) { sender.sendMessage("&#CF6679Использование: /lmutehistory <ник>"); return true; }
UUID uuid = com.loki.lochat.utils.PlayerUtil.findPlayerUUID(args[0]);
if (uuid == null) uuid = muteService.getUUIDByName(args[0]);
if (uuid == null) { sender.sendMessage(plugin.getMessageConfig().getComponent("errors.player-not-found")); return true; }
List<MuteData.MuteHistoryEntry> history = muteService.getPlayerHistory(uuid);
if (history.isEmpty()) { sender.sendMessage("&#9878C9У &#7858E9" + args[0] + " &#9878C9нет истории мутов"); return true; }
sender.sendMessage("&#7858E9▬▬▬▬▬ &#B798A8История мутов &#9878C9" + args[0] + " &#9878C9(" + history.size() + "&#9878C9) &#7858E9▬▬▬▬▬");
int idx = 1;
for (int i = history.size() - 1; i >= 0 && idx <= 10; i--, idx++) {
MuteData.MuteHistoryEntry e = history.get(i);
String status = e.unmuted ? "&#9878C9Размучен" : (e.duration == 0 ? "&#CF6679Активен (навсегда)" :
System.currentTimeMillis() > e.mutedAt + e.duration ? "&#B798A8Истёк" : "&#CF6679Активен (" + muteService.formatTime(e.mutedAt + e.duration - System.currentTimeMillis()) + ")");
sender.sendMessage("&#9878C9" + idx + ". &f" + fmt.format(new Date(e.mutedAt)) + " &#B798A8— " + status);
sender.sendMessage(" &#B798A8Длительность: &f" + (e.duration == 0 ? "навсегда" : muteService.formatTime(e.duration)));
sender.sendMessage(" &#B798A8Причина: &f" + (e.reason != null ? e.reason : "—") + " &#B798A8Выдал: &#7858E9" + e.mutedBy);
if (e.unmuted && e.unmutedBy != null)
sender.sendMessage(" &#B798A8Размутил: &#7858E9" + e.unmutedBy + " &f(" + fmt.format(new Date(e.unmutedAt)) + ")");
}
if (history.size() > 10) sender.sendMessage("&#B798A8...и ещё " + (history.size() - 10) + " записей");
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
List<String> c = new ArrayList<>();
if (args.length == 1) for (Player p : Bukkit.getOnlinePlayers()) if (p.getName().toLowerCase().startsWith(args[0].toLowerCase())) c.add(p.getName());
return c;
}
}

View file

@ -0,0 +1,49 @@
package com.loki.lochat.commands.moderation;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.data.model.MuteData;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
public class MuteListCommand implements CommandExecutor {
private final LoChat plugin;
private final MuteService muteService;
private final SimpleDateFormat fmt = new SimpleDateFormat("dd.MM.yyyy HH:mm");
public MuteListCommand(LoChat plugin) {
this.plugin = plugin;
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
}
@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")); return true; }
Map<UUID, MuteData> mutes = muteService.getActiveMutes();
if (mutes.isEmpty()) { sender.sendMessage("&#9878C9Нет активных мутов"); return true; }
sender.sendMessage("&#7858E9▬▬▬▬▬ &#B798A8Активные муты &#9878C9(" + mutes.size() + "&#9878C9) &#7858E9▬▬▬▬▬");
int i = 1;
for (Map.Entry<UUID, MuteData> e : mutes.entrySet()) {
MuteData d = e.getValue();
String name = d.getPlayerName() != null ? d.getPlayerName() : Bukkit.getOfflinePlayer(e.getKey()).getName();
String time = d.isPermanent() ? "&#CF6679навсегда" : "&#7858E9" + muteService.formatTime(d.getEndTime() - System.currentTimeMillis());
sender.sendMessage("&#9878C9" + i + ". &f" + name + " &#B798A8— " + time);
sender.sendMessage(" &#B798A8Причина: &f" + (d.getReason() != null ? d.getReason() : "—"));
sender.sendMessage(" &#B798A8Выдал: &#7858E9" + d.getMutedBy() + " &#B798A8(" + fmt.format(new Date(d.getMutedAt())) + ")");
i++;
}
return true;
}
}

View file

@ -0,0 +1,74 @@
package com.loki.lochat.commands.moderation;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.utils.ChatFormatter;
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;
public class UnmuteCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final MuteService muteService;
public UnmuteCommand(LoChat plugin) {
this.plugin = plugin;
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
}
@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")); return true; }
if (args.length < 1) { sender.sendMessage(ChatFormatter.parse("&#CF6679Использование: /unmute <ник> [-s]")); return true; }
String targetName = null;
boolean silent = false;
for (String a : args) {
if (a.equalsIgnoreCase("-s")) silent = true;
else if (targetName == null) targetName = a;
}
if (silent && !sender.hasPermission("lochat.mute.silent")) {
sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getHardcodedMessages().getNoSilentUnmutePermission()));
return true;
}
UUID uuid = com.loki.lochat.utils.PlayerUtil.findPlayerUUID(targetName);
if (uuid == null) { sender.sendMessage(plugin.getMessageConfig().getComponent("errors.player-not-found")); return true; }
String name = com.loki.lochat.utils.PlayerUtil.getPlayerName(uuid);
Player target = Bukkit.getPlayer(uuid);
String op = sender.getName();
if (muteService.unmute(uuid, op)) {
sender.sendMessage(ChatFormatter.parse("&#9878C9Игрок &#7858E9" + name + " &#9878C9размучен"));
if (target != null) target.sendMessage(ChatFormatter.parse("&#9878C9Вы были &#7858E9размучены!"));
if (silent) {
String msg = "&#B798A8[&#9878C9ТИХО&#B798A8] &#7858E9" + name + " &#B798A8размучен &#9878C9(" + op + ")";
for (Player p : Bukkit.getOnlinePlayers()) if (p.hasPermission("lochat.mute.see-silent")) p.sendMessage(ChatFormatter.parse(msg));
} else {
Bukkit.broadcast(ChatFormatter.parse("&#9878C9Игрок &#7858E9" + name + " &#9878C9размучен модератором &#7858E9" + op));
}
} else {
sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getHardcodedMessages().getPlayerNotMutedSimple()));
}
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
List<String> 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.add("-s");
return c;
}
}

View file

@ -0,0 +1,92 @@
package com.loki.lochat.commands.nick;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.NickService;
import com.loki.lochat.utils.ChatFormatter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
public class NickCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final NickService nickService;
public NickCommand(LoChat plugin) {
this.plugin = plugin;
this.nickService = plugin.getServiceRegistry().get(NickService.class);
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly(), NamedTextColor.RED));
return true;
}
if (!player.hasPermission("lochat.nick")) {
player.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getHardcodedMessages().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")));
} else {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.no-nick")));
player.sendMessage(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")));
return true;
}
nickService.resetNickname(player.getUniqueId());
player.sendMessage(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")));
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")));
return true;
}
}
if (nickService.setNickname(player.getUniqueId(), nickname)) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.set-success", "{nick}", nickname)));
} else {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("nick.error")));
}
return true;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
List<String> c = new ArrayList<>();
if (args.length == 1) {
if ("reset".startsWith(args[0].toLowerCase())) c.add("reset");
if (sender instanceof Player p) nickService.getNickname(p.getUniqueId()).ifPresent(c::add);
}
return c;
}
}

View file

@ -0,0 +1,39 @@
package com.loki.lochat.commands.nick;
import com.loki.lochat.LoChat;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
public class PlayerInfoCommand implements CommandExecutor {
private final LoChat plugin;
public PlayerInfoCommand(LoChat plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (args.length < 1) return false;
Player target = Bukkit.getPlayer(args[0]);
if (target == null) {
sender.sendMessage(ChatFormatter.parse("&#CF6679Игрок не найден или не в сети"));
return true;
}
sender.sendMessage(ChatFormatter.parse("&#7858E9▬▬▬▬▬ &#B798A8" + target.getName() + " &#7858E9▬▬▬▬▬"));
sender.sendMessage(ChatFormatter.parse("&#9878C9❤ &fЗдоровье: &#7858E9" + Math.round(target.getHealth()) + "/20"));
sender.sendMessage(ChatFormatter.parse("&#9878C9✦ &fРежим: &#7858E9" + target.getGameMode().name().toLowerCase()));
sender.sendMessage(ChatFormatter.parse("&#9878C9◆ &fМир: &#7858E9" + target.getWorld().getName()));
if (plugin.getGradientModule() != null) {
sender.sendMessage(ChatFormatter.parse("&#9878C9⚡ &fГрадиент: " + plugin.getGradientModule().getFormattedName(target)));
}
return true;
}
}

View file

@ -0,0 +1,61 @@
package com.loki.lochat.commands.rp;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.CooldownService;
import com.loki.lochat.commands.base.PlayerCommand;
import com.loki.lochat.utils.ChatFormatter;
import net.kyori.adventure.text.Component;
import org.bukkit.command.Command;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* /do <описание сцены>
* Описывает событие в мире от третьего лица.
* Формат: [Сцена] описание (Игрок)
*/
public class DoCommand extends PlayerCommand {
private final CooldownService cooldownService;
public DoCommand(LoChat plugin) {
super(plugin);
this.cooldownService = plugin.getServiceRegistry().get(CooldownService.class);
}
@Override
protected boolean executePlayerCommand(@NotNull Player player, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (!requirePermission(player, "lochat.rp.do")) return true;
int cooldown = plugin.getConfigManager().getInt("rp.cooldowns.do", 0);
if (cooldown > 0 && cooldownService.isOnCooldown(player.getUniqueId(), "rp_do", cooldown)) {
int remaining = cooldownService.getRemainingCooldown(player.getUniqueId(), "rp_do", cooldown);
String msg = plugin.getConfigManager().getHardcodedMessages().getCooldownMessage().replace("{remaining}", String.valueOf(remaining));
player.sendMessage(ChatFormatter.parse(msg));
return true;
}
if (args.length == 0) {
sendUsage(player, "/do <описание сцены>");
return true;
}
String description = String.join(" ", args);
String format = plugin.getConfigManager().getString("rp.do-format",
"<dark_gray>[</dark_gray><gray>Сцена</gray><dark_gray>]</dark_gray> <white>{description}</white> <dark_gray>({player})</dark_gray>");
String displayName = RpUtil.getDisplayName(plugin, player);
Component message = ChatFormatter.parse(
format.replace("{player}", displayName)
.replace("{description}", description)
);
int radius = plugin.getConfigManager().getInt("rp.radius", 100);
RpUtil.sendToRadius(player, message, radius);
if (cooldown > 0) cooldownService.setCooldown(player.getUniqueId(), "rp_do");
plugin.getLogger().info("[DO] " + player.getName() + ": " + description);
return true;
}
}

View file

@ -0,0 +1,61 @@
package com.loki.lochat.commands.rp;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.CooldownService;
import com.loki.lochat.commands.base.PlayerCommand;
import com.loki.lochat.utils.ChatFormatter;
import net.kyori.adventure.text.Component;
import org.bukkit.command.Command;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* /me <действие>
* Видят игроки в радиусе rp.radius блоков.
* Формат: * ИгрокИмя действие
*/
public class MeCommand extends PlayerCommand {
private final CooldownService cooldownService;
public MeCommand(LoChat plugin) {
super(plugin);
this.cooldownService = plugin.getServiceRegistry().get(CooldownService.class);
}
@Override
protected boolean executePlayerCommand(@NotNull Player player, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (!requirePermission(player, "lochat.rp.me")) return true;
int cooldown = plugin.getConfigManager().getInt("rp.cooldowns.me", 0);
if (cooldown > 0 && cooldownService.isOnCooldown(player.getUniqueId(), "rp_me", cooldown)) {
int remaining = cooldownService.getRemainingCooldown(player.getUniqueId(), "rp_me", cooldown);
String msg = plugin.getConfigManager().getHardcodedMessages().getCooldownMessage().replace("{remaining}", String.valueOf(remaining));
player.sendMessage(ChatFormatter.parse(msg));
return true;
}
if (args.length == 0) {
sendUsage(player, "/me <действие>");
return true;
}
String action = String.join(" ", args);
String format = plugin.getConfigManager().getString("rp.me-format",
"<gray>* </gray><white>{player}</white><gray> {action}</gray>");
String displayName = RpUtil.getDisplayName(plugin, player);
Component message = ChatFormatter.parse(
format.replace("{player}", displayName)
.replace("{action}", action)
);
int radius = plugin.getConfigManager().getInt("rp.radius", 100);
RpUtil.sendToRadius(player, message, radius);
if (cooldown > 0) cooldownService.setCooldown(player.getUniqueId(), "rp_me");
plugin.getLogger().info("[ME] " + player.getName() + ": " + action);
return true;
}
}

View file

@ -0,0 +1,89 @@
package com.loki.lochat.commands.rp;
import com.loki.lochat.LoChat;
import com.loki.lochat.commands.base.PlayerCommand;
import com.loki.lochat.utils.ChatFormatter;
import net.kyori.adventure.text.Component;
import org.bukkit.command.Command;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Random;
/**
* /roll [максимум]
* Бросает кубик от 1 до максимума (по умолчанию 100).
* Формат: * ИгрокИмя бросает кубик и получает N из M
*/
public class RollCommand extends PlayerCommand implements TabCompleter {
private static final Random RANDOM = new Random();
private static final int DEFAULT_MAX = 100;
private static final int ABSOLUTE_MAX = 1_000_000;
public RollCommand(LoChat plugin) {
super(plugin);
}
@Override
protected boolean executePlayerCommand(@NotNull Player player, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (!requirePermission(player, "lochat.rp.roll")) return true;
int max = DEFAULT_MAX;
if (args.length >= 1) {
try {
max = Integer.parseInt(args[0]);
if (max < 2) {
player.sendMessage(ChatFormatter.parse(
plugin.getConfigManager().getString("rp.roll-min-error",
"<red>Минимальное значение кубика: 2</red>")));
return true;
}
if (max > ABSOLUTE_MAX) {
player.sendMessage(ChatFormatter.parse(
plugin.getConfigManager().getString("rp.roll-max-error",
"<red>Максимальное значение кубика: 1000000</red>")));
return true;
}
} catch (NumberFormatException e) {
player.sendMessage(ChatFormatter.parse(
plugin.getConfigManager().getString("rp.roll-invalid-number",
"<red>Укажите число: /roll [максимум]</red>")));
return true;
}
}
int result = RANDOM.nextInt(max) + 1; // 1..max включительно
String format = plugin.getConfigManager().getString("rp.roll-format",
"<gray>* </gray><white>{player}</white><gray> бросает кубик и получает </gray><yellow><bold>{result}</bold></yellow><gray> из </gray><yellow>{max}</yellow>");
String displayName = RpUtil.getDisplayName(plugin, player);
Component message = ChatFormatter.parse(
format.replace("{player}", displayName)
.replace("{result}", String.valueOf(result))
.replace("{max}", String.valueOf(max))
);
int radius = plugin.getConfigManager().getInt("rp.radius", 100);
RpUtil.sendToRadius(player, message, radius);
plugin.getLogger().info("[ROLL] " + player.getName() + ": " + result + "/" + max);
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull org.bukkit.command.CommandSender sender,
@NotNull Command command, @NotNull String alias,
@NotNull String[] args) {
if (args.length == 1) {
return List.of("6", "10", "20", "100");
}
return List.of();
}
}

View file

@ -0,0 +1,51 @@
package com.loki.lochat.commands.rp;
import com.loki.lochat.LoChat;
import com.loki.lochat.util.DistanceUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
/**
* Вспомогательные методы для RP-команд
*/
public final class RpUtil {
private 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);
}
}
}
/**
* Возвращает отображаемое имя игрока — градиентное если модуль включён,
* иначе обычное. Возвращает plain-string для подстановки в шаблон.
*/
public static String getDisplayName(LoChat plugin, Player player) {
try {
if (plugin.getGradientModule() != null && plugin.getGradientModule().isEnabled()) {
return plugin.getGradientModule().getFormattedName(player);
}
} catch (Exception ignored) {}
// Пробуем взять displayName (может содержать градиент от NickService)
Component displayName = player.displayName();
String plain = PlainTextComponentSerializer.plainText().serialize(displayName);
// Если displayName == обычное имя — возвращаем его напрямую (без потери цветов не нужно)
if (plain.equals(player.getName())) {
return player.getName();
}
// Если displayName содержит кастомный ник — сериализуем обратно в MiniMessage строку
// чтобы она правильно спарсилась через ChatFormatter.parse()
return net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().serialize(displayName);
}
}

View file

@ -0,0 +1,71 @@
package com.loki.lochat.commands.rp;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.CooldownService;
import com.loki.lochat.commands.base.PlayerCommand;
import com.loki.lochat.utils.ChatFormatter;
import net.kyori.adventure.text.Component;
import org.bukkit.command.Command;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.Random;
/**
* /try <действие>
* Случайный исход: успех или провал (50/50).
* Формат: * ИгрокИмя пытается действие... [Успех! / Провал!]
*/
public class TryCommand extends PlayerCommand {
private static final Random RANDOM = new Random();
private final CooldownService cooldownService;
public TryCommand(LoChat plugin) {
super(plugin);
this.cooldownService = plugin.getServiceRegistry().get(CooldownService.class);
}
@Override
protected boolean executePlayerCommand(@NotNull Player player, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (!requirePermission(player, "lochat.rp.try")) return true;
int cooldown = plugin.getConfigManager().getInt("rp.cooldowns.try", 0);
if (cooldown > 0 && cooldownService.isOnCooldown(player.getUniqueId(), "rp_try", cooldown)) {
int remaining = cooldownService.getRemainingCooldown(player.getUniqueId(), "rp_try", cooldown);
String msg = plugin.getConfigManager().getHardcodedMessages().getCooldownMessage().replace("{remaining}", String.valueOf(remaining));
player.sendMessage(ChatFormatter.parse(msg));
return true;
}
if (args.length == 0) {
sendUsage(player, "/try <действие>");
return true;
}
String action = String.join(" ", args);
boolean success = RANDOM.nextBoolean();
String resultKey = success ? "rp.try-success" : "rp.try-fail";
String resultDefault = success ? "<green>[Успех!]</green>" : "<red>[Провал!]</red>";
String resultTag = plugin.getConfigManager().getString(resultKey, resultDefault);
String format = plugin.getConfigManager().getString("rp.try-format",
"<gray>* </gray><white>{player}</white><gray> пытается {action}...</gray> {result}");
String displayName = RpUtil.getDisplayName(plugin, player);
Component message = ChatFormatter.parse(
format.replace("{player}", displayName)
.replace("{action}", action)
.replace("{result}", resultTag)
);
int radius = plugin.getConfigManager().getInt("rp.radius", 100);
RpUtil.sendToRadius(player, message, radius);
if (cooldown > 0) cooldownService.setCooldown(player.getUniqueId(), "rp_try");
plugin.getLogger().info("[TRY] " + player.getName() + ": " + action + " -> " + (success ? "SUCCESS" : "FAIL"));
return true;
}
}

View file

@ -0,0 +1,282 @@
package com.loki.lochat.config;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.Plugin;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
/**
* Конфигурация внешнего вида чата
*/
public class AppearanceConfig {
private final Plugin plugin;
private YamlConfiguration config;
private final Map<String, String> emojiCache = new HashMap<>();
public AppearanceConfig(Plugin plugin) {
this.plugin = plugin;
// Не вызываем loadConfig() в конструкторе для избежания this-escape
}
/**
* Инициализировать конфигурацию (вызывать после создания объекта)
*/
public void init() {
loadConfig();
}
/**
* Загрузить конфигурацию
*/
public void loadConfig() {
File configFile = new File(plugin.getDataFolder(), "config/chat-appearance.yml");
if (!configFile.exists()) {
plugin.saveResource("config/chat-appearance.yml", false);
}
config = YamlConfiguration.loadConfiguration(configFile);
loadEmojiCache();
}
/**
* Перезагрузить конфигурацию
*/
public void reload() {
loadConfig();
}
/**
* Загрузить кэш эмодзи
*/
private void loadEmojiCache() {
emojiCache.clear();
if (config.isConfigurationSection("emojis")) {
for (String key : config.getConfigurationSection("emojis").getKeys(false)) {
emojiCache.put(key, config.getString("emojis." + key));
}
}
}
// ========== ПРЕФИКСЫ ==========
/**
* Получить эмодзи для глобального чата
*/
public String getGlobalEmoji() {
return config.getString("prefixes.global.emoji", "🌍");
}
/**
* Получить текст префикса глобального чата
*/
public String getGlobalText() {
return config.getString("prefixes.global.text", "GLOBAL");
}
/**
* Получить цвета градиента для глобального чата
*/
public List<String> getGlobalColors() {
return config.getStringList("prefixes.global.colors");
}
/**
* Получить разделитель для глобального чата
*/
public String getGlobalSeparatorText() {
return config.getString("prefixes.global.separator.text", " ▶ ");
}
/**
* Получить цвет разделителя для глобального чата
*/
public String getGlobalSeparatorColor() {
return config.getString("prefixes.global.separator.color", "#FFD700");
}
/**
* Получить эмодзи для локального чата
*/
public String getLocalEmoji() {
return config.getString("prefixes.local.emoji", "");
}
/**
* Получить текст префикса локального чата
*/
public String getLocalText() {
return config.getString("prefixes.local.text", "LOCAL");
}
/**
* Получить цвета градиента для локального чата
*/
public List<String> getLocalColors() {
return config.getStringList("prefixes.local.colors");
}
/**
* Получить разделитель для локального чата
*/
public String getLocalSeparatorText() {
return config.getString("prefixes.local.separator.text", " ▶ ");
}
/**
* Получить цвет разделителя для локального чата
*/
public String getLocalSeparatorColor() {
return config.getString("prefixes.local.separator.color", "#FFFFFF");
}
/**
* Получить цвет текста сообщений в глобальном чате (дефолт)
*/
public String getGlobalMessageColor() {
return config.getString("prefixes.global.message-color", "#FFFFFF");
}
/**
* Получить цвет текста сообщений в локальном чате (дефолт)
*/
public String getLocalMessageColor() {
return config.getString("prefixes.local.message-color", "#FFFFFF");
}
// ========== HOVER ЭФФЕКТЫ ==========
/**
* Включены ли hover эффекты
*/
public boolean isHoverEnabled() {
return config.getBoolean("hover.enabled", true);
}
/**
* Получить формат hover подсказки
*/
public List<String> getHoverFormat() {
return config.getStringList("hover.format");
}
// ========== ЭМОДЗИ ==========
/**
* Получить все эмодзи замены
*/
public Map<String, String> getEmojis() {
return new HashMap<>(emojiCache);
}
/**
* Получить замену для эмодзи
*/
public String getEmoji(String key) {
return emojiCache.get(key);
}
// ========== НАСТРОЙКИ ЧАТА ==========
/**
* Получить радиус локального чата
*/
public int getLocalRadius() {
return config.getInt("chat.local_radius", 100);
}
/**
* Получить количество строк для очистки чата
*/
public int getClearLines() {
return config.getInt("chat.clear_lines", 100);
}
/**
* Получить минимальную длину сообщения
*/
public int getMinMessageLength() {
return config.getInt("chat.min_message_length", 1);
}
/**
* Получить максимальную длину сообщения
*/
public int getMaxMessageLength() {
return config.getInt("chat.max_message_length", 2000);
}
/**
* Получить цвет "никто не услышал"
*/
public String getNobodyHeardColor() {
return config.getString("chat.colors.nobody_heard", "#FFA726");
}
/**
* Получить цвет ошибки
*/
public String getErrorColor() {
return config.getString("chat.colors.error", "#FF6B6B");
}
/**
* Получить цвет успеха
*/
public String getSuccessColor() {
return config.getString("chat.colors.success", "#6BCB77");
}
/**
* Получить цвет предупреждения
*/
public String getWarningColor() {
return config.getString("chat.colors.warning", "#FFA726");
}
/**
* Получить цвет информации
*/
public String getInfoColor() {
return config.getString("chat.colors.info", "#87CEEB");
}
// ========== DISCORD ==========
/**
* Получить эмодзи для Discord событий
*/
public String getDiscordEmoji(String event) {
return config.getString("discord.emojis." + event, "");
}
/**
* Получить URL аватара по умолчанию
*/
public String getDefaultAvatarUrl() {
return config.getString("discord.avatar_urls.default", "https://mc-heads.net/avatar/minecraft/64");
}
/**
* Получить URL аватара игрока
*/
public String getPlayerAvatarUrl() {
return config.getString("discord.avatar_urls.player", "https://mc-heads.net/avatar/{player}/64");
}
/**
* Получить цвет embed по умолчанию
*/
public String getDefaultEmbedColor() {
return config.getString("discord.default_embed_color", "5865F2");
}
/**
* Получить заголовок события Discord
*/
public String getDiscordEventTitle(String event) {
return config.getString("discord.event_titles." + event, "");
}
}

View file

@ -10,16 +10,40 @@ public class ConfigManager {
private final LoChat plugin;
private FileConfiguration config;
private AppearanceConfig appearanceConfig;
private HardcodedMessages hardcodedMessages;
public ConfigManager(LoChat plugin) {
this.plugin = plugin;
plugin.saveDefaultConfig();
this.config = plugin.getConfig();
// Инициализируем новые конфигурации
this.appearanceConfig = new AppearanceConfig(plugin);
this.hardcodedMessages = new HardcodedMessages(plugin);
// Инициализируем их после создания
this.appearanceConfig.init();
this.hardcodedMessages.init();
// Проверяем версию конфига и обновляем при необходимости
updateConfig();
}
/**
* Получить конфигурацию внешнего вида
*/
public AppearanceConfig getAppearanceConfig() {
return appearanceConfig;
}
/**
* Получить конфигурацию хардкодированных сообщений
*/
public HardcodedMessages getHardcodedMessages() {
return hardcodedMessages;
}
private void updateConfig() {
int currentVersion = config.getInt("config-version", 1);
int requiredVersion = 2; // Текущая версия конфига
@ -81,6 +105,10 @@ public class ConfigManager {
public void reload() {
plugin.reloadConfig();
this.config = plugin.getConfig();
// Перезагружаем новые конфигурации
this.appearanceConfig.reload();
this.hardcodedMessages.reload();
}
/**
@ -145,7 +173,7 @@ public class ConfigManager {
}
public int getLocalRadius() {
return config.getInt("chat.local.radius", com.loki.lochat.utils.Constants.DEFAULT_LOCAL_RADIUS);
return appearanceConfig.getLocalRadius();
}
public String getLocalPrefix() {

View file

@ -0,0 +1,261 @@
package com.loki.lochat.config;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.Plugin;
import java.io.File;
import java.util.List;
/**
* Конфигурация хардкодированных сообщений
*/
public class HardcodedMessages {
private final Plugin plugin;
private YamlConfiguration config;
public HardcodedMessages(Plugin plugin) {
this.plugin = plugin;
// Не вызываем loadConfig() в конструкторе для избежания this-escape
}
/**
* Инициализировать конфигурацию (вызывать после создания объекта)
*/
public void init() {
loadConfig();
}
/**
* Загрузить конфигурацию
*/
public void loadConfig() {
File configFile = new File(plugin.getDataFolder(), "config/hardcoded-messages.yml");
if (!configFile.exists()) {
plugin.saveResource("config/hardcoded-messages.yml", false);
}
config = YamlConfiguration.loadConfiguration(configFile);
}
/**
* Перезагрузить конфигурацию
*/
public void reload() {
loadConfig();
}
// ========== КОМАНДЫ ==========
/**
* Получить сообщение об использовании команды мута
*/
public String getMuteUsage() {
return config.getString("commands.mute.usage", "§cИспользование: /mute <ник> [время] [-s] [причина]");
}
/**
* Получить помощь по времени мута
*/
public String getMuteTimeHelp() {
return config.getString("commands.mute.time_help", "§7Время: 1d, 2h, 30m, 60s или 0/perm для перманентного");
}
/**
* Получить помощь по тихому муту
*/
public String getMuteSilentHelp() {
return config.getString("commands.mute.silent_help", "§7-s - тихий мут (требует право lochat.mute.silent)");
}
/**
* Получить сообщение об отсутствии прав на тихий мут
*/
public String getNoSilentPermission() {
return config.getString("commands.mute.no_silent_permission", "§cУ вас нет права на тихий мут!");
}
/**
* Получить причину мута по умолчанию
*/
public String getDefaultMuteReason() {
return config.getString("commands.mute.default_reason", "Нету.");
}
/**
* Получить длительность мута по умолчанию
*/
public String getDefaultMuteDuration() {
return config.getString("commands.mute.default_duration", "7d");
}
/**
* Получить сообщение кулдауна
*/
public String getCooldownMessage() {
return config.getString("commands.cooldown.message", "§cПодождите {remaining} сек.");
}
// ========== ЛОКАЛЬНЫЙ ЧАТ ==========
/**
* Получить сообщение "никто не услышал"
*/
public String getNobodyHeard() {
return config.getString("local_chat.nobody_heard", "&#FFA726Вас никто не услышал - рядом нет игроков");
}
/**
* Получить сообщение "рядом нет игроков"
*/
public String getNoPlayersNearby() {
return config.getString("local_chat.no_players_nearby", "&#FFA726Рядом нет игроков");
}
// ========== ФИЛЬТРЫ ==========
/**
* Получить минимальную длину для проверки капса
*/
public int getCapsMinLength() {
return config.getInt("filters.caps.min_length", 3);
}
/**
* Получить сообщение о блокировке капса
*/
public String getCapsBlockedMessage() {
return config.getString("filters.caps.blocked_message", "§cСлишком много заглавных букв!");
}
/**
* Получить уведомление об автоисправлении
*/
public String getAutoLowercaseNotice() {
return config.getString("filters.caps.auto_lowercase_notice", "§7Ваше сообщение было автоматически исправлено");
}
// ========== МУТИРОВАНИЕ ==========
/**
* Получить сообщение о том что игрок замучен
*/
public String getMutedMessage() {
return config.getString("mute.muted_message", "§cВы замучены! Причина: {reason}. Осталось: {time}");
}
/**
* Получить сообщение о перманентном муте
*/
public String getMutedPermanent() {
return config.getString("mute.muted_permanent", "§cВы замучены навсегда! Причина: {reason}");
}
/**
* Получить уведомление о муте игрока
*/
public String getPlayerMuted() {
return config.getString("mute.player_muted", "§aИгрок {player} замучен на {time}. Причина: {reason}");
}
/**
* Получить уведомление о перманентном муте игрока
*/
public String getPlayerMutedPermanent() {
return config.getString("mute.player_muted_permanent", "§aИгрок {player} замучен навсегда. Причина: {reason}");
}
/**
* Получить уведомление о тихом муте
*/
public String getPlayerMutedSilent() {
return config.getString("mute.player_muted_silent", "§7[ТИХИЙ МУТ] {player} замучен на {time}");
}
// ========== ИГРОВЫЕ РЕЖИМЫ ==========
/**
* Получить название игрового режима
*/
public String getGamemodeName(String gamemode) {
return config.getString("gamemodes." + gamemode.toLowerCase(), gamemode);
}
// ========== ОБЩИЕ СООБЩЕНИЯ ==========
/**
* Получить сообщение об отсутствии прав
*/
public String getNoPermission() {
return config.getString("general.no_permission", "§cУ вас нет прав для выполнения этой команды!");
}
/**
* Получить сообщение "только для игроков"
*/
public String getPlayerOnly() {
return config.getString("general.player_only", "§cЭта команда доступна только игрокам!");
}
/**
* Получить сообщение о неверных аргументах
*/
public String getInvalidArguments() {
return config.getString("general.invalid_arguments", "§cНеверные аргументы команды!");
}
/**
* Получить сообщение об отключенной команде
*/
public String getCommandDisabled() {
return config.getString("general.command_disabled", "§cЭта команда отключена!");
}
/**
* Получить сообщение об отключенной функции
*/
public String getFeatureDisabled() {
return config.getString("general.feature_disabled", "§cЭта функция отключена в конфигурации!");
}
/**
* Получить формат времени
*/
public String getTimeFormat(String unit) {
return config.getString("general.time_formats." + unit, unit);
}
/**
* Получить сообщение об игроке не найден
*/
public String getPlayerNotFound() {
return config.getString("general.player_not_found", "§cИгрок не найден!");
}
/**
* Получить сообщение игнорирования
*/
public String getIgnore(String key) {
return config.getString("ignore." + key, "§cОшибка игнорирования!");
}
/**
* Получить сообщение личных сообщений
*/
public String getPrivateMessage(String key) {
return config.getString("private_messages." + key, "§cОшибка личных сообщений!");
}
/**
* Получить сообщение об отсутствии прав на тихий размут
*/
public String getNoSilentUnmutePermission() {
return config.getString("mute.no_silent_unmute_permission", "&#CF6679Нет права на тихий размут!");
}
/**
* Получить сообщение о том что игрок не замучен
*/
public String getPlayerNotMutedSimple() {
return config.getString("mute.player_not_muted_simple", "&#CF6679Игрок не замучен!");
}
}

View file

@ -1,252 +1,452 @@
package com.loki.lochat.core.filter;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonArray;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.List;
import java.io.File;
import java.io.FileReader;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Главный фильтр сообщений.
* Порядок: mute (в MessageService) → caps → swear → advertising → ip → repeat → flood → spam
*/
public class AdvancedMessageFilter {
// ── URL / IP / повторы ─────────────────────────────────────────────────────
private static final Pattern URL_PATTERN = Pattern.compile(
"(https?://)?([\\w-]+\\.)+[\\w-]+(/[\\w-./?%&=]*)?",
Pattern.CASE_INSENSITIVE
);
"(https?://)?([\\w-]+\\.)+[\\w-]+(/[\\w-./?%&=]*)?", Pattern.CASE_INSENSITIVE);
private static final Pattern IP_PATTERN = Pattern.compile(
"\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b"
);
private static final Pattern REPEAT_PATTERN = Pattern.compile(
"(.)\\1{3,}"
);
// Паттерн для скрытых ссылок (h t t p, w w w и т.д.)
"\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b");
private static final Pattern REPEAT_PATTERN = Pattern.compile("(.)\\1{3,}");
private static final Pattern HIDDEN_URL_PATTERN = Pattern.compile(
"(?i)(h\\s*t\\s*t\\s*p|w\\s*w\\s*w|d\\s*o\\s*t\\s*c\\s*o\\s*m)",
Pattern.CASE_INSENSITIVE
);
// Паттерн для доменов без http
"(?i)(h\\s*t\\s*t\\s*p|w\\s*w\\s*w|d\\s*o\\s*t\\s*c\\s*o\\s*m)");
private static final Pattern DOMAIN_PATTERN = Pattern.compile(
"\\b([a-zA-Z0-9-]+\\.(com|net|org|ru|gg|me|io|xyz|tk|ml|ga|cf|gq))\\b",
Pattern.CASE_INSENSITIVE
);
Pattern.CASE_INSENSITIVE);
private final FileConfiguration config;
private final List<String> whitelistedDomains;
private final List<String> blacklistedDomains;
private final CapsFilter capsFilter;
// ── Word filter ────────────────────────────────────────────────────────────
private final Set<String> badWords = new HashSet<>();
private final Set<String> badFragments = new HashSet<>();
private final char replacementChar;
private final boolean checkFragments;
private final boolean ignoreCase;
// ── Anti-flood: сколько сообщений за период ────────────────────────────────
// UUID → список timestamp'ов
private final Map<UUID, Deque<Long>> floodTracker = new ConcurrentHashMap<>();
// ── Anti-spam: последние N сообщений игрока ────────────────────────────────
private final Map<UUID, Deque<String>> spamTracker = new ConcurrentHashMap<>();
public AdvancedMessageFilter(FileConfiguration config) {
this(config, null);
}
public AdvancedMessageFilter(FileConfiguration config, JavaPlugin plugin) {
this.config = config;
this.whitelistedDomains = config.getStringList("filters.advertising.whitelist");
this.blacklistedDomains = config.getStringList("filters.advertising.blacklist");
// Инициализация капс фильтра
int maxCapsPercent = config.getInt("filters.caps.max-percent", 70);
int minLength = config.getInt("filters.caps.min-length", 5);
boolean autoLowercase = config.getBoolean("filters.caps.auto-lowercase", true);
boolean blockMessage = config.getBoolean("filters.caps.block", false);
this.capsFilter = new CapsFilter(maxCapsPercent, minLength, autoLowercase, blockMessage);
int minLength = config.getInt("filters.caps.min-length", 5);
boolean autoLower = config.getBoolean("filters.caps.auto-lowercase", true);
boolean blockCaps = config.getBoolean("filters.caps.block", false);
this.capsFilter = new CapsFilter(maxCapsPercent, minLength, autoLower, blockCaps);
this.replacementChar = config.getString("filters.swear.replacement-char", "*").charAt(0);
this.checkFragments = config.getBoolean("filters.swear.check-fragments", true);
this.ignoreCase = config.getBoolean("filters.swear.ignore-case", true);
loadWordFilter(plugin);
}
// ──────────────────────────────────────────────────────────────────────────
// Загрузка словаря
// ──────────────────────────────────────────────────────────────────────────
private void loadWordFilter(JavaPlugin plugin) {
if (!config.getBoolean("filters.swear.enabled", true)) return;
// Встроенные слова из config
List<String> builtIn = config.getStringList("filters.swear.words");
for (String w : builtIn) {
if (!w.isBlank() && !w.startsWith("плохое")) {
badWords.add(ignoreCase ? w.toLowerCase() : w);
}
}
// Внешний файл filter-words.json
if (!config.getBoolean("filters.swear.use-external-file", true)) return;
if (plugin == null) return;
File jsonFile = new File(plugin.getDataFolder(), "filter-words.json");
if (!jsonFile.exists()) {
// Копируем из ресурсов
plugin.saveResource("data/filter-words.json", false);
jsonFile = new File(plugin.getDataFolder(), "data/filter-words.json");
}
if (!jsonFile.exists()) return;
try (FileReader reader = new FileReader(jsonFile)) {
JsonObject root = new Gson().fromJson(reader, JsonObject.class);
JsonArray words = root.getAsJsonArray("words");
if (words != null) {
for (var el : words) {
String w = el.getAsString();
badWords.add(ignoreCase ? w.toLowerCase() : w);
}
}
JsonArray fragments = root.getAsJsonArray("fragments");
if (fragments != null && checkFragments) {
for (var el : fragments) {
String f = el.getAsString();
badFragments.add(ignoreCase ? f.toLowerCase() : f);
}
}
plugin.getLogger().info("[LoChat] Word filter loaded: "
+ badWords.size() + " words, " + badFragments.size() + " fragments");
} catch (Exception e) {
if (plugin != null)
plugin.getLogger().warning("[LoChat] Failed to load filter-words.json: " + e.getMessage());
}
}
// ──────────────────────────────────────────────────────────────────────────
// Главный метод
// ──────────────────────────────────────────────────────────────────────────
public FilterResult filterMessage(Player player, String message) {
// Капс фильтр
// 1. Капс
if (config.getBoolean("filters.caps.enabled", true)) {
String filtered = capsFilter.filter(player, message);
if (filtered == null) {
return new FilterResult(false, message, "&#FF6B6BНе кричите в чате!");
return blocked("&#CF6679Не кричите в чате");
}
message = filtered;
}
// Анти-реклама (объединенная проверка)
// 2. Мат
if (config.getBoolean("filters.swear.enabled", true)
&& !player.hasPermission("lochat.bypass.swear")
&& !player.hasPermission("lochat.bypass.filter")) {
FilterResult swear = filterSwear(player, message);
if (!swear.allowed()) return swear;
message = swear.filteredMessage();
}
// 3. Реклама / URL
if (config.getBoolean("filters.advertising.enabled", true)) {
// URL фильтр
FilterResult urlResult = filterUrls(player, message);
if (!urlResult.allowed()) {
return urlResult;
}
message = urlResult.filteredMessage();
FilterResult r = filterUrls(player, message);
if (!r.allowed()) return r;
message = r.filteredMessage();
// Скрытые ссылки
FilterResult hiddenResult = filterHiddenUrls(player, message);
if (!hiddenResult.allowed()) {
return hiddenResult;
}
r = filterHiddenUrls(player, message);
if (!r.allowed()) return r;
// Домены без http
FilterResult domainResult = filterDomains(player, message);
if (!domainResult.allowed()) {
return domainResult;
}
r = filterDomains(player, message);
if (!r.allowed()) return r;
}
// IP фильтр
// 4. IP
if (config.getBoolean("filters.ip.enabled", true)) {
FilterResult ipResult = filterIPs(player, message);
if (!ipResult.allowed()) {
return ipResult;
}
message = ipResult.filteredMessage();
FilterResult r = filterIPs(player, message);
if (!r.allowed()) return r;
message = r.filteredMessage();
}
// Повторяющиеся символы
// 5. Повторяющиеся символы
if (config.getBoolean("filters.repeat.enabled", true)) {
message = filterRepeatingChars(message);
}
// 6. Anti-flood (количество сообщений в период)
if (config.getBoolean("filters.flood.enabled", true)
&& !player.hasPermission("lochat.bypass.flood")
&& !player.hasPermission("lochat.bypass.filter")) {
FilterResult r = filterFlood(player);
if (!r.allowed()) return r;
}
// 7. Anti-spam (похожие сообщения)
if (config.getBoolean("filters.spam.enabled", true)
&& !player.hasPermission("lochat.bypass.spam")
&& !player.hasPermission("lochat.bypass.filter")) {
FilterResult r = filterSpam(player, message);
if (!r.allowed()) return r;
}
return new FilterResult(true, message, null);
}
// ──────────────────────────────────────────────────────────────────────────
// Фильтр мата
// ──────────────────────────────────────────────────────────────────────────
private FilterResult filterSwear(Player player, String message) {
String action = config.getString("filters.swear.action", "replace");
String check = ignoreCase ? message.toLowerCase() : message;
// Проверка полных слов
String[] tokens = check.split("\\s+");
boolean hasBad = false;
for (String token : tokens) {
// убираем пунктуацию по краям
String clean = token.replaceAll("^[^а-яёa-z]+|[^а-яёa-z]+$", "");
if (badWords.contains(clean)) {
hasBad = true;
break;
}
}
// Проверка фрагментов (подстрок)
if (!hasBad && checkFragments) {
for (String frag : badFragments) {
if (check.contains(frag)) {
hasBad = true;
break;
}
}
}
if (!hasBad) return new FilterResult(true, message, null);
return switch (action) {
case "block" -> blocked(config.getString("filters.swear.block-message",
"&#CF6679Мат запрещён"));
case "replace" -> {
String replaced = replaceSwear(message);
yield new FilterResult(true, replaced, null);
}
default -> new FilterResult(true, message, null);
};
}
/**
* Заменяет матные слова звёздочками, сохраняя длину слова.
*/
private String replaceSwear(String message) {
String[] words = message.split("(?<=\\s)|(?=\\s)"); // разбиваем с сохранением пробелов
StringBuilder result = new StringBuilder();
for (String word : words) {
String check = ignoreCase ? word.toLowerCase() : word;
String cleanCheck = check.replaceAll("^[^а-яёa-z]+|[^а-яёa-z]+$", "");
boolean isBad = badWords.contains(cleanCheck);
if (!isBad && checkFragments) {
for (String frag : badFragments) {
if (check.contains(frag)) { isBad = true; break; }
}
}
if (isBad) {
result.append(String.valueOf(replacementChar).repeat(word.length()));
} else {
result.append(word);
}
}
return result.toString();
}
// ──────────────────────────────────────────────────────────────────────────
// Anti-flood
// ──────────────────────────────────────────────────────────────────────────
private FilterResult filterFlood(Player player) {
int maxMessages = config.getInt("filters.flood.max-messages", 5);
int timePeriod = config.getInt("filters.flood.time-period", 10); // секунды
long now = System.currentTimeMillis();
long windowMs = timePeriod * 1000L;
Deque<Long> timestamps = floodTracker.computeIfAbsent(
player.getUniqueId(), k -> new ArrayDeque<>());
// Удаляем старые timestamp'ы за пределами окна
while (!timestamps.isEmpty() && now - timestamps.peekFirst() > windowMs) {
timestamps.pollFirst();
}
if (timestamps.size() >= maxMessages) {
return blocked(config.getString("filters.flood.block-message",
"&#CF6679Не флудите!"));
}
timestamps.addLast(now);
return new FilterResult(true, null, null);
}
// ──────────────────────────────────────────────────────────────────────────
// Anti-spam (похожие сообщения)
// ──────────────────────────────────────────────────────────────────────────
private FilterResult filterSpam(Player player, String message) {
int maxSimilar = config.getInt("filters.spam.max-similar-messages", 3);
int threshold = config.getInt("filters.spam.similarity-threshold", 80);
Deque<String> history = spamTracker.computeIfAbsent(
player.getUniqueId(), k -> new ArrayDeque<>());
// Считаем сколько прошлых сообщений похожи
long similarCount = history.stream()
.filter(prev -> similarity(prev, message) >= threshold)
.count();
if (similarCount >= maxSimilar) {
return blocked(config.getString("filters.spam.block-message",
"&#CF6679Не отправляйте одинаковые сообщения"));
}
// Добавляем в историю (храним последние 10)
history.addLast(message);
if (history.size() > 10) history.pollFirst();
return new FilterResult(true, null, null);
}
/**
* Простая метрика схожести: процент общих символов (0-100).
*/
private int similarity(String a, String b) {
if (a.equals(b)) return 100;
int maxLen = Math.max(a.length(), b.length());
if (maxLen == 0) return 100;
int dist = levenshtein(
a.toLowerCase().substring(0, Math.min(a.length(), 50)),
b.toLowerCase().substring(0, Math.min(b.length(), 50)));
return 100 - (dist * 100 / maxLen);
}
private int levenshtein(String a, String b) {
int[] dp = new int[b.length() + 1];
for (int j = 0; j <= b.length(); j++) dp[j] = j;
for (int i = 1; i <= a.length(); i++) {
int prev = dp[0];
dp[0] = i;
for (int j = 1; j <= b.length(); j++) {
int temp = dp[j];
dp[j] = a.charAt(i - 1) == b.charAt(j - 1)
? prev
: 1 + Math.min(prev, Math.min(dp[j], dp[j - 1]));
prev = temp;
}
}
return dp[b.length()];
}
// ──────────────────────────────────────────────────────────────────────────
// URL / IP / повторы — прежняя логика
// ──────────────────────────────────────────────────────────────────────────
private FilterResult filterUrls(Player player, String message) {
if (player.hasPermission("lochat.bypass.urlfilter")) {
return new FilterResult(true, message, null);
}
if (player.hasPermission("lochat.bypass.urlfilter")) return ok(message);
Matcher matcher = URL_PATTERN.matcher(message);
while (matcher.find()) {
String url = matcher.group();
String domain = extractDomain(url);
String domain = extractDomain(matcher.group());
if (!whitelistedDomains.isEmpty()) {
boolean isWhitelisted = whitelistedDomains.stream()
boolean ok = whitelistedDomains.stream()
.anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase()));
if (!isWhitelisted) {
return new FilterResult(false, message,
config.getString("filters.url.blocked-message", "&#FF6B6BСсылки запрещены!"));
}
if (!ok) return blocked(config.getString("filters.url.blocked-message",
"&#CF6679Ссылки запрещены!"));
}
if (!blacklistedDomains.isEmpty()) {
boolean isBlacklisted = blacklistedDomains.stream()
boolean bad = blacklistedDomains.stream()
.anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase()));
if (isBlacklisted) {
return new FilterResult(false, message,
config.getString("filters.url.blocked-message", "&#FF6B6BЭта ссылка запрещена!"));
}
if (bad) return blocked(config.getString("filters.url.blocked-message",
"&#CF6679Эта ссылка запрещена!"));
}
}
return new FilterResult(true, message, null);
return ok(message);
}
private FilterResult filterIPs(Player player, String message) {
if (player.hasPermission("lochat.bypass.ipfilter")) {
return new FilterResult(true, message, null);
}
if (player.hasPermission("lochat.bypass.ipfilter")) return ok(message);
Matcher matcher = IP_PATTERN.matcher(message);
if (matcher.find()) {
String replacement = config.getString("filters.ip.replacement", "[IP скрыт]");
String filtered = matcher.replaceAll(replacement);
if (config.getBoolean("filters.ip.block", false)) {
return new FilterResult(false, message,
config.getString("filters.ip.blocked-message", "&#FF6B6BIP адреса запрещены!"));
}
return new FilterResult(true, filtered, null);
if (config.getBoolean("filters.ip.block", false))
return blocked(config.getString("filters.ip.blocked-message",
"&#CF6679IP адреса запрещены!"));
return ok(matcher.replaceAll(config.getString("filters.ip.replacement", "[IP скрыт]")));
}
return ok(message);
}
return new FilterResult(true, message, null);
private FilterResult filterHiddenUrls(Player player, String message) {
if (!config.getBoolean("filters.advertising.block-hidden-urls", true)) return ok(message);
if (player.hasPermission("lochat.bypass.hiddenurls")) return ok(message);
if (HIDDEN_URL_PATTERN.matcher(message).find())
return blocked(config.getString("filters.advertising.blocked-message",
"&#CF6679Реклама запрещена!"));
return ok(message);
}
private FilterResult filterDomains(Player player, String message) {
if (!config.getBoolean("filters.advertising.block-domains", true)) return ok(message);
if (player.hasPermission("lochat.bypass.domains")) return ok(message);
Matcher matcher = DOMAIN_PATTERN.matcher(message);
while (matcher.find()) {
String domain = matcher.group();
if (!whitelistedDomains.isEmpty()) {
boolean ok = whitelistedDomains.stream()
.anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase()));
if (!ok) return blocked(config.getString("filters.advertising.blocked-message",
"&#CF6679Реклама запрещена!"));
}
if (!blacklistedDomains.isEmpty()) {
boolean bad = blacklistedDomains.stream()
.anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase()));
if (bad) return blocked(config.getString("filters.advertising.blocked-message",
"&#CF6679Реклама запрещена!"));
}
}
return ok(message);
}
private String filterRepeatingChars(String message) {
int maxRepeats = config.getInt("filters.repeat.max-repeats", 3);
Matcher matcher = REPEAT_PATTERN.matcher(message);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String repeated = matcher.group(1);
String replacement = repeated.repeat(Math.min(maxRepeats, matcher.group().length()));
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
int max = config.getInt("filters.repeat.max-repeats", 3);
Matcher m = REPEAT_PATTERN.matcher(message);
StringBuffer sb = new StringBuffer();
while (m.find()) {
m.appendReplacement(sb, Matcher.quoteReplacement(
m.group(1).repeat(Math.min(max, m.group().length()))));
}
matcher.appendTail(result);
return result.toString();
m.appendTail(sb);
return sb.toString();
}
private String extractDomain(String url) {
url = url.replaceFirst("^https?://", "");
int slashIndex = url.indexOf('/');
if (slashIndex != -1) {
url = url.substring(0, slashIndex);
}
return url;
int slash = url.indexOf('/');
return slash != -1 ? url.substring(0, slash) : url;
}
// ── Утилиты ────────────────────────────────────────────────────────────────
private static FilterResult ok(String msg) { return new FilterResult(true, msg, null); }
private static FilterResult blocked(String reason) { return new FilterResult(false, null, reason); }
/**
* Фильтрует скрытые ссылки (h t t p, w w w и т.д.)
* Очищает трекеры для игрока при выходе (вызывать из PlayerEventListener).
*/
private FilterResult filterHiddenUrls(Player player, String message) {
if (!config.getBoolean("filters.advertising.block-hidden-urls", true)) {
return new FilterResult(true, message, null);
}
if (player.hasPermission("lochat.bypass.hiddenurls")) {
return new FilterResult(true, message, null);
}
Matcher matcher = HIDDEN_URL_PATTERN.matcher(message);
if (matcher.find()) {
return new FilterResult(false, message,
config.getString("filters.advertising.blocked-message", "&#FF6B6BРеклама запрещена!"));
}
return new FilterResult(true, message, null);
public void clearPlayer(java.util.UUID uuid) {
floodTracker.remove(uuid);
spamTracker.remove(uuid);
}
/**
* Фильтрует домены без http (example.com, discord.gg и т.д.)
*/
private FilterResult filterDomains(Player player, String message) {
if (!config.getBoolean("filters.advertising.block-domains", true)) {
return new FilterResult(true, message, null);
}
if (player.hasPermission("lochat.bypass.domains")) {
return new FilterResult(true, message, null);
}
Matcher matcher = DOMAIN_PATTERN.matcher(message);
while (matcher.find()) {
String domain = matcher.group();
// Проверяем whitelist
if (!whitelistedDomains.isEmpty()) {
boolean isWhitelisted = whitelistedDomains.stream()
.anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase()));
if (!isWhitelisted) {
return new FilterResult(false, message,
config.getString("filters.advertising.blocked-message", "&#FF6B6BРеклама запрещена!"));
}
}
// Проверяем blacklist
if (!blacklistedDomains.isEmpty()) {
boolean isBlacklisted = blacklistedDomains.stream()
.anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase()));
if (isBlacklisted) {
return new FilterResult(false, message,
config.getString("filters.advertising.blocked-message", "&#FF6B6BРеклама запрещена!"));
}
}
}
return new FilterResult(true, message, null);
}
public record FilterResult(boolean allowed, String filteredMessage, String blockReason) {
}
public record FilterResult(boolean allowed, String filteredMessage, String blockReason) {}
}

View file

@ -13,10 +13,12 @@ import org.bukkit.plugin.java.JavaPlugin;
public class CooldownFilter implements MessageFilter {
private final CooldownService cooldownService;
private final FileConfiguration config;
private final JavaPlugin plugin;
public CooldownFilter(CooldownService cooldownService, JavaPlugin plugin) {
this.cooldownService = cooldownService;
this.config = plugin.getConfig();
this.plugin = plugin;
}
@Override
@ -32,7 +34,13 @@ public class CooldownFilter implements MessageFilter {
if (cooldownService.isOnCooldown(player.getUniqueId(), chatType, cooldown)) {
int remaining = cooldownService.getRemainingCooldown(
player.getUniqueId(), chatType, cooldown);
player.sendMessage("§cПодождите " + remaining + " сек.");
// Получаем сообщение из конфигурации
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
String cooldownMessage = loChat.getConfigManager().getHardcodedMessages().getCooldownMessage();
cooldownMessage = cooldownMessage.replace("{remaining}", String.valueOf(remaining));
player.sendMessage(cooldownMessage);
return false;
}
}

View file

@ -2,17 +2,24 @@ package com.loki.lochat.core.filter;
import com.loki.lochat.api.filter.MessageFilter;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.config.HardcodedMessages;
import com.loki.lochat.data.model.ChatMessage;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
/**
* Фильтр проверки мута
*/
public class MuteFilter implements MessageFilter {
private final MuteService muteService;
private final HardcodedMessages hardcodedMessages;
public MuteFilter(MuteService muteService) {
public MuteFilter(MuteService muteService, JavaPlugin plugin) {
this.muteService = muteService;
// Получаем HardcodedMessages через LoChat
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
this.hardcodedMessages = loChat.getConfigManager().getHardcodedMessages();
}
@Override
@ -26,7 +33,21 @@ public class MuteFilter implements MessageFilter {
private void sendMuteNotification(Player player) {
long remaining = muteService.getRemainingTime(player.getUniqueId());
String timeStr = muteService.formatTime(remaining);
player.sendMessage("§cВы замучены! Осталось: " + timeStr);
com.loki.lochat.data.model.MuteData muteData = muteService.getMuteData(player.getUniqueId());
String reason = muteData != null && muteData.getReason() != null ? muteData.getReason() : "";
String msg;
if (remaining < 0) {
// Permanent mute
msg = hardcodedMessages.getMutedPermanent()
.replace("{reason}", reason);
} else {
String timeStr = muteService.formatTime(remaining);
msg = hardcodedMessages.getMutedMessage()
.replace("{reason}", reason)
.replace("{time}", timeStr);
}
player.sendMessage(ChatFormatter.parse(msg));
}
}

View file

@ -26,9 +26,15 @@ public class ServiceRegistry {
private void registerServicesWithDeps(ConfigManager configManager, MessageConfig messageConfig) {
register(ChatService.class, new ChatServiceImpl(plugin, this));
register(MuteService.class, new MuteServiceImpl(plugin));
register(CooldownService.class, new CooldownServiceImpl());
// CooldownService регистрируем раньше PlayerDataService, чтобы передать его как зависимость
CooldownServiceImpl cooldownService = new CooldownServiceImpl();
register(CooldownService.class, cooldownService);
register(MessageService.class, new MessageServiceImpl(plugin, this));
register(PlayerDataService.class, new PlayerDataServiceImpl(plugin));
// Передаём тот же экземпляр CooldownService, а не новый — иначе очистка при выходе не работает
register(PlayerDataService.class, new PlayerDataServiceImpl(plugin, cooldownService));
register(PMService.class, new PMServiceImpl());
register(IgnoreService.class, new IgnoreServiceImpl(plugin));

View file

@ -4,40 +4,47 @@ import com.loki.lochat.api.service.ChatService;
import com.loki.lochat.core.registry.ServiceRegistry;
import com.loki.lochat.renderer.EnhancedChatRenderer;
import com.loki.lochat.util.DistanceUtil;
import com.loki.lochat.util.FoliaUtil;
import com.loki.lochat.utils.ChatFormatter;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Реализация сервиса чата
* Реализация сервиса чата.
* globalChatDisabled персистируется в data/players.yml.
*/
public class ChatServiceImpl implements ChatService {
private final JavaPlugin plugin;
private final Set<UUID> globalChatDisabled = ConcurrentHashMap.newKeySet();
private final File playersFile;
public ChatServiceImpl(JavaPlugin plugin, ServiceRegistry registry) {
this.plugin = plugin;
this.playersFile = new File(plugin.getDataFolder(), "data/players.yml");
loadDisabled();
}
// ──────────────────────────────────────────────────────────────────────────
// Chat
// ──────────────────────────────────────────────────────────────────────────
@Override
public void sendGlobalMessage(Player sender, Object message) {
Component messageComponent = (message instanceof Component)
? (Component) message
: ChatFormatter.parse(message.toString());
// Используем тот же рендерер что и в ChatEventListener для единообразия
EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, sender, messageComponent, true);
for (Player player : Bukkit.getOnlinePlayers()) {
if (!isGlobalChatDisabled(player.getUniqueId())) {
Component renderedMessage = renderer.render(sender, sender.displayName(), messageComponent, player);
player.sendMessage(renderedMessage);
Component msg = toComponent(message);
EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, sender, msg, true);
for (Player p : Bukkit.getOnlinePlayers()) {
if (!isGlobalChatDisabled(p.getUniqueId())) {
p.sendMessage(renderer.render(sender, sender.displayName(), msg, p));
}
}
}
@ -45,42 +52,81 @@ public class ChatServiceImpl implements ChatService {
@Override
public void sendLocalMessage(Player sender, Object message) {
int radius = plugin.getConfig().getInt("chat.local.radius", 100);
Component messageComponent = (message instanceof Component)
? (Component) message
: ChatFormatter.parse(message.toString());
Component msg = toComponent(message);
EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, sender, msg, false);
// Используем тот же рендерер что и в ChatEventListener для единообразия
EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, sender, messageComponent, false);
int recipientCount = 0;
for (Player player : Bukkit.getOnlinePlayers()) {
if (DistanceUtil.isInRange(sender, player, radius)) {
Component renderedMessage = renderer.render(sender, sender.displayName(), messageComponent, player);
player.sendMessage(renderedMessage);
recipientCount++;
int count = 0;
for (Player p : Bukkit.getOnlinePlayers()) {
if (DistanceUtil.isInRange(sender, p, radius)) {
p.sendMessage(renderer.render(sender, sender.displayName(), msg, p));
count++;
}
}
// Если никто не услышал (только отправитель в радиусе)
if (recipientCount <= 1) {
sender.sendMessage(ChatFormatter.parse(plugin.getConfig().getString("messages.local.nobody-heard", "<#FFA726>Вас никто не услышал - рядом нет игроков")));
if (count <= 1) {
sender.sendMessage(ChatFormatter.parse(
plugin.getConfig().getString("messages.local.nobody-heard",
"<color:#9878C9>Вас никто не услышал — рядом нет игроков</color>")));
}
}
// ──────────────────────────────────────────────────────────────────────────
// Toggle global chat — с персистенцией
// ──────────────────────────────────────────────────────────────────────────
@Override
public boolean toggleGlobalChat(UUID player) {
boolean nowEnabled;
if (globalChatDisabled.contains(player)) {
globalChatDisabled.remove(player);
return true;
nowEnabled = true;
} else {
globalChatDisabled.add(player);
return false;
nowEnabled = false;
}
FoliaUtil.runAsync(plugin, this::saveDisabled);
return nowEnabled;
}
@Override
public boolean isGlobalChatDisabled(UUID player) {
return globalChatDisabled.contains(player);
}
// ──────────────────────────────────────────────────────────────────────────
// Персистенция
// ──────────────────────────────────────────────────────────────────────────
private void loadDisabled() {
if (!playersFile.exists()) return;
YamlConfiguration cfg = YamlConfiguration.loadConfiguration(playersFile);
for (String uuidStr : cfg.getStringList("global-chat-disabled")) {
try {
globalChatDisabled.add(UUID.fromString(uuidStr));
} catch (IllegalArgumentException ignored) {}
}
}
private void saveDisabled() {
try {
ensureDir();
YamlConfiguration cfg = playersFile.exists()
? YamlConfiguration.loadConfiguration(playersFile)
: new YamlConfiguration();
cfg.set("global-chat-disabled",
globalChatDisabled.stream().map(UUID::toString).toList());
cfg.save(playersFile);
} catch (IOException e) {
plugin.getLogger().warning("[LoChat] Failed to save players.yml: " + e.getMessage());
}
}
private void ensureDir() {
File dir = playersFile.getParentFile();
if (!dir.exists()) dir.mkdirs();
}
private Component toComponent(Object message) {
return message instanceof Component c ? c : ChatFormatter.parse(message.toString());
}
}

View file

@ -29,24 +29,21 @@ public class MessageServiceImpl implements MessageService {
this.filters = new ArrayList<>();
// Регистрируем фильтры в порядке применения
filters.add(new MuteFilter(registry.get(MuteService.class)));
// MuteFilter теперь получает plugin для доступа к HardcodedMessages
filters.add(new MuteFilter(registry.get(MuteService.class), plugin));
filters.add(new CooldownFilter(registry.get(CooldownService.class), plugin));
}
@Override
public boolean processMessage(Player player, String rawMessage) {
// Создаем объект сообщения
ChatMessage message = ChatMessage.create(player, rawMessage);
// Применяем фильтры (мут, кулдаун)
for (MessageFilter filter : filters) {
if (!filter.apply(player, message)) {
return false; // Сообщение заблокировано
return false;
}
}
// Возвращаем true - сообщение разрешено
// Рендерер сам отправит его
return true;
}
}

View file

@ -73,22 +73,21 @@ public class MuteServiceImpl implements MuteService {
@Override
public boolean isMuted(UUID uuid) {
if (uuid == null) {
return false;
}
MuteData data = mutes.get(uuid);
if (data == null) {
return false;
}
if (uuid == null) return false;
if (isExpired(data)) {
mutes.remove(uuid);
MuteData data = mutes.get(uuid);
if (data == null) return false;
if (!isExpired(data)) return true;
// Атомарно удаляем только если значение не изменилось между get() и remove().
// ConcurrentHashMap.remove(key, value) удаляет запись лишь когда она точно та же.
boolean removed = mutes.remove(uuid, data);
if (removed) {
saveAsync();
plugin.getLogger().info("Mute expired for player " + data.getPlayerName());
return false;
}
return true;
return false;
}
@Override

View file

@ -2,27 +2,118 @@ package com.loki.lochat.core.service;
import com.loki.lochat.api.service.CooldownService;
import com.loki.lochat.api.service.PlayerDataService;
import com.loki.lochat.util.FoliaUtil;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* Реализация сервиса данных игроков
* Реализация сервиса данных игроков.
* Хранит и сохраняет статистику сообщений в statistics.yml.
*/
public class PlayerDataServiceImpl implements PlayerDataService {
private final JavaPlugin plugin;
private final CooldownService cooldownService;
public PlayerDataServiceImpl(JavaPlugin plugin) {
this.cooldownService = new CooldownServiceImpl();
// Счётчики сообщений per-player (сбрасываются при выходе, сохраняются в файл)
private final Map<UUID, Long> playerMessages = new ConcurrentHashMap<>();
// Глобальные счётчики по типу чата
private final AtomicLong globalChatCount = new AtomicLong(0);
private final AtomicLong localChatCount = new AtomicLong(0);
private final AtomicLong pmCount = new AtomicLong(0);
private final AtomicLong totalMessages = new AtomicLong(0);
private final File statsFile;
private YamlConfiguration stats;
public PlayerDataServiceImpl(JavaPlugin plugin, CooldownService cooldownService) {
this.plugin = plugin;
this.cooldownService = cooldownService;
this.statsFile = new File(plugin.getDataFolder(), "data/statistics.yml");
load();
}
// ── Публичные методы статистики ────────────────────────────────────────────
/** Вызывать при каждом сообщении в чате */
public void recordMessage(UUID player, String chatType) {
playerMessages.merge(player, 1L, Long::sum);
totalMessages.incrementAndGet();
switch (chatType) {
case "global" -> globalChatCount.incrementAndGet();
case "local" -> localChatCount.incrementAndGet();
case "pm" -> pmCount.incrementAndGet();
}
}
public long getPlayerMessages(UUID player) {
return playerMessages.getOrDefault(player, 0L);
}
// ── PlayerDataService ──────────────────────────────────────────────────────
@Override
public void clearPlayerData(UUID player) {
cooldownService.removeCooldown(player);
// Сохраняем статистику игрока в файл при выходе
long msgs = playerMessages.getOrDefault(player, 0L);
if (msgs > 0) {
FoliaUtil.runAsync(plugin, () -> persistPlayerMessages(player, msgs));
}
}
@Override
public void saveAll() {
// Сохранение всех данных при выключении
try {
ensureDir();
// Обновляем глобальные счётчики
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());
stats.save(statsFile);
} catch (IOException e) {
plugin.getLogger().warning("[LoChat] Failed to save statistics: " + e.getMessage());
}
}
// ── Внутренние методы ──────────────────────────────────────────────────────
private void persistPlayerMessages(UUID uuid, long count) {
try {
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());
stats.save(statsFile);
playerMessages.remove(uuid);
} catch (IOException e) {
plugin.getLogger().warning("[LoChat] Failed to persist player stats: " + e.getMessage());
}
}
private void load() {
ensureDir();
if (!statsFile.exists()) {
plugin.saveResource("data/statistics.yml", false);
}
stats = YamlConfiguration.loadConfiguration(statsFile);
}
private void ensureDir() {
File dir = statsFile.getParentFile();
if (!dir.exists()) dir.mkdirs();
}
}

View file

@ -33,6 +33,7 @@ public class GradientModule {
public GradientModule(JavaPlugin plugin) {
this.plugin = plugin;
this.enabled = false;
this.luckPermsHook = null; // Явно инициализируем как null
}
/**
@ -155,7 +156,7 @@ public class GradientModule {
// LuckPerms префикс
String lpPrefix = null;
if (luckPermsHook.isEnabled()) {
if (luckPermsHook != null && luckPermsHook.isEnabled()) {
lpPrefix = luckPermsHook.getActivePrefix(player);
}
@ -241,7 +242,7 @@ public class GradientModule {
}
// LuckPerms префикс
if (luckPermsHook.isEnabled()) {
if (luckPermsHook != null && luckPermsHook.isEnabled()) {
String lpPrefix = luckPermsHook.getActivePrefix(player);
if (lpPrefix != null && !lpPrefix.isEmpty()) {
if (data.hasColors() && data.isColorEnabled() && config.isGradientOnLuckPermsPrefix()) {
@ -259,7 +260,7 @@ public class GradientModule {
* Получает только LuckPerms префикс
*/
public String getLuckPermsPrefix(Player player) {
if (!enabled || !luckPermsHook.isEnabled()) return "";
if (!enabled || luckPermsHook == null || !luckPermsHook.isEnabled()) return "";
String prefix = luckPermsHook.getActivePrefix(player);
return prefix != null ? prefix : "";
}

View file

@ -52,18 +52,18 @@ public class DiscordIntegration {
private void initWebhook() {
if (!enabled) return;
// Получаем конфигурацию внешнего вида для дефолтных значений
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String webhookUrl = config.getString("webhook.url", "");
String username = config.getString("webhook.username", "Minecraft Server");
String avatarUrl = config.getString("webhook.avatar_url", "https://mc-heads.net/avatar/minecraft/64");
String avatarUrl = config.getString("webhook.avatar_url", appearanceConfig.getDefaultAvatarUrl());
int timeout = config.getInt("performance.timeout", 10);
int retryAttempts = config.getInt("performance.retry_attempts", 3);
long retryDelay = config.getLong("performance.retry_delay", 1000);
// Логируем информацию о URL для отладки
plugin.getLogger().info("Discord webhook URL length: " + webhookUrl.length());
if (webhookUrl.length() > 0) {
plugin.getLogger().info("Discord webhook URL starts with: " + webhookUrl.substring(0, Math.min(50, webhookUrl.length())));
}
// Логируем только факт наличия URL, но не его содержимое (security)
webhook = new DiscordWebhook(plugin, webhookUrl, username, avatarUrl, timeout, retryAttempts, retryDelay);
@ -118,9 +118,21 @@ public class DiscordIntegration {
*/
private void sendChatMessageInternal(String message, String playerName, boolean isGlobal) {
if (config.getBoolean("chat.use_embed", true)) {
String title = isGlobal ? "🌍 Глобальный чат" : "📍 Локальный чат";
String color = config.getString("chat.embed_color", "5865F2");
String thumbnail = "https://mc-heads.net/avatar/" + playerName + "/64";
// Получаем заголовки из конфигурации внешнего вида
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String title = isGlobal ?
appearanceConfig.getDiscordEventTitle("global_chat") :
appearanceConfig.getDiscordEventTitle("local_chat");
// Если заголовок не найден в новой конфигурации, используем старый способ
if (title.isEmpty()) {
title = isGlobal ? "🌍 Глобальный чат" : "📍 Локальный чат";
}
String color = config.getString("chat.embed_color", appearanceConfig.getDefaultEmbedColor());
String thumbnail = appearanceConfig.getPlayerAvatarUrl().replace("{player}", playerName);
webhook.sendEmbed(title, message, color, thumbnail);
} else {
@ -135,12 +147,16 @@ public class DiscordIntegration {
if (!enabled || !config.getBoolean("events.enabled", true) ||
!config.getBoolean("events.join.enabled", true)) return;
// Получаем конфигурацию внешнего вида
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String playerName = player.getName();
String title = config.getString("events.join.title", "🟢 Игрок зашел на сервер");
String title = config.getString("events.join.title", appearanceConfig.getDiscordEventTitle("join"));
String description = config.getString("events.join.description", "**{player}** присоединился к серверу")
.replace("{player}", playerName);
String color = config.getString("events.join.color", "00FF00");
String thumbnail = config.getString("events.join.thumbnail", "https://mc-heads.net/avatar/{player}/64")
String thumbnail = config.getString("events.join.thumbnail", appearanceConfig.getPlayerAvatarUrl())
.replace("{player}", playerName);
if (config.getBoolean("performance.async", true)) {
@ -157,12 +173,16 @@ public class DiscordIntegration {
if (!enabled || !config.getBoolean("events.enabled", true) ||
!config.getBoolean("events.quit.enabled", true)) return;
// Получаем конфигурацию внешнего вида
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String playerName = player.getName();
String title = config.getString("events.quit.title", "🔴 Игрок покинул сервер");
String title = config.getString("events.quit.title", appearanceConfig.getDiscordEventTitle("quit"));
String description = config.getString("events.quit.description", "**{player}** покинул сервер")
.replace("{player}", playerName);
String color = config.getString("events.quit.color", "FF0000");
String thumbnail = config.getString("events.quit.thumbnail", "https://mc-heads.net/avatar/{player}/64")
String thumbnail = config.getString("events.quit.thumbnail", appearanceConfig.getPlayerAvatarUrl())
.replace("{player}", playerName);
if (config.getBoolean("performance.async", true)) {
@ -179,13 +199,17 @@ public class DiscordIntegration {
if (!enabled || !config.getBoolean("events.enabled", true) ||
!config.getBoolean("events.death.enabled", true)) return;
// Получаем конфигурацию внешнего вида
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String playerName = player.getName();
String title = config.getString("events.death.title", "💀 Смерть игрока");
String title = config.getString("events.death.title", appearanceConfig.getDiscordEventTitle("death"));
String description = config.getString("events.death.description", "**{player}** {death_message}")
.replace("{player}", playerName)
.replace("{death_message}", sanitizeMessage(deathMessage));
String color = config.getString("events.death.color", "800080");
String thumbnail = config.getString("events.death.thumbnail", "https://mc-heads.net/avatar/{player}/64")
String thumbnail = config.getString("events.death.thumbnail", appearanceConfig.getPlayerAvatarUrl())
.replace("{player}", playerName);
if (config.getBoolean("performance.async", true)) {
@ -241,6 +265,13 @@ public class DiscordIntegration {
webhook.sendEmbed(title, description, color, thumbnail);
}
/**
* Остановить executor при выключении плагина
*/
public void shutdown() {
if (webhook != null) webhook.shutdown();
}
/**
* Проверить включена ли интеграция
*/

View file

@ -2,7 +2,6 @@ package com.loki.lochat.integrations;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.loki.lochat.util.FoliaUtil;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.IOException;
@ -12,11 +11,16 @@ import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Класс для отправки сообщений в Discord через вебхуки
* Отправка сообщений в Discord через вебхуки.
* Все запросы идут в выделенный daemon-поток LoChat-Discord,
* а не в ForkJoinPool.commonPool().
*/
public class DiscordWebhook {
private final JavaPlugin plugin;
private final String webhookUrl;
private final String username;
@ -25,188 +29,127 @@ public class DiscordWebhook {
private final int retryAttempts;
private final long retryDelay;
public DiscordWebhook(JavaPlugin plugin, String webhookUrl, String username, String avatarUrl,
int timeout, int retryAttempts, long retryDelay) {
// Выделенный пул — не засоряем ForkJoinPool.commonPool()
private final ExecutorService executor = Executors.newSingleThreadExecutor(
r -> {
Thread t = new Thread(r, "LoChat-Discord");
t.setDaemon(true);
return t;
}
);
public DiscordWebhook(JavaPlugin plugin, String webhookUrl, String username, String avatarUrl,
int timeout, int retryAttempts, long retryDelay) {
this.plugin = plugin;
this.webhookUrl = webhookUrl != null ? webhookUrl.trim() : null; // Убираем пробелы
this.webhookUrl = webhookUrl != null ? webhookUrl.trim() : null;
this.username = username;
this.avatarUrl = avatarUrl;
this.timeout = timeout * 1000; // конвертируем в миллисекунды
this.timeout = timeout * 1000;
this.retryAttempts = retryAttempts;
this.retryDelay = retryDelay;
}
/**
* Отправить простое текстовое сообщение
*/
// ── Публичные методы отправки ─────────────────────────────────────────────
public CompletableFuture<Boolean> sendMessage(String content) {
JsonObject json = new JsonObject();
json.addProperty("content", content);
json.addProperty("username", username);
json.addProperty("avatar_url", avatarUrl);
return sendWebhook(json);
}
/**
* Отправить embed сообщение
*/
public CompletableFuture<Boolean> sendEmbed(String title, String description, String color, String thumbnailUrl) {
JsonObject embed = new JsonObject();
if (title != null) embed.addProperty("title", title);
if (description != null) embed.addProperty("description", description);
if (color != null) embed.addProperty("color", Integer.parseInt(color, 16));
if (thumbnailUrl != null) {
JsonObject thumbnail = new JsonObject();
thumbnail.addProperty("url", thumbnailUrl);
embed.add("thumbnail", thumbnail);
}
// Добавляем timestamp
embed.addProperty("timestamp", java.time.Instant.now().toString());
JsonArray embeds = new JsonArray();
embeds.add(embed);
JsonObject json = new JsonObject();
json.add("embeds", embeds);
json.addProperty("username", username);
json.addProperty("avatar_url", avatarUrl);
return sendWebhook(json);
return sendWebhook(buildEmbed(title, description, color, thumbnailUrl, null));
}
/**
* Отправить embed с кастомными полями
*/
public CompletableFuture<Boolean> sendEmbedWithFields(String title, String description, String color,
String thumbnailUrl, JsonArray fields) {
public CompletableFuture<Boolean> sendEmbedWithFields(String title, String description, String color,
String thumbnailUrl, JsonArray fields) {
return sendWebhook(buildEmbed(title, description, color, thumbnailUrl, fields));
}
/** Остановить executor при выключении плагина */
public void shutdown() {
executor.shutdown();
}
// ── Внутренние методы ─────────────────────────────────────────────────────
private JsonObject buildEmbed(String title, String description, String color,
String thumbnailUrl, JsonArray fields) {
JsonObject embed = new JsonObject();
if (title != null) embed.addProperty("title", title);
if (description != null) embed.addProperty("description", description);
if (color != null) embed.addProperty("color", Integer.parseInt(color, 16));
if (fields != null) embed.add("fields", fields);
if (thumbnailUrl != null) {
JsonObject thumbnail = new JsonObject();
thumbnail.addProperty("url", thumbnailUrl);
embed.add("thumbnail", thumbnail);
JsonObject thumb = new JsonObject();
thumb.addProperty("url", thumbnailUrl);
embed.add("thumbnail", thumb);
}
embed.addProperty("timestamp", java.time.Instant.now().toString());
JsonArray embeds = new JsonArray();
embeds.add(embed);
JsonObject json = new JsonObject();
json.add("embeds", embeds);
json.addProperty("username", username);
json.addProperty("avatar_url", avatarUrl);
return sendWebhook(json);
return json;
}
/**
* Отправить JSON в вебхук с повторными попытками
* Отправляет JSON в вебхук асинхронно через выделенный executor.
*/
private CompletableFuture<Boolean> sendWebhook(JsonObject json) {
return CompletableFuture.supplyAsync(() -> {
for (int attempt = 1; attempt <= retryAttempts; attempt++) {
try {
boolean success = sendWebhookSync(json.toString());
if (success) {
return true;
}
if (sendWebhookSync(json.toString())) return true;
} catch (Exception e) {
plugin.getLogger().warning("Discord webhook attempt " + attempt + " failed: " + e.getMessage());
}
// Ждем перед следующей попыткой (кроме последней)
if (attempt < retryAttempts) {
try {
Thread.sleep(retryDelay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
try { Thread.sleep(retryDelay); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); break; }
}
}
plugin.getLogger().severe("Failed to send Discord webhook after " + retryAttempts + " attempts");
return false;
});
}, executor); // ← передаём свой executor, не commonPool
}
/**
* Синхронная отправка в вебхук
*/
private boolean sendWebhookSync(String jsonPayload) throws IOException {
String cleanUrl = webhookUrl.trim();
URL url = URI.create(cleanUrl).toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
URL url = URI.create(webhookUrl.trim()).toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
try {
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("User-Agent", "LoChat-Discord-Integration/1.0");
connection.setDoOutput(true);
connection.setConnectTimeout(timeout);
connection.setReadTimeout(timeout);
// Отправляем JSON
try (OutputStream os = connection.getOutputStream()) {
byte[] input = jsonPayload.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("User-Agent", "LoChat-Discord-Integration/1.0");
conn.setDoOutput(true);
conn.setConnectTimeout(timeout);
conn.setReadTimeout(timeout);
try (OutputStream os = conn.getOutputStream()) {
os.write(jsonPayload.getBytes(StandardCharsets.UTF_8));
}
int responseCode = connection.getResponseCode();
if (responseCode == 204) {
return true; // Успешная отправка
} else if (responseCode == 429) {
plugin.getLogger().warning("Discord webhook rate limited (429)");
return false;
} else {
plugin.getLogger().warning("Discord webhook returned code: " + responseCode);
return false;
}
int code = conn.getResponseCode();
if (code == 204) return true;
if (code == 429) { plugin.getLogger().warning("Discord webhook rate limited (429)"); return false; }
plugin.getLogger().warning("Discord webhook returned code: " + code);
return false;
} finally {
connection.disconnect();
conn.disconnect();
}
}
/**
* Проверить валидность вебхука
*/
public boolean isValid() {
if (webhookUrl == null || webhookUrl.trim().isEmpty()) {
plugin.getLogger().warning("Discord webhook URL is null or empty");
return false;
}
String cleanUrl = webhookUrl.trim();
if (!cleanUrl.startsWith("https://discord.com/api/webhooks/") &&
!cleanUrl.startsWith("https://discordapp.com/api/webhooks/")) {
plugin.getLogger().warning("Discord webhook URL has invalid format: " + cleanUrl);
return false;
}
if (cleanUrl.length() < 50) {
plugin.getLogger().warning("Discord webhook URL is too short: " + cleanUrl);
return false;
}
// Проверяем на недопустимые символы
try {
URI.create(cleanUrl);
} catch (IllegalArgumentException e) {
plugin.getLogger().warning("Discord webhook URL contains illegal characters: " + e.getMessage());
return false;
}
if (webhookUrl == null || webhookUrl.trim().isEmpty()) return false;
String url = webhookUrl.trim();
if (!url.startsWith("https://discord.com/api/webhooks/") &&
!url.startsWith("https://discordapp.com/api/webhooks/")) return false;
if (url.length() < 50) return false;
try { URI.create(url); } catch (IllegalArgumentException e) { return false; }
return true;
}
}
}

View file

@ -5,7 +5,9 @@ import com.loki.lochat.api.service.MessageService;
import com.loki.lochat.core.filter.AdvancedMessageFilter;
import com.loki.lochat.core.registry.ServiceRegistry;
import com.loki.lochat.renderer.EnhancedChatRenderer;
import com.loki.lochat.utils.ChatFormatter;
import io.papermc.paper.event.player.AsyncChatEvent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@ -18,70 +20,88 @@ public class ChatEventListener implements Listener {
private final MessageService messageService;
private final AdvancedMessageFilter advancedFilter;
public ChatEventListener(JavaPlugin plugin, ServiceRegistry registry) {
public ChatEventListener(JavaPlugin plugin, ServiceRegistry registry, com.loki.lochat.core.filter.AdvancedMessageFilter filter) {
this.plugin = plugin;
this.messageService = registry.get(MessageService.class);
this.advancedFilter = new AdvancedMessageFilter(plugin.getConfig());
this.advancedFilter = filter;
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onChat(AsyncChatEvent event) {
Player sender = event.getPlayer();
// plain нужен только для фильтров — цвета игрока обрабатываем ниже отдельно
String rawMessage = PlainTextComponentSerializer.plainText().serialize(event.message());
// Определяем тип чата и очищаем сообщение от !
boolean isGlobal = rawMessage.startsWith("!");
String message = isGlobal ? rawMessage.substring(1).stripLeading() : rawMessage;
String plainMessage = isGlobal ? rawMessage.substring(1).stripLeading() : rawMessage;
boolean allowed = messageService.processMessage(sender, message);
if (!allowed) {
// Мут, кулдаун
if (!messageService.processMessage(sender, plainMessage)) {
event.setCancelled(true);
return;
}
AdvancedMessageFilter.FilterResult filterResult = advancedFilter.filterMessage(sender, message);
// Капс, реклама, повторы
AdvancedMessageFilter.FilterResult filterResult = advancedFilter.filterMessage(sender, plainMessage);
if (!filterResult.allowed()) {
event.setCancelled(true);
sender.sendMessage(com.loki.lochat.utils.ChatFormatter.parse(filterResult.blockReason()));
sender.sendMessage(ChatFormatter.parse(filterResult.blockReason()));
return;
}
String filteredMessage = filterResult.filteredMessage();
// Убеждаемся что ! удален из отображаемого сообщения
if (isGlobal && filteredMessage.startsWith("!")) {
filteredMessage = filteredMessage.substring(1).stripLeading();
}
event.message(com.loki.lochat.utils.ChatFormatter.parse(filteredMessage));
// Фильтруем получателей для локального чата
// ── Фиксируем цвет текста ──────────────────────────────────────────────
// Проблема: Paper наследует цвет от предыдущего компонента если явный не задан.
// Синий цвет в LOCAL — это цвет последней буквы градиента "LOCAL" (#4169E1),
// который просачивается в текст сообщения.
// Решение: всегда явно задаём цвет сообщения.
// • lochat.chat.colors → парсим &7 / &#RRGGBB / MiniMessage из самого текста
// • без права → берём message-color из конфига
LoChat loChat = (LoChat) plugin;
Component messageComponent;
if (sender.hasPermission("lochat.chat.colors")) {
messageComponent = ChatFormatter.parse(filteredMessage);
} else {
String defaultColor = isGlobal
? loChat.getConfigManager().getAppearanceConfig().getGlobalMessageColor()
: loChat.getConfigManager().getAppearanceConfig().getLocalMessageColor();
String hex = defaultColor.startsWith("#") ? defaultColor : "#" + defaultColor;
messageComponent = ChatFormatter.parse("<color:" + hex + ">" + filteredMessage + "</color>");
}
event.message(messageComponent);
// Радиус для локального чата
if (!isGlobal) {
int radius = plugin.getConfig().getInt("chat.local.radius", 100);
event.viewers().removeIf(viewer ->
viewer instanceof Player p && !com.loki.lochat.util.DistanceUtil.isInRange(sender, p, radius)
int radius = loChat.getConfigManager().getAppearanceConfig().getLocalRadius();
event.viewers().removeIf(v ->
v instanceof Player p && !com.loki.lochat.util.DistanceUtil.isInRange(sender, p, radius)
);
// Проверяем, есть ли получатели (кроме отправителя)
long recipientCount = event.viewers().stream()
.filter(viewer -> viewer instanceof Player p && !p.equals(sender))
.count();
if (recipientCount == 0) {
// Отправляем уведомление отправителю после обработки события (Folia-совместимо)
sender.getScheduler().run(plugin, (task) ->
sender.sendMessage(com.loki.lochat.utils.ChatFormatter.parse(plugin.getConfig().getString("messages.local.nobody-heard", "<#FFA726>Вас никто не услышал - рядом нет игроков"))), null
long recipients = event.viewers().stream()
.filter(v -> v instanceof Player p && !p.equals(sender))
.count();
if (recipients == 0) {
String msg = loChat.getConfigManager().getHardcodedMessages().getNobodyHeard();
sender.getScheduler().run(plugin, t ->
sender.sendMessage(ChatFormatter.parse(msg)), null
);
}
}
event.renderer(new EnhancedChatRenderer(plugin, sender, event.message(), isGlobal));
// Отправляем сообщение в Discord
if (plugin instanceof LoChat loChat) {
loChat.getDiscordIntegration().sendChatMessage(sender, filteredMessage, isGlobal);
event.renderer(new EnhancedChatRenderer(plugin, sender, messageComponent, isGlobal));
loChat.getDiscordIntegration().sendChatMessage(sender, filteredMessage, isGlobal);
// Статистика
com.loki.lochat.api.service.PlayerDataService pds =
loChat.getServiceRegistry().get(com.loki.lochat.api.service.PlayerDataService.class);
if (pds instanceof com.loki.lochat.core.service.PlayerDataServiceImpl impl) {
impl.recordMessage(sender.getUniqueId(), isGlobal ? "global" : "local");
}
}
}
}

View file

@ -2,6 +2,9 @@ package com.loki.lochat.listener;
import com.loki.lochat.api.service.NickService;
import com.loki.lochat.api.service.PlayerDataService;
import com.loki.lochat.api.service.PMService;
import com.loki.lochat.api.service.SpyService;
import com.loki.lochat.core.filter.AdvancedMessageFilter;
import com.loki.lochat.core.registry.ServiceRegistry;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@ -15,16 +18,21 @@ import org.bukkit.event.player.PlayerQuitEvent;
public class PlayerEventListener implements Listener {
private final PlayerDataService playerDataService;
private final NickService nickService;
private final PMService pmService;
private final SpyService spyService;
private final AdvancedMessageFilter advancedFilter;
public PlayerEventListener(ServiceRegistry registry) {
this.playerDataService = registry.get(PlayerDataService.class);
this.nickService = registry.get(NickService.class);
public PlayerEventListener(ServiceRegistry registry, AdvancedMessageFilter advancedFilter) {
this.playerDataService = registry.get(PlayerDataService.class);
this.nickService = registry.get(NickService.class);
this.pmService = registry.get(PMService.class);
this.spyService = registry.get(SpyService.class);
this.advancedFilter = advancedFilter;
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
// Обновляем displayName с кастомным ником если есть
if (nickService != null) {
nickService.updatePlayerDisplay(player);
}
@ -33,6 +41,13 @@ public class PlayerEventListener implements Listener {
@EventHandler
public void onQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
playerDataService.clearPlayerData(player.getUniqueId());
java.util.UUID uuid = player.getUniqueId();
playerDataService.clearPlayerData(uuid);
pmService.removeConversation(uuid);
spyService.removeSpy(uuid);
// Очищаем flood / spam трекеры — иначе данные висят в памяти вечно
advancedFilter.clearPlayer(uuid);
}
}

View file

@ -8,113 +8,173 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.List;
/**
* Рендерер чата с поддержкой глобального/локального режимов
* Рендерер чата — глобальный и локальный режимы.
* Цвета PREFIX и текста сообщения полностью настраиваются через chat-appearance.yml.
*/
public class EnhancedChatRenderer implements ChatRenderer {
private static final TextColor FALLBACK_COLOR = NamedTextColor.WHITE;
private final JavaPlugin plugin;
private final Player sender;
private final Component originalMessage;
private final boolean isGlobal;
private final MentionHandler mentionHandler;
private final com.loki.lochat.config.AppearanceConfig cfg;
private final com.loki.lochat.config.HardcodedMessages hm;
public EnhancedChatRenderer(JavaPlugin plugin, Player sender, Component originalMessage, boolean isGlobal) {
this.plugin = plugin;
this.sender = sender;
this.originalMessage = originalMessage;
this.isGlobal = isGlobal;
this.mentionHandler = new MentionHandler(plugin);
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
this.cfg = loChat.getConfigManager().getAppearanceConfig();
this.hm = loChat.getConfigManager().getHardcodedMessages();
}
@Override
public Component render(Player source, Component sourceDisplayName, Component message, Audience viewer) {
// Красивые префиксы с градиентами и эмодзи
Component prefixComponent;
if (isGlobal) {
// 🌍 GLOBAL с градиентом от золотого к оранжевому
prefixComponent = Component.text("🌍 ")
.append(Component.text("G").color(net.kyori.adventure.text.format.TextColor.fromHexString("#FFD700")))
.append(Component.text("L").color(net.kyori.adventure.text.format.TextColor.fromHexString("#FFA500")))
.append(Component.text("O").color(net.kyori.adventure.text.format.TextColor.fromHexString("#FF8C00")))
.append(Component.text("B").color(net.kyori.adventure.text.format.TextColor.fromHexString("#FF7F50")))
.append(Component.text("A").color(net.kyori.adventure.text.format.TextColor.fromHexString("#FF6347")))
.append(Component.text("L").color(net.kyori.adventure.text.format.TextColor.fromHexString("#FF4500")));
} else {
// LOCAL с градиентом от голубого к синему (без эмодзи)
prefixComponent = Component.text("L").color(net.kyori.adventure.text.format.TextColor.fromHexString("#87CEEB"))
.append(Component.text("O").color(net.kyori.adventure.text.format.TextColor.fromHexString("#87CEFA")))
.append(Component.text("C").color(net.kyori.adventure.text.format.TextColor.fromHexString("#00BFFF")))
.append(Component.text("A").color(net.kyori.adventure.text.format.TextColor.fromHexString("#1E90FF")))
.append(Component.text("L").color(net.kyori.adventure.text.format.TextColor.fromHexString("#4169E1")));
}
// Используем displayName игрока напрямую (он уже Component с градиентом)
Component playerComponent = createPlayerComponent(source, source.displayName());
Component processedMessage = message;
Component prefix = buildPrefix();
Component separator = buildSeparator();
Component player = buildPlayerComponent(source);
processedMessage = TextFormatter.formatMarkdown(processedMessage);
processedMessage = processUrls(processedMessage);
processedMessage = processEmojis(processedMessage);
Component processed = message;
processed = TextFormatter.formatMarkdown(processed);
processed = processUrls(processed);
processed = processEmojis(processed);
if (viewer instanceof Player viewerPlayer) {
processedMessage = mentionHandler.processMentions(processedMessage, source, viewerPlayer);
if (viewer instanceof Player vp) {
processed = mentionHandler.processMentions(processed, source, vp);
}
// Красивое разделение с градиентными стрелками
Component separator = isGlobal ?
Component.text(" ▶ ", net.kyori.adventure.text.format.TextColor.fromHexString("#FFD700")) :
Component.text(" ▶ ", net.kyori.adventure.text.format.TextColor.fromHexString("#FFFFFF"));
return prefixComponent
return prefix
.append(separator)
.append(playerComponent)
.append(Component.text(": ", net.kyori.adventure.text.format.NamedTextColor.WHITE))
.append(processedMessage);
.append(player)
.append(Component.text(": ", NamedTextColor.WHITE))
.append(processed);
}
private Component createPlayerComponent(Player player, Component displayName) {
if (!plugin.getConfig().getBoolean("chat.hover.enabled", true)) {
// ──────────────────────────────────────────────────────────────────────────
// Префикс
// ──────────────────────────────────────────────────────────────────────────
private Component buildPrefix() {
String emoji = isGlobal ? cfg.getGlobalEmoji() : cfg.getLocalEmoji();
String text = isGlobal ? cfg.getGlobalText() : cfg.getLocalText();
List<String> colors = isGlobal ? cfg.getGlobalColors() : cfg.getLocalColors();
Component prefix = Component.empty();
if (!emoji.isEmpty()) {
prefix = prefix.append(Component.text(emoji + " "));
}
prefix = prefix.append(buildGradientText(text, colors));
return prefix;
}
/**
* Строит компонент с плавным градиентом.
* colors — список hex-цветов (минимум 1).
* Если цветов меньше чем букв — интерполируем плавно между соседними цветами.
* Если цветов больше чем букв — берём по одному на букву.
*/
private Component buildGradientText(String text, List<String> colors) {
if (text.isEmpty()) return Component.empty();
if (colors.isEmpty()) {
return Component.text(text, FALLBACK_COLOR);
}
// Один цвет — просто красим весь текст
if (colors.size() == 1) {
return Component.text(text, parseColor(colors.get(0)));
}
char[] chars = text.toCharArray();
int len = chars.length;
Component result = Component.empty();
for (int i = 0; i < len; i++) {
// Нормализованная позиция 0..1
float t = len == 1 ? 0f : (float) i / (len - 1);
// Какой сегмент градиента
float scaled = t * (colors.size() - 1);
int seg = (int) scaled;
float frac = scaled - seg;
// Зажимаем в пределах списка
if (seg >= colors.size() - 1) {
seg = colors.size() - 2;
frac = 1f;
}
TextColor from = parseColor(colors.get(seg));
TextColor to = parseColor(colors.get(seg + 1));
TextColor blended = blend(from, to, frac);
result = result.append(Component.text(String.valueOf(chars[i]), blended));
}
return result;
}
/** Линейная интерполяция между двумя TextColor */
private static TextColor blend(TextColor from, TextColor to, float t) {
int r = Math.round(from.red() + t * (to.red() - from.red()));
int g = Math.round(from.green() + t * (to.green() - from.green()));
int b = Math.round(from.blue() + t * (to.blue() - from.blue()));
return TextColor.color(clamp(r), clamp(g), clamp(b));
}
private static int clamp(int v) { return Math.max(0, Math.min(255, v)); }
// ──────────────────────────────────────────────────────────────────────────
// Разделитель
// ──────────────────────────────────────────────────────────────────────────
private Component buildSeparator() {
String text = isGlobal ? cfg.getGlobalSeparatorText() : cfg.getLocalSeparatorText();
String color = isGlobal ? cfg.getGlobalSeparatorColor() : cfg.getLocalSeparatorColor();
return Component.text(text, parseColor(color));
}
// ──────────────────────────────────────────────────────────────────────────
// Ник игрока с hover
// ──────────────────────────────────────────────────────────────────────────
private Component buildPlayerComponent(Player player) {
Component displayName = player.displayName();
if (!cfg.isHoverEnabled()) {
return displayName.clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " "));
}
java.util.List<String> hoverLines = plugin.getConfig().getStringList("chat.hover.format");
if (hoverLines.isEmpty()) {
hoverLines = java.util.Arrays.asList(
"<gradient:#FFD700:#FFA500><bold>▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬</bold></gradient>",
"<gradient:#87CEEB:#4169E1><bold>👤 Информация об игроке</bold></gradient>",
"",
"<gray>🏷️ Имя: <white><bold>{player}</bold>",
"<gray>📶 Пинг: <yellow><bold>{ping}ms</bold>",
"<gray>❤️ Здоровье: <red><bold>{health}/20</bold>",
"<gray>🍖 Голод: <gold><bold>{food}/20</bold>",
"",
"<gradient:#FFD700:#FFA500><bold>▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬</bold></gradient>",
"<yellow>💬 <bold>Клик:</bold> <gray>написать личное сообщение"
);
}
List<String> hoverLines = cfg.getHoverFormat();
net.kyori.adventure.text.TextComponent.Builder hoverBuilder = Component.text();
for (int i = 0; i < hoverLines.size(); i++) {
String line = hoverLines.get(i);
String line = hoverLines.get(i)
.replace("{player}", player.getName())
.replace("{world}", player.getWorld().getName())
.replace("{ping}", String.valueOf(player.getPing()))
.replace("{gamemode}", hm.getGamemodeName(player.getGameMode().name().toLowerCase()))
.replace("{health}", String.valueOf(Math.round(player.getHealth())))
.replace("{food}", String.valueOf(player.getFoodLevel()));
line = line.replace("{player}", player.getName())
.replace("{world}", player.getWorld().getName())
.replace("{ping}", String.valueOf(player.getPing()))
.replace("{gamemode}", getGameModeDisplay(player.getGameMode()))
.replace("{health}", String.valueOf(Math.round(player.getHealth())))
.replace("{food}", String.valueOf(player.getFoodLevel()));
Component lineComponent = com.loki.lochat.utils.ChatFormatter.parse(line);
hoverBuilder.append(lineComponent);
if (i < hoverLines.size() - 1) {
hoverBuilder.append(Component.newline());
}
hoverBuilder.append(com.loki.lochat.utils.ChatFormatter.parse(line));
if (i < hoverLines.size() - 1) hoverBuilder.append(Component.newline());
}
return displayName
@ -122,86 +182,43 @@ public class EnhancedChatRenderer implements ChatRenderer {
.clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " "));
}
/**
* Получить красивое отображение игрового режима
*/
private String getGameModeDisplay(org.bukkit.GameMode gameMode) {
return switch (gameMode) {
case SURVIVAL -> "Выживание";
case CREATIVE -> "Творчество";
case ADVENTURE -> "Приключение";
case SPECTATOR -> "Наблюдатель";
};
}
// ──────────────────────────────────────────────────────────────────────────
// URL и эмодзи
// ──────────────────────────────────────────────────────────────────────────
private Component processUrls(Component message) {
String plainText = PlainTextComponentSerializer.plainText().serialize(message);
// Fixed regex: escape the dash in character class to avoid illegal range
String urlPattern = "(https?://[\\w\\-.~:/?#\\[\\]@!$&'()*+,;=%]+)";
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(urlPattern);
java.util.regex.Matcher matcher = pattern.matcher(plainText);
String plain = PlainTextComponentSerializer.plainText().serialize(message);
java.util.regex.Pattern pattern =
java.util.regex.Pattern.compile("(https?://[\\w\\-.~:/?#\\[\\]@!$&'()*+,;=%]+)");
java.util.regex.Matcher matcher = pattern.matcher(plain);
Component result = message;
while (matcher.find()) {
String url = matcher.group(1);
Component urlComponent = Component.text(url, NamedTextColor.AQUA, TextDecoration.UNDERLINED)
Component urlComp = Component.text(url, NamedTextColor.AQUA, TextDecoration.UNDERLINED)
.hoverEvent(HoverEvent.showText(Component.text("Открыть: " + url, NamedTextColor.GRAY)))
.clickEvent(ClickEvent.openUrl(url));
result = result.replaceText(builder ->
builder.matchLiteral(url).replacement(urlComponent)
);
result = result.replaceText(b -> b.matchLiteral(url).replacement(urlComp));
}
return result;
}
private Component processEmojis(Component message) {
Component result = message;
// Основные символы
result = result.replaceText(builder -> builder.matchLiteral(":heart:").replacement("♥"));
result = result.replaceText(builder -> builder.matchLiteral(":star:").replacement("★"));
result = result.replaceText(builder -> builder.matchLiteral(":check:").replacement("✓"));
result = result.replaceText(builder -> builder.matchLiteral(":cross:").replacement("✗"));
result = result.replaceText(builder -> builder.matchLiteral(":arrow:").replacement("→"));
result = result.replaceText(builder -> builder.matchLiteral(":skull:").replacement("☠"));
result = result.replaceText(builder -> builder.matchLiteral(":note:").replacement("♪"));
result = result.replaceText(builder -> builder.matchLiteral(":sun:").replacement("☀"));
result = result.replaceText(builder -> builder.matchLiteral(":moon:").replacement("☽"));
result = result.replaceText(builder -> builder.matchLiteral(":snowflake:").replacement("❄"));
result = result.replaceText(builder -> builder.matchLiteral(":fire:").replacement("▲"));
// Minecraft символы
result = result.replaceText(builder -> builder.matchLiteral(":diamond:").replacement("◆"));
result = result.replaceText(builder -> builder.matchLiteral(":sword:").replacement("⚔"));
result = result.replaceText(builder -> builder.matchLiteral(":pickaxe:").replacement("⛏"));
result = result.replaceText(builder -> builder.matchLiteral(":bow:").replacement("⌐"));
result = result.replaceText(builder -> builder.matchLiteral(":shield:").replacement("▣"));
result = result.replaceText(builder -> builder.matchLiteral(":potion:").replacement("⚗"));
result = result.replaceText(builder -> builder.matchLiteral(":book:").replacement("▤"));
result = result.replaceText(builder -> builder.matchLiteral(":crown:").replacement("♔"));
result = result.replaceText(builder -> builder.matchLiteral(":gem:").replacement("◊"));
result = result.replaceText(builder -> builder.matchLiteral(":emerald:").replacement("◈"));
result = result.replaceText(builder -> builder.matchLiteral(":gold:").replacement("◉"));
result = result.replaceText(builder -> builder.matchLiteral(":iron:").replacement("◎"));
// Дополнительные символы
result = result.replaceText(builder -> builder.matchLiteral(":thumbsup:").replacement("▲"));
result = result.replaceText(builder -> builder.matchLiteral(":thumbsdown:").replacement("▼"));
result = result.replaceText(builder -> builder.matchLiteral(":clap:").replacement("※"));
result = result.replaceText(builder -> builder.matchLiteral(":wave:").replacement("~"));
result = result.replaceText(builder -> builder.matchLiteral(":peace:").replacement("✌"));
result = result.replaceText(builder -> builder.matchLiteral(":ok:").replacement("◯"));
result = result.replaceText(builder -> builder.matchLiteral(":facepalm:").replacement("⌐_⌐"));
result = result.replaceText(builder -> builder.matchLiteral(":shrug:").replacement("¯\\_(ツ)_/¯"));
result = result.replaceText(builder -> builder.matchLiteral(":thinking:").replacement("?"));
result = result.replaceText(builder -> builder.matchLiteral(":laugh:").replacement("xD"));
result = result.replaceText(builder -> builder.matchLiteral(":cry:").replacement(":'("));
result = result.replaceText(builder -> builder.matchLiteral(":rage:").replacement(">:("));
result = result.replaceText(builder -> builder.matchLiteral(":cool:").replacement("B)"));
result = result.replaceText(builder -> builder.matchLiteral(":wink:").replacement(";)"));
for (java.util.Map.Entry<String, String> e : cfg.getEmojis().entrySet()) {
result = result.replaceText(b -> b.matchLiteral(e.getKey()).replacement(e.getValue()));
}
return result;
}
}
// ──────────────────────────────────────────────────────────────────────────
// Утилиты
// ──────────────────────────────────────────────────────────────────────────
private TextColor parseColor(String hex) {
if (hex == null || hex.isBlank()) return FALLBACK_COLOR;
String n = hex.startsWith("#") ? hex : "#" + hex;
TextColor c = TextColor.fromHexString(n);
return c != null ? c : FALLBACK_COLOR;
}
}

View file

@ -222,12 +222,19 @@ public final class ChatFormatter {
* Получает отображаемое имя игрока с градиентом
*/
private static String getPlayerDisplayName(Player player) {
// Получаем экземпляр плагина
com.loki.lochat.LoChat plugin = com.loki.lochat.LoChat.getInstance();
try {
// Получаем экземпляр плагина
com.loki.lochat.LoChat plugin = com.loki.lochat.LoChat.getInstance();
// Если градиентный модуль включен, используем его
if (plugin.getGradientModule() != null && plugin.getGradientModule().isEnabled()) {
return plugin.getGradientModule().getFormattedName(player);
// Если градиентный модуль включен, используем его
if (plugin.getGradientModule() != null && plugin.getGradientModule().isEnabled()) {
return plugin.getGradientModule().getFormattedName(player);
}
} catch (Exception e) {
// Если произошла ошибка, логируем и возвращаем обычное имя
com.loki.lochat.LoChat.getInstance().getLogger().warning(
"Ошибка при получении градиентного имени для " + player.getName() + ": " + e.getMessage()
);
}
// Иначе просто имя игрока

View file

@ -1,27 +1,25 @@
package com.loki.lochat.utils;
/**
* Константы плагина
* Константы плагина (только неизменяемые системные значения)
*/
public final class Constants {
// Чат
public static final int DEFAULT_LOCAL_RADIUS = 100;
public static final int CLEAR_CHAT_LINES = 100;
// Время
// Время (системные константы)
public static final long MILLIS_IN_SECOND = 1000L;
public static final int SECONDS_IN_MINUTE = 60;
public static final int MINUTES_IN_HOUR = 60;
public static final int HOURS_IN_DAY = 24;
// Градиенты
public static final int DEFAULT_PRICE_PER_COLOR = 50;
public static final int DEFAULT_PREFIX_PRICE = 500;
public static final int LUCKPERMS_PREFIX_PRIORITY = 100;
// Folia/Paper совместимость
// Folia/Paper совместимость (системные константы)
public static final int TICKS_PER_SECOND = 20;
public static final int MILLIS_PER_TICK = 50;
// Анти-спам
// Математические константы
public static final int PERCENTAGE_MULTIPLIER = 100;
// LuckPerms приоритет (системная константа)
public static final int LUCKPERMS_PREFIX_PRIORITY = 100;
private Constants() {
}

View file

@ -3,7 +3,6 @@ package com.loki.lochat.utils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TextFormatter {
@ -14,82 +13,86 @@ public class TextFormatter {
private static final Pattern STRIKETHROUGH_PATTERN = Pattern.compile("~~(.+?)~~");
private static final Pattern CODE_PATTERN = Pattern.compile("`(.+?)`");
// Паттерн для быстрой проверки — есть ли в тексте markdown-символы
private static final Pattern HAS_MARKDOWN = Pattern.compile("[*_~`]");
/**
* Применяет markdown-форматирование к компоненту.
*
* Важно: PlainTextComponentSerializer стриппает все Adventure-цвета.
* Поэтому мы обрабатываем только replaceText на оригинальном компоненте,
* не конвертируя его в plain string и обратно.
* Это сохраняет градиенты и цвета, добавленные Adventure.
*/
public static Component formatMarkdown(Component message) {
String text = PlainTextComponentSerializer.plainText().serialize(message);
String plain = PlainTextComponentSerializer.plainText().serialize(message);
text = formatBold(text);
text = formatItalic(text);
text = formatUnderline(text);
text = formatStrikethrough(text);
text = formatCode(text);
// Быстрая проверка — нет markdown-символов, возвращаем как есть (без потери цветов)
if (!HAS_MARKDOWN.matcher(plain).find()) {
return message;
}
return ChatFormatter.parse(text);
// Применяем замены через replaceText прямо на компоненте — цвета сохраняются
message = applyBold(message);
message = applyItalic(message);
message = applyUnderline(message);
message = applyStrikethrough(message);
message = applyCode(message);
return message;
}
private static String formatBold(String text) {
Matcher matcher = BOLD_PATTERN.matcher(text);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String content = matcher.group(1);
matcher.appendReplacement(result, "<bold>" + content + "</bold>");
}
matcher.appendTail(result);
return result.toString();
private static Component applyBold(Component message) {
return message.replaceText(config -> config
.match(BOLD_PATTERN)
.replacement((matchResult, builder) -> {
String content = matchResult.group(1);
return ChatFormatter.parse("<bold>" + content + "</bold>");
})
);
}
private static String formatItalic(String text) {
Matcher matcher = ITALIC_PATTERN.matcher(text);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String content = matcher.group(1);
if (!content.startsWith("*") && !content.endsWith("*")) {
matcher.appendReplacement(result, "<italic>" + content + "</italic>");
}
}
matcher.appendTail(result);
return result.toString();
private static Component applyItalic(Component message) {
return message.replaceText(config -> config
.match(ITALIC_PATTERN)
.replacement((matchResult, builder) -> {
String content = matchResult.group(1);
// Пропускаем если это часть ** (bold)
if (content.startsWith("*") || content.endsWith("*")) {
return builder.content(matchResult.group());
}
return ChatFormatter.parse("<italic>" + content + "</italic>");
})
);
}
private static String formatUnderline(String text) {
Matcher matcher = UNDERLINE_PATTERN.matcher(text);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String content = matcher.group(1);
matcher.appendReplacement(result, "<underlined>" + content + "</underlined>");
}
matcher.appendTail(result);
return result.toString();
private static Component applyUnderline(Component message) {
return message.replaceText(config -> config
.match(UNDERLINE_PATTERN)
.replacement((matchResult, builder) -> {
String content = matchResult.group(1);
return ChatFormatter.parse("<underlined>" + content + "</underlined>");
})
);
}
private static String formatStrikethrough(String text) {
Matcher matcher = STRIKETHROUGH_PATTERN.matcher(text);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String content = matcher.group(1);
matcher.appendReplacement(result, "<strikethrough>" + content + "</strikethrough>");
}
matcher.appendTail(result);
return result.toString();
private static Component applyStrikethrough(Component message) {
return message.replaceText(config -> config
.match(STRIKETHROUGH_PATTERN)
.replacement((matchResult, builder) -> {
String content = matchResult.group(1);
return ChatFormatter.parse("<strikethrough>" + content + "</strikethrough>");
})
);
}
private static String formatCode(String text) {
Matcher matcher = CODE_PATTERN.matcher(text);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String content = matcher.group(1);
matcher.appendReplacement(result, "&#888888`" + content + "`");
}
matcher.appendTail(result);
return result.toString();
private static Component applyCode(Component message) {
return message.replaceText(config -> config
.match(CODE_PATTERN)
.replacement((matchResult, builder) -> {
String content = matchResult.group(1);
return ChatFormatter.parse("&#888888`" + content + "`");
})
);
}
}