chore: update .gitignore and build.gradle.kts for local server testing

- Added entries to .gitignore for local Paper test server and API documentation.
- Included the run-paper plugin in build.gradle.kts for local server execution.
- Configured runServer task with Minecraft version 1.21.4 for testing.
This commit is contained in:
loki5512344 2026-04-01 16:09:15 +02:00
parent d1d2b93742
commit 2d79d2a91d
260 changed files with 4808 additions and 3847 deletions

View file

@ -1,20 +1,14 @@
package com.loki.lochat;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.api.service.NickService;
import com.loki.lochat.commands.*;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.commands.CommandManager;
import com.loki.lochat.config.ConfigManager;
import com.loki.lochat.config.MessageConfig;
import com.loki.lochat.core.PluginInitializer;
import com.loki.lochat.core.PluginShutdown;
import com.loki.lochat.core.registry.ServiceRegistry;
import com.loki.lochat.gradient.GradientModule;
import com.loki.lochat.integrations.DiscordIntegration;
import com.loki.lochat.integrations.LibertyBansHook;
import com.loki.lochat.integrations.PlaceholderAPIHook;
import com.loki.lochat.core.filter.AdvancedMessageFilter;
import com.loki.lochat.listener.ChatEventListener;
import com.loki.lochat.listener.DiscordEventListener;
import com.loki.lochat.listener.PlayerEventListener;
import com.loki.lochat.managers.AutoMessageManager;
import com.loki.lochat.managers.CustomCommandManager;
import org.bukkit.Bukkit;
@ -27,6 +21,8 @@ import org.bukkit.plugin.java.JavaPlugin;
public final class LoChat extends JavaPlugin {
private static LoChat instance;
// Основные компоненты
private ServiceRegistry serviceRegistry;
private ConfigManager configManager;
private MessageConfig messageConfig;
@ -35,115 +31,51 @@ public final class LoChat extends JavaPlugin {
private CustomCommandManager customCommandManager;
private DiscordIntegration discordIntegration;
private CommandManager commandManager;
private AdvancedMessageFilter advancedMessageFilter;
// Геттеры
public static LoChat getInstance() {
return instance;
}
// Вспомогательные классы
private PluginInitializer initializer;
private PluginShutdown shutdown;
@Override
public void onEnable() {
instance = this;
getLogger().info("Загрузка LoChat v" + getPluginMeta().getVersion() + " для Paper/Folia " + Bukkit.getMinecraftVersion() + "...");
// Инициализация
initializer = new PluginInitializer(this);
shutdown = new PluginShutdown(this);
// Конфиги
configManager = new ConfigManager(this);
messageConfig = new MessageConfig(this);
// Service Registry (SOLID DI) - с зависимостями
serviceRegistry = ServiceRegistry.createWithDeps(this, configManager, messageConfig);
getLogger().info("ServiceRegistry инициализирован (SOLID DI)");
configManager = initializer.initConfigs();
messageConfig = initializer.initMessageConfig();
// Service Registry (SOLID DI)
serviceRegistry = initializer.initServiceRegistry(configManager, messageConfig);
// Менеджеры
customCommandManager = CustomCommandManager.create(this);
autoMessageManager = new AutoMessageManager(this);
customCommandManager = initializer.initCustomCommandManager();
autoMessageManager = initializer.initAutoMessageManager();
// Интеграции
initIntegrations();
discordIntegration = initializer.initDiscordIntegration();
gradientModule = initializer.initGradientModule();
initializer.initIntegrations(discordIntegration, gradientModule);
// Команды и слушатели
commandManager = new CommandManager(this);
commandManager.registerCommands();
registerListeners();
registerPluginChannels();
initializer.registerListeners(serviceRegistry, discordIntegration);
initializer.registerPluginChannels();
// Автосообщения
autoMessageManager.start();
getLogger().info("LoChat успешно запущен!");
}
@Override
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();
MuteService muteService = serviceRegistry.get(MuteService.class);
if (muteService != null) muteService.save();
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 отключен!");
}
private void initIntegrations() {
// Discord интеграция
discordIntegration = new DiscordIntegration(this);
// Gradient модуль
gradientModule = new GradientModule(this);
if (gradientModule.init()) {
getLogger().info("Gradient модуль (LoPreff) загружен!");
}
// LibertyBans
new LibertyBansHook(this);
// PlaceholderAPI
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
new PlaceholderAPIHook(this).register();
getLogger().info("PlaceholderAPI подключен!");
}
}
// Старый метод регистрации команд - заменен на CommandManager
/*
private void registerCommands() {
// Код перенесен в CommandManager для лучшей организации
}
*/
private void registerListeners() {
// Создаём один экземпляр фильтра и передаём в оба листенера — 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()) {
getServer().getPluginManager().registerEvents(new DiscordEventListener(discordIntegration), this);
getLogger().info("Discord event listener registered");
}
getLogger().info("Слушатели зарегистрированы (новая архитектура)");
}
private void registerPluginChannels() {
getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord");
getLogger().info("BungeeCord plugin messaging channel registered");
shutdown.shutdown(serviceRegistry, autoMessageManager, discordIntegration, gradientModule);
}
public void reload() {
@ -153,6 +85,17 @@ public final class LoChat extends JavaPlugin {
if (gradientModule != null) gradientModule.reload();
if (customCommandManager != null) customCommandManager.reload();
if (discordIntegration != null) discordIntegration.reload();
if (serviceRegistry != null) {
try {
serviceRegistry.get(PunishmentService.class).save();
} catch (IllegalStateException ignored) {
}
}
}
// Геттеры
public static LoChat getInstance() {
return instance;
}
public ServiceRegistry getServiceRegistry() {

View file

@ -0,0 +1,38 @@
package com.loki.lochat.api.service;
import com.loki.lochat.data.model.BanRecord;
import com.loki.lochat.data.model.WarnEntry;
import net.kyori.adventure.text.Component;
import java.util.List;
import java.util.UUID;
/**
* Варны и баны LoChat
*/
public interface PunishmentService {
void addWarn(UUID uuid, String playerName, String moderator, String reason, boolean silent);
List<WarnEntry> getWarns(UUID uuid);
int getWarnCount(UUID uuid);
/**
* @param durationMs длительность; 0 = навсегда
*/
void ban(UUID uuid, String playerName, long durationMs, String reason, String bannedBy);
boolean unban(UUID uuid);
boolean isBanned(UUID uuid);
BanRecord getActiveBan(UUID uuid);
/** Сообщение при кике (MiniMessage + legacy &#) */
Component buildBanKickMessage(BanRecord ban);
void save();
void reload();
}

View file

@ -43,10 +43,14 @@ public class CommandManager {
reg("lmutelist", new MuteListCommand(plugin));
regTab("lmutehistory", new MuteHistoryCommand(plugin));
regTab("lmuteblame", new MuteBlameCommand(plugin));
regTab("warn", new WarnCommand(plugin));
regTab("silentwarn", new SilentWarnCommand(plugin));
regTab("lban", new BanCommand(plugin));
regTab("lunban", new UnbanCommand(plugin));
// ── Ник / профиль ─────────────────────────────────────────────────────
regTab("nick", new NickCommand(plugin));
reg("playerinfo", new PlayerInfoCommand(plugin));
regTab("playerinfo", new PlayerInfoCommand(plugin));
// ── Админ ─────────────────────────────────────────────────────────────
reg("announce", new AnnounceCommand(plugin));

View file

@ -35,4 +35,4 @@ public final class CustomCommand extends Command {
// Базовое автодополнение - можно расширить при необходимости
return new ArrayList<>();
}
}
}

View file

@ -8,7 +8,6 @@ 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.Arrays;
@ -125,4 +124,4 @@ public class CustomCommandsCommand implements CommandExecutor, TabCompleter {
return new ArrayList<>();
}
}
}

View file

@ -22,7 +22,7 @@ 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(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly());
sender.sendMessage(plugin.getConfigManager().getMessagesConfig().getPlayerOnly());
return true;
}
boolean enabled = spyService.toggleSpy(player.getUniqueId());

View file

@ -18,7 +18,7 @@ public class ReloadConfigCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("lochat.admin.reload")) {
sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getHardcodedMessages().getNoPermission()));
sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoPermission()));
return true;
}
try {

View file

@ -32,4 +32,4 @@ public abstract class AdminCommand extends BaseCommand {
*/
protected abstract boolean executeAdminCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args);
}
}

View file

@ -68,7 +68,7 @@ public abstract class BaseCommand implements CommandExecutor, TabCompleter {
*/
protected boolean requirePlayer(CommandSender sender) {
if (!(sender instanceof Player)) {
sender.sendMessage(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly());
sender.sendMessage(plugin.getConfigManager().getMessagesConfig().getPlayerOnly());
return false;
}
return true;
@ -79,7 +79,7 @@ public abstract class BaseCommand implements CommandExecutor, TabCompleter {
*/
protected boolean requirePermission(CommandSender sender, String permission) {
if (!hasPermission(sender, permission)) {
sender.sendMessage(plugin.getConfigManager().getHardcodedMessages().getNoPermission());
sender.sendMessage(plugin.getConfigManager().getMessagesConfig().getNoPermission());
return false;
}
return true;
@ -98,4 +98,4 @@ public abstract class BaseCommand implements CommandExecutor, TabCompleter {
protected void sendUsage(CommandSender sender, String usage) {
sender.sendMessage("§cИспользование: " + usage);
}
}
}

View file

@ -31,4 +31,4 @@ public abstract class PlayerCommand extends BaseCommand {
*/
protected abstract boolean executePlayerCommand(@NotNull Player player, @NotNull Command command,
@NotNull String label, @NotNull String[] args);
}
}

View file

@ -3,7 +3,6 @@ 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;
@ -25,7 +24,7 @@ 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(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly());
sender.sendMessage(plugin.getConfigManager().getMessagesConfig().getPlayerOnly());
return true;
}

View file

@ -24,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(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly());
sender.sendMessage(plugin.getConfigManager().getMessagesConfig().getPlayerOnly());
return true;
}

View file

@ -22,7 +22,7 @@ public class IgnoreCommand 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(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly()); return true; }
if (!(sender instanceof Player player)) { sender.sendMessage(plugin.getConfigManager().getMessagesConfig().getPlayerOnly()); return true; }
if (args.length == 0) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/ignore <ник>"))); return true; }
Player target = Bukkit.getPlayer(args[0]);

View file

@ -25,7 +25,7 @@ public class IgnoreListCommand 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(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly()); return true; }
if (!(sender instanceof Player player)) { sender.sendMessage(plugin.getConfigManager().getMessagesConfig().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; }

View file

@ -31,7 +31,7 @@ public class MsgCommand 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(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly());
sender.sendMessage(plugin.getConfigManager().getMessagesConfig().getPlayerOnly());
return true;
}
if (!plugin.getConfigManager().isPmEnabled()) return true;

View file

@ -34,7 +34,7 @@ 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(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly());
sender.sendMessage(plugin.getConfigManager().getMessagesConfig().getPlayerOnly());
return true;
}
if (!plugin.getConfigManager().isPmEnabled()) return true;

View file

@ -22,7 +22,7 @@ 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(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly()); return true; }
if (!(sender instanceof Player player)) { sender.sendMessage(plugin.getConfigManager().getMessagesConfig().getPlayerOnly()); return true; }
if (args.length == 0) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getInvalidUsage("/unignore <ник>"))); return true; }
Player target = Bukkit.getPlayer(args[0]);

View file

@ -0,0 +1,136 @@
package com.loki.lochat.commands.moderation;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.config.MessagesConfig;
import com.loki.lochat.data.model.BanRecord;
import com.loki.lochat.utils.ChatFormatter;
import com.loki.lochat.utils.PlayerUtil;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* /lban — бан с красивым киком
*/
public class BanCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final PunishmentService punishmentService;
private final MuteService muteService;
public BanCommand(LoChat plugin) {
this.plugin = plugin;
this.punishmentService = plugin.getServiceRegistry().get(PunishmentService.class);
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
MessagesConfig msg = plugin.getConfigManager().getMessagesConfig();
if (!sender.hasPermission("lochat.ban")) {
sender.sendMessage(ChatFormatter.parse(msg.getNoPermission()));
return true;
}
if (args.length < 1) {
sender.sendMessage(ChatFormatter.parse(msg.getBanUsage()));
sender.sendMessage(ChatFormatter.parse(msg.getMuteTimeHelp()));
return true;
}
String targetName = args[0];
UUID targetUuid = PlayerUtil.findPlayerUUID(targetName);
if (targetUuid == null) {
sender.sendMessage(ChatFormatter.parse(msg.getPlayerNotFound()));
return true;
}
if (sender instanceof Player p && p.getUniqueId().equals(targetUuid)) {
sender.sendMessage(ChatFormatter.parse("&#CF6679Нельзя забанить самого себя"));
return true;
}
Player onlineTarget = Bukkit.getPlayer(targetUuid);
if (onlineTarget != null && onlineTarget.hasPermission("lochat.bypass.ban") && !sender.hasPermission("lochat.ban.override")) {
sender.sendMessage(ChatFormatter.parse("&#CF6679Нельзя забанить этого игрока"));
return true;
}
if (punishmentService.isBanned(targetUuid)) {
sender.sendMessage(ChatFormatter.parse(msg.getAlreadyBanned()));
return true;
}
String timeStr = null;
StringBuilder reasonBuilder = new StringBuilder();
for (int i = 1; i < args.length; i++) {
if (timeStr == null && isTimeFormat(args[i])) {
timeStr = args[i];
} else {
if (reasonBuilder.length() > 0) reasonBuilder.append(" ");
reasonBuilder.append(args[i]);
}
}
long durationMs;
if (timeStr == null || timeStr.isEmpty()) {
durationMs = 0;
} else if (timeStr.equalsIgnoreCase("perm") || timeStr.equals("0")) {
durationMs = 0;
} else {
durationMs = muteService.parseTime(timeStr);
if (durationMs < 0) {
sender.sendMessage(ChatFormatter.parse(msg.getInvalidTime()));
return true;
}
}
String reason = reasonBuilder.length() > 0 ? reasonBuilder.toString() : msg.getModerationDefaultReason();
String finalName = PlayerUtil.getPlayerName(targetUuid);
if (finalName == null) finalName = targetName;
punishmentService.ban(targetUuid, finalName, durationMs, reason, sender.getName());
String durationLabel = durationMs == 0
? msg.getModerationDurationPermanent()
: muteService.formatTime(durationMs);
sender.sendMessage(ChatFormatter.parse(msg.getBanConfirm().replace("{player}", finalName).replace("{duration}", durationLabel)));
BanRecord ban = punishmentService.getActiveBan(targetUuid);
if (ban != null) {
Player kicked = Bukkit.getPlayer(targetUuid);
if (kicked != null && kicked.isOnline()) {
kicked.kick(punishmentService.buildBanKickMessage(ban));
}
}
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("1h", "1d", "7d", "30d", "perm"));
}
return c;
}
}

View file

@ -2,7 +2,7 @@ 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.config.MessagesConfig;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
@ -34,7 +34,7 @@ public class MuteCommand implements CommandExecutor, TabCompleter {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
HardcodedMessages hm = plugin.getConfigManager().getHardcodedMessages();
MessagesConfig hm = plugin.getConfigManager().getMessagesConfig();
if (!sender.hasPermission("lochat.mute")) {
sender.sendMessage(plugin.getMessageConfig().getComponent("errors.no-permission"));
@ -68,7 +68,7 @@ public class MuteCommand implements CommandExecutor, TabCompleter {
}
if (silent && !sender.hasPermission("lochat.mute.silent")) {
sender.sendMessage(ChatFormatter.parse(hm.getNoSilentPermission()));
sender.sendMessage(ChatFormatter.parse(hm.getNoSilentMutePermission()));
return true;
}
@ -123,6 +123,12 @@ public class MuteCommand implements CommandExecutor, TabCompleter {
Bukkit.broadcast(ChatFormatter.parse(broadcastMsg));
}
String voiceCmd = plugin.getConfig().getString("moderation.voice-mute-console-command", "");
if (target != null && voiceCmd != null && !voiceCmd.isEmpty()) {
String c = voiceCmd.replace("{player}", finalName).replace("{uuid}", targetUUID.toString());
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), c);
}
return true;
}

View file

@ -4,7 +4,6 @@ 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;

View file

@ -0,0 +1,85 @@
package com.loki.lochat.commands.moderation;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.config.MessagesConfig;
import com.loki.lochat.utils.ChatFormatter;
import com.loki.lochat.utils.PlayerUtil;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* /silentwarn — варн в базе, цель не видит; модераторы с lochat.warn.notify видят
*/
public class SilentWarnCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final PunishmentService punishmentService;
public SilentWarnCommand(LoChat plugin) {
this.plugin = plugin;
this.punishmentService = plugin.getServiceRegistry().get(PunishmentService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
MessagesConfig msg = plugin.getConfigManager().getMessagesConfig();
if (!sender.hasPermission("lochat.warn.silent")) {
sender.sendMessage(ChatFormatter.parse(msg.getNoPermission()));
return true;
}
if (args.length < 1) {
sender.sendMessage(ChatFormatter.parse(msg.getSilentWarnUsage()));
return true;
}
UUID targetUuid = PlayerUtil.findPlayerUUID(args[0]);
if (targetUuid == null) {
sender.sendMessage(ChatFormatter.parse(msg.getPlayerNotFound()));
return true;
}
String mod = sender.getName();
String reason = args.length > 1
? String.join(" ", java.util.Arrays.copyOfRange(args, 1, args.length))
: msg.getModerationDefaultReason();
String name = PlayerUtil.getPlayerName(targetUuid);
if (name == null) name = args[0];
punishmentService.addWarn(targetUuid, name, mod, reason, true);
sender.sendMessage(ChatFormatter.parse(msg.getWarnStaffConfirm().replace("{player}", name).replace("{reason}", reason)));
String staffLine = msg.getSilentWarnStaff().replace("{player}", name).replace("{reason}", reason);
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.hasPermission("lochat.warn.notify")) {
p.sendMessage(ChatFormatter.parse(staffLine));
}
}
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
List<String> out = new ArrayList<>();
if (args.length == 1) {
String p = args[0].toLowerCase();
for (Player pl : Bukkit.getOnlinePlayers()) {
if (pl.getName().toLowerCase().startsWith(p)) out.add(pl.getName());
}
}
return out;
}
}

View file

@ -0,0 +1,78 @@
package com.loki.lochat.commands.moderation;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.config.MessagesConfig;
import com.loki.lochat.utils.ChatFormatter;
import com.loki.lochat.utils.PlayerUtil;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* /lunban — снять бан
*/
public class UnbanCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final PunishmentService punishmentService;
public UnbanCommand(LoChat plugin) {
this.plugin = plugin;
this.punishmentService = plugin.getServiceRegistry().get(PunishmentService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
MessagesConfig msg = plugin.getConfigManager().getMessagesConfig();
if (!sender.hasPermission("lochat.unban")) {
sender.sendMessage(ChatFormatter.parse(msg.getNoPermission()));
return true;
}
if (args.length < 1) {
sender.sendMessage(ChatFormatter.parse("&#B798A8Использование: &#7858E9/lunban <ник>"));
return true;
}
UUID uuid = PlayerUtil.findPlayerUUID(args[0]);
if (uuid == null) {
sender.sendMessage(ChatFormatter.parse(msg.getPlayerNotFound()));
return true;
}
if (!punishmentService.isBanned(uuid)) {
sender.sendMessage(ChatFormatter.parse(msg.getNotBanned()));
return true;
}
String name = PlayerUtil.getPlayerName(uuid);
if (name == null) name = args[0];
punishmentService.unban(uuid);
sender.sendMessage(ChatFormatter.parse(msg.getUnbanConfirm().replace("{player}", name)));
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
List<String> out = new ArrayList<>();
if (args.length == 1) {
String p = args[0].toLowerCase();
for (Player pl : Bukkit.getOnlinePlayers()) {
if (pl.getName().toLowerCase().startsWith(p)) out.add(pl.getName());
}
}
return out;
}
}

View file

@ -38,7 +38,7 @@ public class UnmuteCommand implements CommandExecutor, TabCompleter {
else if (targetName == null) targetName = a;
}
if (silent && !sender.hasPermission("lochat.mute.silent")) {
sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getHardcodedMessages().getNoSilentUnmutePermission()));
sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoSilentUnmutePermission()));
return true;
}
@ -59,7 +59,7 @@ public class UnmuteCommand implements CommandExecutor, TabCompleter {
Bukkit.broadcast(ChatFormatter.parse("&#9878C9Игрок &#7858E9" + name + " &#9878C9размучен модератором &#7858E9" + op));
}
} else {
sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getHardcodedMessages().getPlayerNotMutedSimple()));
sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getPlayerNotMutedSimple()));
}
return true;
}

View file

@ -0,0 +1,103 @@
package com.loki.lochat.commands.moderation;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.config.MessagesConfig;
import com.loki.lochat.utils.ChatFormatter;
import com.loki.lochat.utils.PlayerUtil;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* /warn — игрок видит предупреждение в чате
*/
public class WarnCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final PunishmentService punishmentService;
public WarnCommand(LoChat plugin) {
this.plugin = plugin;
this.punishmentService = plugin.getServiceRegistry().get(PunishmentService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
MessagesConfig msg = plugin.getConfigManager().getMessagesConfig();
if (!sender.hasPermission("lochat.warn")) {
sender.sendMessage(ChatFormatter.parse(msg.getNoPermission()));
return true;
}
if (args.length < 1) {
sender.sendMessage(ChatFormatter.parse(msg.getWarnUsage()));
return true;
}
UUID targetUuid = PlayerUtil.findPlayerUUID(args[0]);
if (targetUuid == null) {
sender.sendMessage(ChatFormatter.parse(msg.getPlayerNotFound()));
return true;
}
if (sender instanceof Player p && p.getUniqueId().equals(targetUuid)) {
sender.sendMessage(ChatFormatter.parse("&#CF6679Нельзя выдать варн самому себе"));
return true;
}
String mod = sender.getName();
String reason = buildReason(msg, args, 1);
String name = PlayerUtil.getPlayerName(targetUuid);
if (name == null) name = args[0];
Player targetOnline = Bukkit.getPlayer(targetUuid);
if (targetOnline != null && targetOnline.hasPermission("lochat.bypass.warn")) {
sender.sendMessage(ChatFormatter.parse("&#CF6679У этого игрока защита от варнов"));
return true;
}
punishmentService.addWarn(targetUuid, name, mod, reason, false);
sender.sendMessage(ChatFormatter.parse(msg.getWarnStaffConfirm().replace("{player}", name).replace("{reason}", reason)));
if (targetOnline != null && targetOnline.isOnline()) {
targetOnline.sendMessage(ChatFormatter.parse(msg.getWarnReceived().replace("{reason}", reason)));
}
return true;
}
private static String buildReason(MessagesConfig msg, String[] args, int from) {
if (args.length <= from) {
return msg.getModerationDefaultReason();
}
StringBuilder sb = new StringBuilder();
for (int i = from; i < args.length; i++) {
if (sb.length() > 0) sb.append(" ");
sb.append(args[i]);
}
return sb.toString();
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
List<String> out = new ArrayList<>();
if (args.length == 1) {
String p = args[0].toLowerCase();
for (Player pl : Bukkit.getOnlinePlayers()) {
if (pl.getName().toLowerCase().startsWith(p)) out.add(pl.getName());
}
}
return out;
}
}

View file

@ -27,11 +27,11 @@ public class NickCommand implements CommandExecutor, TabCompleter {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text(plugin.getConfigManager().getHardcodedMessages().getPlayerOnly(), NamedTextColor.RED));
sender.sendMessage(Component.text(plugin.getConfigManager().getMessagesConfig().getPlayerOnly(), NamedTextColor.RED));
return true;
}
if (!player.hasPermission("lochat.nick")) {
player.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getHardcodedMessages().getNoPermission()));
player.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoPermission()));
return true;
}

View file

@ -1,39 +1,137 @@
package com.loki.lochat.commands.nick;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.data.model.BanRecord;
import com.loki.lochat.data.model.MuteData;
import com.loki.lochat.data.model.WarnEntry;
import com.loki.lochat.utils.ChatFormatter;
import com.loki.lochat.utils.PlayerUtil;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class PlayerInfoCommand implements CommandExecutor {
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* /playerinfo — статистика варнов, банов и мутов
*/
public class PlayerInfoCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final MuteService muteService;
private final PunishmentService punishmentService;
public PlayerInfoCommand(LoChat plugin) {
this.plugin = plugin;
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
this.punishmentService = plugin.getServiceRegistry().get(PunishmentService.class);
}
@Override
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Игрок не найден или не в сети"));
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("lochat.playerinfo")) {
sender.sendMessage(ChatFormatter.parse(plugin.getConfigManager().getMessagesConfig().getNoPermission()));
return true;
}
if (args.length < 1) {
sender.sendMessage(ChatFormatter.parse("&#B798A8Использование: &#7858E9/playerinfo <ник>"));
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) {
UUID uuid = PlayerUtil.findPlayerUUID(args[0]);
if (uuid == null) {
sender.sendMessage(ChatFormatter.parse("&#CF6679Игрок не найден"));
return true;
}
String name = PlayerUtil.getPlayerName(uuid);
if (name == null) name = args[0];
Player target = Bukkit.getPlayer(uuid);
sender.sendMessage(ChatFormatter.parse("<gradient:#7858E9:#B798A8><bold>═══════ Профиль: " + name + " ═══════</bold></gradient>"));
if (target != null) {
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()));
} else {
sender.sendMessage(ChatFormatter.parse("&#B798A8Игрок &#7858E9не в сети"));
}
if (plugin.getGradientModule() != null && target != null) {
sender.sendMessage(ChatFormatter.parse("&#9878C9⚡ &fГрадиент: " + plugin.getGradientModule().getFormattedName(target)));
}
int warns = punishmentService.getWarnCount(uuid);
sender.sendMessage(ChatFormatter.parse("&#CF6679⚠ &fВарнов всего: &#7858E9" + warns));
List<WarnEntry> recent = punishmentService.getWarns(uuid);
int show = Math.min(5, recent.size());
for (int i = recent.size() - show; i < recent.size(); i++) {
WarnEntry w = recent.get(i);
String tag = w.silent ? "&#B798A8[тихий] " : "";
String reason = w.reason != null ? w.reason : "—";
long t = w.time;
sender.sendMessage(ChatFormatter.parse(tag + "&#9878C9• &7" + formatTime(t) + " &#B798A8от &#7858E9" + w.moderator + "&#9878C9: &f" + reason));
}
BanRecord ban = punishmentService.getActiveBan(uuid);
if (ban != null) {
String until = ban.isPermanent()
? "&#CF6679навсегда"
: "&#7858E9" + muteService.formatTime(Math.max(0, ban.until - System.currentTimeMillis()));
sender.sendMessage(ChatFormatter.parse("&#CF6679🚫 &fБан: &cактивен &7(" + until + "&7)"));
sender.sendMessage(ChatFormatter.parse("&#B798A8Причина: &f" + (ban.reason != null ? ban.reason : "—")));
} else {
sender.sendMessage(ChatFormatter.parse("&#9878C9🚫 &fБан: &aнет"));
}
if (muteService.isMuted(uuid)) {
MuteData md = muteService.getMuteData(uuid);
long rem = muteService.getRemainingTime(uuid);
String remStr = rem < 0 ? "навсегда" : muteService.formatTime(rem);
sender.sendMessage(ChatFormatter.parse("&#CF6679🔇 &fМут: &cда &7(" + remStr + "&7)"));
if (md != null) {
sender.sendMessage(ChatFormatter.parse("&#B798A8Причина: &f" + (md.getReason() != null ? md.getReason() : "—")));
}
} else {
sender.sendMessage(ChatFormatter.parse("&#9878C9🔇 &fМут: &aнет"));
}
var muteHist = muteService.getPlayerHistory(uuid);
if (muteHist != null && !muteHist.isEmpty()) {
sender.sendMessage(ChatFormatter.parse("&#B798A8Записей в истории мутов: &#7858E9" + muteHist.size()));
}
return true;
}
private static String formatTime(long ms) {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm");
return sdf.format(new java.util.Date(ms));
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
List<String> out = new ArrayList<>();
if (args.length == 1) {
String p = args[0].toLowerCase();
for (Player pl : Bukkit.getOnlinePlayers()) {
if (pl.getName().toLowerCase().startsWith(p)) out.add(pl.getName());
}
}
return out;
}
}

View file

@ -31,7 +31,7 @@ public class DoCommand extends PlayerCommand {
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));
String msg = plugin.getConfigManager().getMessagesConfig().getCooldownMessage().replace("{remaining}", String.valueOf(remaining));
player.sendMessage(ChatFormatter.parse(msg));
return true;
}

View file

@ -31,7 +31,7 @@ public class MeCommand extends PlayerCommand {
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));
String msg = plugin.getConfigManager().getMessagesConfig().getCooldownMessage().replace("{remaining}", String.valueOf(remaining));
player.sendMessage(ChatFormatter.parse(msg));
return true;
}

View file

@ -5,7 +5,6 @@ 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;
@ -18,7 +17,7 @@ import java.util.Random;
* Бросает кубик от 1 до максимума (по умолчанию 100).
* Формат: * ИгрокИмя бросает кубик и получает N из M
*/
public class RollCommand extends PlayerCommand implements TabCompleter {
public class RollCommand extends PlayerCommand {
private static final Random RANDOM = new Random();
private static final int DEFAULT_MAX = 100;

View file

@ -34,7 +34,7 @@ public class TryCommand extends PlayerCommand {
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));
String msg = plugin.getConfigManager().getMessagesConfig().getCooldownMessage().replace("{remaining}", String.valueOf(remaining));
player.sendMessage(ChatFormatter.parse(msg));
return true;
}

View file

@ -1,51 +1,33 @@
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;
/**
* Конфигурация внешнего вида чата
* Конфигурация внешнего вида чата (config/appearance.yml)
*/
public class AppearanceConfig {
private final Plugin plugin;
private YamlConfiguration config;
public class AppearanceConfig extends BaseConfig {
private final Map<String, String> emojiCache = new HashMap<>();
public AppearanceConfig(Plugin plugin) {
this.plugin = plugin;
// Не вызываем loadConfig() в конструкторе для избежания this-escape
super((org.bukkit.plugin.java.JavaPlugin) plugin, "appearance.yml", true); // true = в папке config/
// init() вызывается отдельно в методе init()
}
/**
* Инициализировать конфигурацию (вызывать после создания объекта)
*/
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);
super.init(); // ✅ Вызываем init() родителя
loadEmojiCache();
}
/**
* Перезагрузить конфигурацию
*/
public void reload() {
loadConfig();
@Override
protected void onLoad() {
loadEmojiCache();
}
/**
@ -146,6 +128,22 @@ public class AppearanceConfig {
return config.getString("prefixes.local.message-color", "#FFFFFF");
}
// ========== ФОРМАТ СООБЩЕНИЙ ==========
/**
* Получить формат сообщения для глобального чата
*/
public String getGlobalChatFormat() {
return config.getString("chat-format.global", "{emoji} {prefix} {separator} {player} : {message}");
}
/**
* Получить формат сообщения для локального чата
*/
public String getLocalChatFormat() {
return config.getString("chat-format.local", "{emoji} {prefix} {separator} {player} : {message}");
}
// ========== HOVER ЭФФЕКТЫ ==========
/**
@ -279,4 +277,4 @@ public class AppearanceConfig {
public String getDiscordEventTitle(String event) {
return config.getString("discord.event_titles." + event, "");
}
}
}

View file

@ -0,0 +1,121 @@
package com.loki.lochat.config;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.InputStream;
import java.nio.file.Files;
/**
* Базовый класс для всех конфигураций
* Избегает дублирования кода загрузки конфигов
*/
public abstract class BaseConfig {
protected final JavaPlugin plugin;
protected File configFile;
protected FileConfiguration config;
private final String fileName;
private final boolean inConfigFolder;
/**
* Конструктор для конфигов в корне папки плагина
* @param plugin плагин
* @param fileName имя файла (например "config.yml")
*/
protected BaseConfig(JavaPlugin plugin, String fileName) {
this(plugin, fileName, false);
}
/**
* Конструктор для конфигов в подпапке config/
* @param plugin плагин
* @param fileName имя файла (например "messages.yml")
* @param inConfigFolder true если файл в папке config/
*/
protected BaseConfig(JavaPlugin plugin, String fileName, boolean inConfigFolder) {
this.plugin = plugin;
this.fileName = fileName;
this.inConfigFolder = inConfigFolder;
// ✅ FIX: Не вызываем loadConfig() здесь чтобы избежать this-escape
// Вызывается явно после создания объекта
}
/**
* Инициализация конфига (вызывать после создания)
*/
public void init() {
loadConfig();
}
/**
* Загрузить конфигурацию
*/
protected void loadConfig() {
// Определяем путь к файлу
if (inConfigFolder) {
File configFolder = new File(plugin.getDataFolder(), "config");
if (!configFolder.exists()) {
configFolder.mkdirs();
}
configFile = new File(configFolder, fileName);
} else {
configFile = new File(plugin.getDataFolder(), fileName);
}
// Копируем из ресурсов если не существует
if (!configFile.exists()) {
try {
String resourcePath = inConfigFolder ? "config/" + fileName : fileName;
InputStream in = plugin.getResource(resourcePath);
if (in != null) {
Files.copy(in, configFile.toPath());
in.close();
plugin.getLogger().info("Created config file: " + (inConfigFolder ? "config/" : "") + fileName);
} else {
plugin.getLogger().warning("Resource not found: " + resourcePath);
}
} catch (Exception e) {
plugin.getLogger().warning("Failed to copy " + fileName + ": " + e.getMessage());
}
}
config = YamlConfiguration.loadConfiguration(configFile);
onLoad();
}
/**
* Перезагрузить конфигурацию
*/
public void reload() {
config = YamlConfiguration.loadConfiguration(configFile);
onLoad();
}
/**
* Сохранить конфигурацию
*/
public void save() {
try {
config.save(configFile);
} catch (Exception e) {
plugin.getLogger().warning("Failed to save " + fileName + ": " + e.getMessage());
}
}
/**
* Вызывается после загрузки конфига
* Переопределите для дополнительной инициализации
*/
protected void onLoad() {
// Переопределяется в подклассах
}
/**
* Получить FileConfiguration
*/
public FileConfiguration getConfig() {
return config;
}
}

View file

@ -9,9 +9,15 @@ import java.util.List;
public class ConfigManager {
private final LoChat plugin;
private FileConfiguration config;
private AppearanceConfig appearanceConfig;
private HardcodedMessages hardcodedMessages;
// ✅ THREAD-SAFETY: volatile для безопасного reload из разных потоков
// volatile гарантирует что все потоки видят актуальную версию конфига после reload
private volatile FileConfiguration config;
private volatile AppearanceConfig appearanceConfig;
private volatile MessagesConfig messagesConfig;
private volatile MuteConfig muteConfig;
private volatile FiltersConfig filtersConfig;
private volatile SoundsConfig soundsConfig;
public ConfigManager(LoChat plugin) {
this.plugin = plugin;
@ -20,11 +26,13 @@ public class ConfigManager {
// Инициализируем новые конфигурации
this.appearanceConfig = new AppearanceConfig(plugin);
this.hardcodedMessages = new HardcodedMessages(plugin);
this.messagesConfig = new MessagesConfig(plugin);
this.muteConfig = new MuteConfig(plugin);
this.filtersConfig = new FiltersConfig(plugin);
this.soundsConfig = new SoundsConfig(plugin);
// Инициализируем их после создания
this.appearanceConfig.init();
this.hardcodedMessages.init();
// Проверяем версию конфига и обновляем при необходимости
updateConfig();
@ -38,32 +46,47 @@ public class ConfigManager {
}
/**
* Получить конфигурацию хардкодированных сообщений
* Получить конфигурацию системных сообщений
*/
public HardcodedMessages getHardcodedMessages() {
return hardcodedMessages;
public MessagesConfig getMessagesConfig() {
return messagesConfig;
}
/**
* Получить конфигурацию мутов
*/
public MuteConfig getMuteConfig() {
return muteConfig;
}
/**
* Получить конфигурацию фильтров
*/
public FiltersConfig getFiltersConfig() {
return filtersConfig;
}
/**
* Получить конфигурацию звуков
*/
public SoundsConfig getSoundsConfig() {
return soundsConfig;
}
private void updateConfig() {
int currentVersion = config.getInt("config-version", 1);
int requiredVersion = 2; // Текущая версия конфига
int requiredVersion = 5;
if (currentVersion < requiredVersion) {
plugin.getLogger().info("Обновление конфига с версии " + currentVersion + " до " + requiredVersion);
// Добавляем новые настройки если их нет
addMissingConfigOptions();
// Обновляем версию
config.set("config-version", requiredVersion);
plugin.saveConfig();
plugin.getLogger().info("Конфиг успешно обновлен!");
}
}
private void addMissingConfigOptions() {
// Добавляем настройки объявлений если их нет
if (!config.contains("announcements")) {
config.set("announcements.show-title", true);
config.set("announcements.title-header", "&#FFD700ОБЪЯВЛЕНИЕ");
@ -71,7 +94,6 @@ public class ConfigManager {
config.set("announcements.title-duration", 3);
}
// Можно добавить другие недостающие настройки
if (!config.contains("chat.pm.enabled")) {
config.set("chat.pm.enabled", true);
}
@ -100,48 +122,47 @@ public class ConfigManager {
config.set("lopreff.use-gradient-name", true);
config.set("lopreff.use-custom-prefix", true);
}
if (!config.contains("moderation.voice-mute-console-command")) {
config.set("moderation.voice-mute-console-command", "");
}
}
public void reload() {
/**
* Перезагрузить все конфигурации
* ✅ THREAD-SAFE: использует volatile для безопасного обновления
*/
public synchronized void reload() {
plugin.reloadConfig();
this.config = plugin.getConfig();
// Перезагружаем новые конфигурации
// volatile гарантирует что изменения видны всем потокам
this.appearanceConfig.reload();
this.hardcodedMessages.reload();
this.messagesConfig.reload();
this.muteConfig.reload();
this.filtersConfig.reload();
this.soundsConfig.reload();
plugin.getLogger().info("Все конфигурации успешно перезагружены");
}
/**
* Получить строку из конфига с дефолтным значением
*/
public String getString(String path, String defaultValue) {
return config.getString(path, defaultValue);
}
/**
* Получить строку из конфига
*/
public String getString(String path) {
return config.getString(path);
}
/**
* Получить int из конфига
*/
public int getInt(String path, int defaultValue) {
return config.getInt(path, defaultValue);
}
/**
* Получить boolean из конфига
*/
public boolean getBoolean(String path, boolean defaultValue) {
return config.getBoolean(path, defaultValue);
}
/**
* Получить список строк из конфига
*/
public List<String> getStringList(String path) {
return config.getStringList(path);
}
@ -151,18 +172,6 @@ public class ConfigManager {
return config.getBoolean("chat.global.enabled", true);
}
public String getGlobalPrefix() {
return ChatFormatter.convertAllColors(config.getString("chat.global.prefix", "[G]"));
}
public String getGlobalFormat() {
return ChatFormatter.convertAllColors(config.getString("chat.global.format", "<prefix><player>: <message>"));
}
public String getGlobalSymbol() {
return config.getString("chat.global.symbol", "!");
}
public int getGlobalCooldown() {
return config.getInt("chat.global.cooldown", 3);
}
@ -176,14 +185,6 @@ public class ConfigManager {
return appearanceConfig.getLocalRadius();
}
public String getLocalPrefix() {
return ChatFormatter.convertAllColors(config.getString("chat.local.prefix", "[L]"));
}
public String getLocalFormat() {
return ChatFormatter.convertAllColors(config.getString("chat.local.format", "<player>: <message>"));
}
public int getLocalCooldown() {
return config.getInt("chat.local.cooldown", 1);
}
@ -198,11 +199,13 @@ public class ConfigManager {
}
public boolean isPmSoundEnabled() {
return config.getBoolean("chat.pm.sound", true);
// ✅ Используем SoundsConfig если доступен
return soundsConfig != null ? soundsConfig.isPmSoundEnabled() : config.getBoolean("chat.pm.sound", true);
}
public String getPmSoundType() {
return config.getString("chat.pm.sound-type", "ENTITY_EXPERIENCE_ORB_PICKUP");
// ✅ Используем SoundsConfig если доступен
return soundsConfig != null ? soundsConfig.getPmSound() : config.getString("chat.pm.sound-type", "ENTITY_EXPERIENCE_ORB_PICKUP");
}
// Mentions
@ -211,11 +214,13 @@ public class ConfigManager {
}
public boolean isMentionSoundEnabled() {
return config.getBoolean("mentions.sound", true);
// ✅ Используем SoundsConfig если доступен
return soundsConfig != null ? soundsConfig.isMentionSoundEnabled() : config.getBoolean("mentions.sound", true);
}
public String getMentionSoundType() {
return config.getString("mentions.sound-type", "BLOCK_NOTE_BLOCK_PLING");
// ✅ Используем SoundsConfig если доступен
return soundsConfig != null ? soundsConfig.getMentionSound() : config.getString("mentions.sound-type", "BLOCK_NOTE_BLOCK_PLING");
}
public String getMentionHighlight() {

View file

@ -0,0 +1,162 @@
package com.loki.lochat.config;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.List;
/**
* Конфигурация фильтров (config/filters.yml)
*/
public class FiltersConfig extends BaseConfig {
public FiltersConfig(JavaPlugin plugin) {
super(plugin, "filters.yml", true);
init(); // ✅ Явная инициализация
}
// ── Капс ────────────────────────────────────────────────────────────────────
public boolean isCapsEnabled() {
return config.getBoolean("caps.enabled", true);
}
public int getCapsMaxPercent() {
return config.getInt("caps.max-percent", 70);
}
public int getCapsMinLength() {
return config.getInt("caps.min-length", 5);
}
public boolean isCapsAutoLowercase() {
return config.getBoolean("caps.auto-lowercase", true);
}
public boolean isCapsBlock() {
return config.getBoolean("caps.block", false);
}
// ── Мат ─────────────────────────────────────────────────────────────────────
public boolean isSwearEnabled() {
return config.getBoolean("swear.enabled", true);
}
public String getSwearAction() {
return config.getString("swear.action", "replace");
}
public String getSwearReplacementChar() {
return config.getString("swear.replacement-char", "*");
}
public boolean isSwearCheckFragments() {
return config.getBoolean("swear.check-fragments", true);
}
public boolean isSwearIgnoreCase() {
return config.getBoolean("swear.ignore-case", true);
}
public boolean isSwearUseExternalFile() {
return config.getBoolean("swear.use-external-file", true);
}
public List<String> getSwearWords() {
return config.getStringList("swear.words");
}
public String getSwearBlockMessage() {
return config.getString("swear.block-message", "&#CF6679Мат запрещён");
}
// ── Реклама ─────────────────────────────────────────────────────────────────
public boolean isAdvertisingEnabled() {
return config.getBoolean("advertising.enabled", true);
}
public boolean isBlockHiddenUrls() {
return config.getBoolean("advertising.block-hidden-urls", true);
}
public boolean isBlockDomains() {
return config.getBoolean("advertising.block-domains", true);
}
public List<String> getWhitelistedDomains() {
return config.getStringList("advertising.whitelist");
}
public List<String> getBlacklistedDomains() {
return config.getStringList("advertising.blacklist");
}
public String getAdvertisingBlockMessage() {
return config.getString("advertising.blocked-message", "&#CF6679Реклама запрещена!");
}
// ── IP адреса ───────────────────────────────────────────────────────────────
public boolean isIpEnabled() {
return config.getBoolean("ip.enabled", true);
}
public boolean isIpBlock() {
return config.getBoolean("ip.block", false);
}
public String getIpReplacement() {
return config.getString("ip.replacement", "[IP скрыт]");
}
public String getIpBlockMessage() {
return config.getString("ip.blocked-message", "&#CF6679IP адреса запрещены!");
}
// ── Повторяющиеся символы ───────────────────────────────────────────────────
public boolean isRepeatEnabled() {
return config.getBoolean("repeat.enabled", true);
}
public int getRepeatMaxRepeats() {
return config.getInt("repeat.max-repeats", 3);
}
// ── Флуд ────────────────────────────────────────────────────────────────────
public boolean isFloodEnabled() {
return config.getBoolean("flood.enabled", true);
}
public int getFloodMaxMessages() {
return config.getInt("flood.max-messages", 5);
}
public int getFloodTimePeriod() {
return config.getInt("flood.time-period", 10);
}
public String getFloodBlockMessage() {
return config.getString("flood.block-message", "&#CF6679Не флудите!");
}
// ── Спам ────────────────────────────────────────────────────────────────────
public boolean isSpamEnabled() {
return config.getBoolean("spam.enabled", true);
}
public int getSpamMaxSimilarMessages() {
return config.getInt("spam.max-similar-messages", 3);
}
public int getSpamSimilarityThreshold() {
return config.getInt("spam.similarity-threshold", 80);
}
public String getSpamBlockMessage() {
return config.getString("spam.block-message", "&#CF6679Не отправляйте одинаковые сообщения");
}
}

View file

@ -1,261 +0,0 @@
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

@ -0,0 +1,220 @@
package com.loki.lochat.config;
import org.bukkit.plugin.java.JavaPlugin;
/**
* Конфигурация системных сообщений (config/messages.yml)
*/
public class MessagesConfig extends BaseConfig {
public MessagesConfig(JavaPlugin plugin) {
super(plugin, "messages.yml", true); // true = в папке config/
init(); // ✅ Явная инициализация
}
// ── Общие сообщения ────────────────────────────────────────────────────────
public String getNoPermission() {
return config.getString("general.no_permission", "&#CF6679Нет прав!");
}
public String getPlayerOnly() {
return config.getString("general.player_only", "&#CF6679Только для игроков!");
}
public String getPlayerNotFound() {
return config.getString("general.player_not_found", "&#CF6679Игрок не найден!");
}
// ── Кулдаун ────────────────────────────────────────────────────────────────
public String getCooldownMessage() {
return config.getString("commands.cooldown.message", "&#B798A8Подождите &#7858E9{remaining} &#B798A8сек");
}
// ── Локальный чат ──────────────────────────────────────────────────────────
public String getNobodyHeard() {
return config.getString("local_chat.nobody_heard", "&#9878C9Вас никто не услышал &#B798A8— рядом нет игроков");
}
// ── Мут ────────────────────────────────────────────────────────────────────
public String getMutedMessage() {
return config.getString("mute.muted_message", "&#CF6679Вы замучены! &#B798A8Причина: &#7858E9{reason}&#B798A8. Осталось: &#7858E9{time}");
}
public String getMutedPermanent() {
return config.getString("mute.muted_permanent", "&#CF6679Вы замучены навсегда! &#B798A8Причина: &#7858E9{reason}");
}
public String getPlayerMuted() {
return config.getString("mute.player_muted", "&#9878C9Игрок &#7858E9{player} &#9878C9замучен на &#7858E9{time}&#9878C9. Причина: &#B798A8{reason}");
}
public String getPlayerMutedPermanent() {
return config.getString("mute.player_muted_permanent", "&#9878C9Игрок &#7858E9{player} &#9878C9замучен навсегда. Причина: &#B798A8{reason}");
}
public String getPlayerMutedSilent() {
return config.getString("mute.player_muted_silent", "&#B798A8[&#9878C9ТИХИЙ МУТ&#B798A8] &#7858E9{player} &#B798A8замучен на &#7858E9{time}");
}
public String getPlayerUnmuted() {
return config.getString("mute.player_unmuted", "&#9878C9Игрок &#7858E9{player} &#9878C9размучен");
}
public String getPlayerNotMuted() {
return config.getString("mute.not_muted", "&#B798A8Игрок &#7858E9{player} &#B798A8не замучен");
}
public String getPlayerNotMutedSimple() {
return config.getString("mute.player_not_muted_simple", "&#CF6679Игрок не замучен!");
}
public String getInvalidTime() {
return config.getString("mute.invalid_time", "&#CF6679Неверный формат времени!");
}
public String getAlreadyMuted() {
return config.getString("mute.already_muted", "&#CF6679Игрок уже замучен!");
}
public String getNoSilentUnmutePermission() {
return config.getString("mute.no_silent_unmute_permission", "&#CF6679Нет права на тихий размут!");
}
public String getNoSilentMutePermission() {
return config.getString("commands.mute.no_silent_permission", "&#CF6679Нет права на тихий мут");
}
public String getMuteUsage() {
return config.getString("commands.mute.usage", "&#B798A8Использование: &#7858E9/mute <ник> [время] [-s] [причина]");
}
public String getMuteTimeHelp() {
return config.getString("commands.mute.time_help", "&#9878C9Время: &#7858E91d&#9878C9, &#7858E92h&#9878C9, &#7858E930m&#9878C9, &#7858E960s&#9878C9, &#7858E9perm");
}
public String getMuteSilentHelp() {
return config.getString("commands.mute.silent_help", "&#B798A8-s &#9878C9— тихий мут (право &#7858E9lochat.mute.silent&#9878C9)");
}
public String getDefaultMuteReason() {
return config.getString("commands.mute.default_reason", "Без причины");
}
public String getDefaultMuteDuration() {
return config.getString("commands.mute.default_duration", "7d");
}
// ── ЛС ─────────────────────────────────────────────────────────────────────
public String getNoReplyTarget() {
return config.getString("private_messages.no_reply_target", "&#CF6679Нет цели для ответа");
}
public String getPlayerOffline() {
return config.getString("private_messages.player_offline", "&#CF6679Игрок &#7858E9{player} &#CF6679не в сети");
}
public String getCannotMessageSelf() {
return config.getString("private_messages.cannot_message_self", "&#CF6679Нельзя писать самому себе");
}
// ── Игнор ──────────────────────────────────────────────────────────────────
public String getPlayerIgnored() {
return config.getString("ignore.player_ignored", "&#9878C9Игрок &#7858E9{player} &#9878C9добавлен в игнор");
}
public String getPlayerUnignored() {
return config.getString("ignore.player_unignored", "&#9878C9Игрок &#7858E9{player} &#9878C9убран из игнора");
}
public String getAlreadyIgnored() {
return config.getString("ignore.already_ignored", "&#B798A8Игрок &#7858E9{player} &#B798A8уже в игноре");
}
public String getNotIgnored() {
return config.getString("ignore.not_ignored", "&#B798A8Игрок &#7858E9{player} &#B798A8не в игноре");
}
public String getCannotIgnoreSelf() {
return config.getString("ignore.cannot_ignore_self", "&#CF6679Нельзя игнорировать самого себя");
}
public String getIgnoreListEmpty() {
return config.getString("ignore.ignore_list_empty", "&#B798A8Список игнора пуст");
}
public String getIgnoreListHeader() {
return config.getString("ignore.ignore_list_header", "&#B798A8Игнорируемые игроки&#9878C9:");
}
// ── Модерация (варны / баны) ────────────────────────────────────────────────
public String getModerationDefaultReason() {
return config.getString("moderation.default_reason", "Без причины");
}
public String getModerationDurationPermanent() {
return config.getString("moderation.duration_permanent", "навсегда");
}
/**
* MiniMessage + legacy &# цвета; плейсхолдеры {reason},{moderator},{duration},{player}; перевод строки \\n
*/
public String getBanKickMessage() {
return config.getString("moderation.ban_kick_message", defaultBanKick());
}
private String defaultBanKick() {
return "<gradient:#FF5555:#8B0000><bold>═══════ ДОСТУП ЗАПРЕЩЁН ═══════</bold></gradient>\n"
+ "<gray>Вы заблокированы на этом сервере.</gray>\n"
+ "<white>Причина: <gradient:#7858E9:#B798A8>{reason}</gradient></white>\n"
+ "<gray>Модератор: <white>{moderator}</white></gray>\n"
+ "<gray>Срок: <#7858E9>{duration}</#7858E9></gray>\n"
+ "<gradient:#FF5555:#8B0000><bold>════════════════════════</bold></gradient>";
}
public String getWarnReceived() {
return config.getString("moderation.warn_received", "&#CF6679⚠ Вам выдано предупреждение! &#B798A8Причина: &#7858E9{reason}");
}
public String getWarnStaffConfirm() {
return config.getString("moderation.warn_staff_confirm", "&#9878C9Игроку &#7858E9{player} &#9878C9выдан варн. &#B798A8Причина: &#7858E9{reason}");
}
public String getSilentWarnStaff() {
return config.getString("moderation.silent_warn_staff", "&#B798A8[&#9878C9ТИХИЙ ВАРН&#B798A8] &#7858E9{player}&#9878C9: &#B798A8{reason}");
}
public String getBanUsage() {
return config.getString("moderation.ban_usage", "&#B798A8Использование: &#7858E9/lban <ник> [время|perm] [причина]");
}
public String getBanConfirm() {
return config.getString("moderation.ban_confirm", "&#9878C9Игрок &#7858E9{player} &#9878C9забанен. &#B798A8Срок: &#7858E9{duration}");
}
public String getUnbanConfirm() {
return config.getString("moderation.unban_confirm", "&#9878C9Бан снят с &#7858E9{player}");
}
public String getNotBanned() {
return config.getString("moderation.not_banned", "&#CF6679Игрок не забанен!");
}
public String getAlreadyBanned() {
return config.getString("moderation.already_banned", "&#CF6679Игрок уже забанен!");
}
public String getWarnUsage() {
return config.getString("moderation.warn_usage", "&#B798A8Использование: &#7858E9/warn <ник> [причина]");
}
public String getSilentWarnUsage() {
return config.getString("moderation.silent_warn_usage", "&#B798A8Использование: &#7858E9/silentwarn <ник> [причина]");
}
}

View file

@ -0,0 +1,62 @@
package com.loki.lochat.config;
import org.bukkit.plugin.java.JavaPlugin;
/**
* Конфигурация мутов (config/mute.yml)
*/
public class MuteConfig extends BaseConfig {
public MuteConfig(JavaPlugin plugin) {
super(plugin, "mute.yml", true);
init(); // ✅ Явная инициализация
}
// ── Основные настройки ──────────────────────────────────────────────────────
public boolean isEnabled() {
return config.getBoolean("enabled", true);
}
public String getDefaultDuration() {
return config.getString("default-duration", "7d");
}
public String getDefaultReason() {
return config.getString("default-reason", "Без причины");
}
public int getMaxHistoryPerPlayer() {
return config.getInt("max-history-per-player", 50);
}
// ── Права на длительность ───────────────────────────────────────────────────
public boolean isPermissionBasedDuration() {
return config.getBoolean("permission-based-duration", true);
}
// ── Уведомления ─────────────────────────────────────────────────────────────
public boolean isNotifyOnMute() {
return config.getBoolean("notify.on-mute", true);
}
public boolean isNotifyOnUnmute() {
return config.getBoolean("notify.on-unmute", true);
}
public boolean isNotifyOnExpire() {
return config.getBoolean("notify.on-expire", true);
}
// ── Тихий мут ───────────────────────────────────────────────────────────────
public boolean isSilentMuteEnabled() {
return config.getBoolean("silent-mute.enabled", true);
}
public boolean isSilentMuteLogToConsole() {
return config.getBoolean("silent-mute.log-to-console", true);
}
}

View file

@ -0,0 +1,68 @@
package com.loki.lochat.config;
import org.bukkit.plugin.java.JavaPlugin;
/**
* Конфигурация звуков (config/sounds.yml)
*/
public class SoundsConfig extends BaseConfig {
public SoundsConfig(JavaPlugin plugin) {
super(plugin, "sounds.yml", true);
init(); // ✅ Явная инициализация
}
// ── Звуки упоминаний ────────────────────────────────────────────────────────
public boolean isMentionSoundEnabled() {
return config.getBoolean("mention.enabled", true);
}
public String getMentionSound() {
return config.getString("mention.sound", "BLOCK_NOTE_BLOCK_PLING");
}
public float getMentionVolume() {
return (float) config.getDouble("mention.volume", 1.0);
}
public float getMentionPitch() {
return (float) config.getDouble("mention.pitch", 1.0);
}
// ── Звуки личных сообщений ──────────────────────────────────────────────────
public boolean isPmSoundEnabled() {
return config.getBoolean("pm.enabled", true);
}
public String getPmSound() {
return config.getString("pm.sound", "ENTITY_EXPERIENCE_ORB_PICKUP");
}
public float getPmVolume() {
return (float) config.getDouble("pm.volume", 1.0);
}
public float getPmPitch() {
return (float) config.getDouble("pm.pitch", 1.0);
}
// ── Звуки команд ────────────────────────────────────────────────────────────
public boolean isCommandSoundEnabled() {
return config.getBoolean("command.enabled", false);
}
public String getCommandSound() {
return config.getString("command.sound", "UI_BUTTON_CLICK");
}
public float getCommandVolume() {
return (float) config.getDouble("command.volume", 0.5);
}
public float getCommandPitch() {
return (float) config.getDouble("command.pitch", 1.0);
}
}

View file

@ -0,0 +1,131 @@
package com.loki.lochat.core;
import com.loki.lochat.LoChat;
import com.loki.lochat.config.ConfigManager;
import com.loki.lochat.config.MessageConfig;
import com.loki.lochat.core.filter.AdvancedMessageFilter;
import com.loki.lochat.core.registry.ServiceRegistry;
import com.loki.lochat.gradient.GradientModule;
import com.loki.lochat.integrations.DiscordIntegration;
import com.loki.lochat.integrations.LibertyBansHook;
import com.loki.lochat.integrations.PlaceholderAPIHook;
import com.loki.lochat.listener.ChatEventListener;
import com.loki.lochat.listener.DiscordEventListener;
import com.loki.lochat.listener.ModerationListener;
import com.loki.lochat.listener.PlayerEventListener;
import com.loki.lochat.managers.AutoMessageManager;
import com.loki.lochat.managers.CustomCommandManager;
import org.bukkit.Bukkit;
/**
* Инициализатор компонентов плагина
* Выносит логику инициализации из главного класса
*/
public class PluginInitializer {
private final LoChat plugin;
public PluginInitializer(LoChat plugin) {
this.plugin = plugin;
}
/**
* Инициализировать конфигурации
*/
public ConfigManager initConfigs() {
return new ConfigManager(plugin);
}
public MessageConfig initMessageConfig() {
return new MessageConfig(plugin);
}
/**
* Инициализировать Service Registry
*/
public ServiceRegistry initServiceRegistry(ConfigManager configManager, MessageConfig messageConfig) {
ServiceRegistry registry = ServiceRegistry.createWithDeps(plugin, configManager, messageConfig);
plugin.getLogger().info("ServiceRegistry инициализирован (SOLID DI)");
return registry;
}
/**
* Инициализировать менеджеры
*/
public CustomCommandManager initCustomCommandManager() {
return CustomCommandManager.create(plugin);
}
public AutoMessageManager initAutoMessageManager() {
return new AutoMessageManager(plugin);
}
/**
* Инициализировать интеграции
*/
public void initIntegrations(DiscordIntegration discordIntegration, GradientModule gradientModule) {
// LibertyBans
new LibertyBansHook(plugin);
// PlaceholderAPI
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
new PlaceholderAPIHook(plugin).register();
plugin.getLogger().info("PlaceholderAPI подключен!");
}
}
/**
* Инициализировать Discord интеграцию
*/
public DiscordIntegration initDiscordIntegration() {
return new DiscordIntegration(plugin);
}
/**
* Инициализировать Gradient модуль
*/
public GradientModule initGradientModule() {
GradientModule module = new GradientModule(plugin);
if (module.init()) {
plugin.getLogger().info("Gradient модуль (LoPreff) загружен!");
}
return module;
}
/**
* Зарегистрировать слушатели событий
*/
public AdvancedMessageFilter registerListeners(ServiceRegistry serviceRegistry, DiscordIntegration discordIntegration) {
// Создаём один экземпляр фильтра и передаём в оба листенера
AdvancedMessageFilter filter = new AdvancedMessageFilter(plugin.getConfig(), plugin);
plugin.getServer().getPluginManager().registerEvents(
new ChatEventListener(plugin, serviceRegistry, filter), plugin
);
plugin.getServer().getPluginManager().registerEvents(
new PlayerEventListener(serviceRegistry, filter), plugin
);
plugin.getServer().getPluginManager().registerEvents(
new ModerationListener(serviceRegistry), plugin
);
// Discord listener
if (discordIntegration.isEnabled()) {
plugin.getServer().getPluginManager().registerEvents(
new DiscordEventListener(discordIntegration), plugin
);
plugin.getLogger().info("Discord event listener registered");
}
plugin.getLogger().info("Слушатели зарегистрированы");
return filter;
}
/**
* Зарегистрировать plugin channels
*/
public void registerPluginChannels() {
plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, "BungeeCord");
plugin.getLogger().info("BungeeCord plugin messaging channel registered");
}
}

View file

@ -0,0 +1,93 @@
package com.loki.lochat.core;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.api.service.NickService;
import com.loki.lochat.api.service.PlayerDataService;
import com.loki.lochat.core.registry.ServiceRegistry;
import com.loki.lochat.gradient.GradientModule;
import com.loki.lochat.integrations.DiscordIntegration;
import com.loki.lochat.managers.AutoMessageManager;
import org.bukkit.event.HandlerList;
/**
* Обработчик выключения плагина
* Выносит логику shutdown из главного класса
*/
public class PluginShutdown {
private final LoChat plugin;
public PluginShutdown(LoChat plugin) {
this.plugin = plugin;
}
/**
* Выполнить полное выключение плагина
*/
public void shutdown(ServiceRegistry serviceRegistry,
AutoMessageManager autoMessageManager,
DiscordIntegration discordIntegration,
GradientModule gradientModule) {
// ✅ ПЕРВЫМ ДЕЛОМ отменяем все event listeners
HandlerList.unregisterAll(plugin);
// Останавливаем менеджеры
if (autoMessageManager != null) {
autoMessageManager.stop();
}
// Сохраняем данные через сервисы
saveAllData(serviceRegistry);
// Останавливаем интеграции
if (discordIntegration != null) {
discordIntegration.shutdown();
}
if (gradientModule != null) {
gradientModule.shutdown();
}
plugin.getLogger().info("LoChat отключен!");
}
/**
* Сохранить все данные через сервисы
*/
private void saveAllData(ServiceRegistry serviceRegistry) {
if (serviceRegistry == null) return;
// Статистика игроков
PlayerDataService playerDataService = serviceRegistry.get(PlayerDataService.class);
if (playerDataService != null) {
playerDataService.saveAll();
}
// Игнорирования
IgnoreService ignoreService = serviceRegistry.get(IgnoreService.class);
if (ignoreService != null) {
ignoreService.save();
}
// Муты
MuteService muteService = serviceRegistry.get(MuteService.class);
if (muteService != null) {
muteService.save();
}
PunishmentService punishmentService = serviceRegistry.get(PunishmentService.class);
if (punishmentService != null) {
punishmentService.save();
}
// Ники
NickService nickService = serviceRegistry.get(NickService.class);
if (nickService != null) {
nickService.save();
}
}
}

View file

@ -57,18 +57,42 @@ public class AdvancedMessageFilter {
public AdvancedMessageFilter(FileConfiguration config, JavaPlugin plugin) {
this.config = config;
this.whitelistedDomains = config.getStringList("filters.advertising.whitelist");
this.blacklistedDomains = config.getStringList("filters.advertising.blacklist");
// ✅ Используем FiltersConfig если доступен
com.loki.lochat.config.FiltersConfig filtersConfig = null;
if (plugin instanceof com.loki.lochat.LoChat loChat) {
filtersConfig = loChat.getConfigManager().getFiltersConfig();
}
// Читаем настройки из FiltersConfig или fallback на config.yml
if (filtersConfig != null) {
this.whitelistedDomains = filtersConfig.getWhitelistedDomains();
this.blacklistedDomains = filtersConfig.getBlacklistedDomains();
int maxCapsPercent = filtersConfig.getCapsMaxPercent();
int minLength = filtersConfig.getCapsMinLength();
boolean autoLower = filtersConfig.isCapsAutoLowercase();
boolean blockCaps = filtersConfig.isCapsBlock();
this.capsFilter = new CapsFilter(maxCapsPercent, minLength, autoLower, blockCaps);
this.replacementChar = filtersConfig.getSwearReplacementChar().charAt(0);
this.checkFragments = filtersConfig.isSwearCheckFragments();
this.ignoreCase = filtersConfig.isSwearIgnoreCase();
} else {
// Fallback на config.yml
this.whitelistedDomains = config.getStringList("filters.advertising.whitelist");
this.blacklistedDomains = config.getStringList("filters.advertising.blacklist");
int maxCapsPercent = config.getInt("filters.caps.max-percent", 70);
int minLength = config.getInt("filters.caps.min-length", 5);
boolean autoLower = config.getBoolean("filters.caps.auto-lowercase", true);
boolean blockCaps = config.getBoolean("filters.caps.block", false);
this.capsFilter = new CapsFilter(maxCapsPercent, minLength, autoLower, blockCaps);
int maxCapsPercent = config.getInt("filters.caps.max-percent", 70);
int minLength = config.getInt("filters.caps.min-length", 5);
boolean autoLower = config.getBoolean("filters.caps.auto-lowercase", true);
boolean blockCaps = config.getBoolean("filters.caps.block", false);
this.capsFilter = new CapsFilter(maxCapsPercent, minLength, autoLower, blockCaps);
this.replacementChar = config.getString("filters.swear.replacement-char", "*").charAt(0);
this.checkFragments = config.getBoolean("filters.swear.check-fragments", true);
this.ignoreCase = config.getBoolean("filters.swear.ignore-case", true);
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);
}

View file

@ -37,7 +37,7 @@ public class CooldownFilter implements MessageFilter {
// Получаем сообщение из конфигурации
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
String cooldownMessage = loChat.getConfigManager().getHardcodedMessages().getCooldownMessage();
String cooldownMessage = loChat.getConfigManager().getMessagesConfig().getCooldownMessage();
cooldownMessage = cooldownMessage.replace("{remaining}", String.valueOf(remaining));
player.sendMessage(cooldownMessage);

View file

@ -2,7 +2,7 @@ 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.config.MessagesConfig;
import com.loki.lochat.data.model.ChatMessage;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.entity.Player;
@ -13,13 +13,13 @@ import org.bukkit.plugin.java.JavaPlugin;
*/
public class MuteFilter implements MessageFilter {
private final MuteService muteService;
private final HardcodedMessages hardcodedMessages;
private final MessagesConfig messagesConfig;
public MuteFilter(MuteService muteService, JavaPlugin plugin) {
this.muteService = muteService;
// Получаем HardcodedMessages через LoChat
// Получаем MessagesConfig через LoChat
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
this.hardcodedMessages = loChat.getConfigManager().getHardcodedMessages();
this.messagesConfig = loChat.getConfigManager().getMessagesConfig();
}
@Override
@ -39,11 +39,11 @@ public class MuteFilter implements MessageFilter {
String msg;
if (remaining < 0) {
// Permanent mute
msg = hardcodedMessages.getMutedPermanent()
msg = messagesConfig.getMutedPermanent()
.replace("{reason}", reason);
} else {
String timeStr = muteService.formatTime(remaining);
msg = hardcodedMessages.getMutedMessage()
msg = messagesConfig.getMutedMessage()
.replace("{reason}", reason)
.replace("{time}", timeStr);
}

View file

@ -38,9 +38,11 @@ public class ServiceRegistry {
register(PMService.class, new PMServiceImpl());
register(IgnoreService.class, new IgnoreServiceImpl(plugin));
register(SpyService.class, new SpyServiceImpl(plugin, messageConfig));
SpyService spyService = new SpyServiceImpl(messageConfig);
register(SpyService.class, spyService);
register(MentionService.class, new MentionServiceImpl(configManager));
register(NickService.class, new NickServiceImpl(plugin));
register(PunishmentService.class, new PunishmentServiceImpl(plugin, configManager.getMessagesConfig()));
}
public <T> void register(Class<T> serviceClass, T implementation) {

View file

@ -41,7 +41,7 @@ public class ChatServiceImpl implements ChatService {
@Override
public void sendGlobalMessage(Player sender, Object message) {
Component msg = toComponent(message);
EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, sender, msg, true);
EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, true);
for (Player p : Bukkit.getOnlinePlayers()) {
if (!isGlobalChatDisabled(p.getUniqueId())) {
p.sendMessage(renderer.render(sender, sender.displayName(), msg, p));
@ -53,7 +53,7 @@ public class ChatServiceImpl implements ChatService {
public void sendLocalMessage(Player sender, Object message) {
int radius = plugin.getConfig().getInt("chat.local.radius", 100);
Component msg = toComponent(message);
EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, sender, msg, false);
EnhancedChatRenderer renderer = new EnhancedChatRenderer(plugin, false);
int count = 0;
for (Player p : Bukkit.getOnlinePlayers()) {

View file

@ -1,7 +1,6 @@
package com.loki.lochat.core.service;
import com.loki.lochat.api.filter.MessageFilter;
import com.loki.lochat.api.service.ChatService;
import com.loki.lochat.api.service.CooldownService;
import com.loki.lochat.api.service.MessageService;
import com.loki.lochat.api.service.MuteService;
@ -19,13 +18,9 @@ import java.util.List;
* Реализация сервиса обработки сообщений (Chain of Responsibility)
*/
public class MessageServiceImpl implements MessageService {
private final JavaPlugin plugin;
private final List<MessageFilter> filters;
private final ChatService chatService;
public MessageServiceImpl(JavaPlugin plugin, ServiceRegistry registry) {
this.plugin = plugin;
this.chatService = registry.get(ChatService.class);
this.filters = new ArrayList<>();
// Регистрируем фильтры в порядке применения

View file

@ -15,6 +15,9 @@ import java.util.concurrent.ConcurrentHashMap;
public class MuteHistoryManager {
// ✅ FIX: Ограничиваем размер истории для предотвращения утечки памяти
private static final int MAX_HISTORY_PER_PLAYER = 50;
private final Map<UUID, List<MuteData.MuteHistoryEntry>> history = new ConcurrentHashMap<>();
private final File historyFile;
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
@ -36,6 +39,11 @@ public class MuteHistoryManager {
null,
0
));
// ✅ FIX: Удаляем старые записи если превышен лимит
if (entries.size() > MAX_HISTORY_PER_PLAYER) {
entries.remove(0); // Удаляем самую старую запись
}
}
public void updateUnmute(UUID uuid, String unmutedBy) {

View file

@ -44,7 +44,7 @@ public class PlayerDataServiceImpl implements PlayerDataService {
/** Вызывать при каждом сообщении в чате */
public void recordMessage(UUID player, String chatType) {
playerMessages.merge(player, 1L, Long::sum);
playerMessages.compute(player, (k, v) -> (v == null ? 0L : v) + 1L);
totalMessages.incrementAndGet();
switch (chatType) {
case "global" -> globalChatCount.incrementAndGet();

View file

@ -0,0 +1,192 @@
package com.loki.lochat.core.service;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.config.MessagesConfig;
import com.loki.lochat.data.model.BanRecord;
import com.loki.lochat.data.model.WarnEntry;
import com.loki.lochat.util.FoliaUtil;
import com.loki.lochat.utils.ChatFormatter;
import com.loki.lochat.utils.TimeFormatter;
import net.kyori.adventure.text.Component;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.*;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Варны и баны в punishments.json
*/
public class PunishmentServiceImpl implements PunishmentService {
private final JavaPlugin plugin;
private final MessagesConfig messages;
private final File dataFile;
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
private final Map<UUID, List<WarnEntry>> warns = new ConcurrentHashMap<>();
private final Map<UUID, BanRecord> bans = new ConcurrentHashMap<>();
public PunishmentServiceImpl(JavaPlugin plugin, MessagesConfig messagesConfig) {
this.plugin = plugin;
this.messages = messagesConfig;
this.dataFile = new File(plugin.getDataFolder(), "punishments.json");
load();
}
@Override
public void addWarn(UUID uuid, String playerName, String moderator, String reason, boolean silent) {
WarnEntry entry = new WarnEntry(System.currentTimeMillis(), moderator, reason, silent);
warns.computeIfAbsent(uuid, k -> Collections.synchronizedList(new ArrayList<>())).add(entry);
saveAsync();
}
@Override
public List<WarnEntry> getWarns(UUID uuid) {
List<WarnEntry> list = warns.get(uuid);
if (list == null) return List.of();
synchronized (list) {
return List.copyOf(list);
}
}
@Override
public int getWarnCount(UUID uuid) {
List<WarnEntry> list = warns.get(uuid);
return list == null ? 0 : list.size();
}
@Override
public void ban(UUID uuid, String playerName, long durationMs, String reason, String bannedBy) {
long until = durationMs <= 0 ? 0 : System.currentTimeMillis() + durationMs;
BanRecord record = new BanRecord(uuid, playerName, reason, bannedBy, until, System.currentTimeMillis());
bans.put(uuid, record);
saveAsync();
}
@Override
public boolean unban(UUID uuid) {
BanRecord removed = bans.remove(uuid);
if (removed != null) {
saveAsync();
return true;
}
return false;
}
@Override
public boolean isBanned(UUID uuid) {
BanRecord b = bans.get(uuid);
if (b == null) return false;
if (b.isPermanent()) return true;
if (b.isExpired()) {
bans.remove(uuid, b);
saveAsync();
return false;
}
return true;
}
@Override
public BanRecord getActiveBan(UUID uuid) {
if (!isBanned(uuid)) return null;
return bans.get(uuid);
}
@Override
public Component buildBanKickMessage(BanRecord ban) {
String reason = ban.reason != null ? ban.reason : messages.getModerationDefaultReason();
String mod = ban.bannedBy != null ? ban.bannedBy : "?";
String duration;
if (ban.isPermanent()) {
duration = messages.getModerationDurationPermanent();
} else {
long left = Math.max(0, ban.until - System.currentTimeMillis());
duration = TimeFormatter.format(left);
}
String template = messages.getBanKickMessage();
String filled = template
.replace("{reason}", reason)
.replace("{moderator}", mod)
.replace("{duration}", duration)
.replace("{player}", ban.playerName != null ? ban.playerName : "?");
return ChatFormatter.parse(filled);
}
@Override
public void save() {
try {
if (!dataFile.getParentFile().exists() && !dataFile.getParentFile().mkdirs()) {
plugin.getLogger().warning("Could not create data folder for punishments");
}
PunishmentSnapshot snap = new PunishmentSnapshot();
for (Map.Entry<UUID, List<WarnEntry>> e : warns.entrySet()) {
List<WarnEntry> list = e.getValue();
synchronized (list) {
snap.warns.put(e.getKey().toString(), new ArrayList<>(list));
}
}
for (Map.Entry<UUID, BanRecord> e : bans.entrySet()) {
snap.bans.put(e.getKey().toString(), e.getValue());
}
try (Writer w = new OutputStreamWriter(new FileOutputStream(dataFile), java.nio.charset.StandardCharsets.UTF_8)) {
gson.toJson(snap, w);
}
} catch (IOException e) {
plugin.getLogger().warning("Failed to save punishments: " + e.getMessage());
}
}
@Override
public void reload() {
warns.clear();
bans.clear();
load();
}
private void saveAsync() {
FoliaUtil.runAsync(plugin, this::save);
}
private void load() {
if (!dataFile.exists()) return;
try (Reader r = new InputStreamReader(new FileInputStream(dataFile), java.nio.charset.StandardCharsets.UTF_8)) {
Type type = new TypeToken<PunishmentSnapshot>() {}.getType();
PunishmentSnapshot snap = gson.fromJson(r, type);
if (snap == null) return;
if (snap.warns != null) {
for (Map.Entry<String, List<WarnEntry>> e : snap.warns.entrySet()) {
try {
warns.put(UUID.fromString(e.getKey()), Collections.synchronizedList(new ArrayList<>(e.getValue())));
} catch (IllegalArgumentException ignored) {
}
}
}
if (snap.bans != null) {
for (Map.Entry<String, BanRecord> e : snap.bans.entrySet()) {
try {
UUID id = UUID.fromString(e.getKey());
BanRecord br = e.getValue();
if (br != null) {
br.uuid = id;
if (!br.isPermanent() && br.isExpired()) continue;
bans.put(id, br);
}
} catch (IllegalArgumentException ignored) {
}
}
}
} catch (IOException e) {
plugin.getLogger().warning("Failed to load punishments: " + e.getMessage());
}
}
private static class PunishmentSnapshot {
Map<String, List<WarnEntry>> warns = new HashMap<>();
Map<String, BanRecord> bans = new HashMap<>();
}
}

View file

@ -7,8 +7,6 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@ -17,12 +15,10 @@ import java.util.concurrent.ConcurrentHashMap;
* Реализация сервиса шпионажа за чатом
*/
public class SpyServiceImpl implements SpyService {
private final JavaPlugin plugin;
private final MessageConfig messageConfig;
private final Set<UUID> spyEnabled = ConcurrentHashMap.newKeySet();
public SpyServiceImpl(JavaPlugin plugin, MessageConfig messageConfig) {
this.plugin = plugin;
public SpyServiceImpl(MessageConfig messageConfig) {
this.messageConfig = messageConfig;
}

View file

@ -0,0 +1,37 @@
package com.loki.lochat.data.model;
import java.util.UUID;
/**
* Активная блокировка
*/
public class BanRecord {
public UUID uuid;
public String playerName;
public String reason;
public String bannedBy;
/** 0 = перманент; иначе timestamp окончания бана */
public long until;
public long createdAt;
public BanRecord() {
}
public BanRecord(UUID uuid, String playerName, String reason, String bannedBy, long until, long createdAt) {
this.uuid = uuid;
this.playerName = playerName;
this.reason = reason;
this.bannedBy = bannedBy;
this.until = until;
this.createdAt = createdAt;
}
public boolean isPermanent() {
return until == 0;
}
public boolean isExpired() {
if (isPermanent()) return false;
return System.currentTimeMillis() >= until;
}
}

View file

@ -0,0 +1,21 @@
package com.loki.lochat.data.model;
/**
* Запись о варне (для JSON)
*/
public class WarnEntry {
public long time;
public String moderator;
public String reason;
public boolean silent;
public WarnEntry() {
}
public WarnEntry(long time, String moderator, String reason, boolean silent) {
this.time = time;
this.moderator = moderator;
this.reason = reason;
this.silent = silent;
}
}

View file

@ -1,14 +1,8 @@
package com.loki.lochat.gradient.config;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
@ -19,7 +13,6 @@ public class GradientConfig {
private final JavaPlugin plugin;
private FileConfiguration config;
private FileConfiguration mainConfig;
private File configFile;
public GradientConfig(JavaPlugin plugin) {
this.plugin = plugin;

View file

@ -121,7 +121,8 @@ public final class DisplayNameUtil {
if (!data.hasPrefix() || !data.isPrefixEnabled()) return null;
GradientConfig cfg = module.getConfig();
String prefix = cfg.getPrefixFormat().replace("{prefix}", data.getPrefix()).stripTrailing();
// ✅ FIX: Убрал stripTrailing() чтобы сохранить пробел из prefix-format
String prefix = cfg.getPrefixFormat().replace("{prefix}", data.getPrefix());
// Применяем градиент только если цвета включены И есть цвета
if (data.hasColors() && data.isColorEnabled() && cfg.isGradientOnPrefix()) {

View file

@ -157,6 +157,7 @@ public final class GradientUtil {
String prefixFormat, boolean useLegacyFormat) {
if (colors == null || colors.isEmpty()) {
if (prefix != null && !prefix.isEmpty()) {
// ✅ FIX: Не используем stripTrailing() чтобы сохранить пробел из prefixFormat
return prefixFormat.replace("{prefix}", prefix) + nick;
}
return nick;
@ -167,13 +168,16 @@ public final class GradientUtil {
}
if (continuousGradient && gradientOnPrefix) {
// ✅ FIX: Сохраняем пробел между префиксом и ником
String fullText = prefixFormat.replace("{prefix}", prefix) + nick;
return applyGradient(fullText, colors, useLegacyFormat);
} else if (gradientOnPrefix) {
// ✅ FIX: Применяем градиент к префиксу с форматом (включая пробел)
String coloredPrefix = applyGradient(prefixFormat.replace("{prefix}", prefix), colors, useLegacyFormat);
String coloredNick = applyGradient(nick, colors, useLegacyFormat);
return coloredPrefix + coloredNick;
} else {
// ✅ FIX: Префикс без градиента, но с пробелом из формата
String formattedPrefix = prefixFormat.replace("{prefix}", prefix);
return formattedPrefix + applyGradient(nick, colors, useLegacyFormat);
}

View file

@ -1,7 +1,6 @@
package com.loki.lochat.integrations;
import com.loki.lochat.util.FoliaUtil;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
@ -32,9 +31,24 @@ public class DiscordIntegration {
* Загрузить конфигурацию
*/
private void loadConfig() {
File configFile = new File(plugin.getDataFolder(), "config/discord.yml");
// Создаём папку config если её нет
File configFolder = new File(plugin.getDataFolder(), "config");
if (!configFolder.exists()) {
configFolder.mkdirs();
}
File configFile = new File(configFolder, "discord.yml");
if (!configFile.exists()) {
plugin.saveResource("config/discord.yml", false);
try {
// Копируем из resources/config/discord.yml
java.io.InputStream in = plugin.getResource("config/discord.yml");
if (in != null) {
java.nio.file.Files.copy(in, configFile.toPath());
in.close();
}
} catch (Exception e) {
plugin.getLogger().warning("Failed to copy config/discord.yml: " + e.getMessage());
}
}
config = YamlConfiguration.loadConfiguration(configFile);
@ -278,4 +292,4 @@ public class DiscordIntegration {
public boolean isEnabled() {
return enabled;
}
}
}

View file

@ -10,11 +10,9 @@ import org.bukkit.entity.Player;
*/
public class LibertyBansHook {
private final LoChat plugin;
private final MuteService muteService;
public LibertyBansHook(LoChat plugin) {
this.plugin = plugin;
this.muteService = plugin.getServiceRegistry().get(MuteService.class);
// Проверяем наличие LibertyBans

View file

@ -81,4 +81,4 @@ public class SkinsRestorerHook {
// Используем стандартные сервисы для получения голов
return "https://minotar.net/helm/" + playerName + "/8.png";
}
}
}

View file

@ -33,8 +33,35 @@ public class ChatEventListener implements Listener {
// plain нужен только для фильтров — цвета игрока обрабатываем ниже отдельно
String rawMessage = PlainTextComponentSerializer.plainText().serialize(event.message());
boolean isGlobal = rawMessage.startsWith("!");
String plainMessage = isGlobal ? rawMessage.substring(1).stripLeading() : rawMessage;
// ✅ РЕЖИМ ЕДИНОГО ЧАТА: если локальный отключен, весь чат = глобальный
LoChat loChat = (LoChat) plugin;
boolean localEnabled = loChat.getConfigManager().isLocalEnabled();
boolean isGlobal;
String plainMessage;
if (localEnabled) {
// Старая логика: "!" = глобальный, без "!" = локальный
isGlobal = rawMessage.startsWith("!");
plainMessage = isGlobal ? rawMessage.substring(1).stripLeading() : rawMessage;
} else {
// Новая логика: весь чат глобальный, "!" не нужен
isGlobal = true;
plainMessage = rawMessage;
}
// ✅ Проверка: включен ли глобальный/локальный чат
if (isGlobal && !loChat.getConfigManager().isGlobalEnabled()) {
event.setCancelled(true);
sender.sendMessage(ChatFormatter.parse("&#CF6679Глобальный чат отключен!"));
return;
}
if (!isGlobal && !localEnabled) {
event.setCancelled(true);
sender.sendMessage(ChatFormatter.parse("&#CF6679Локальный чат отключен!"));
return;
}
// Мут, кулдаун
if (!messageService.processMessage(sender, plainMessage)) {
@ -52,14 +79,14 @@ public class ChatEventListener implements Listener {
String filteredMessage = filterResult.filteredMessage();
// ── Фиксируем цвет текста ──────────────────────────────────────────────
// Проблема: Paper наследует цвет от предыдущего компонента если явный не задан.
// Синий цвет в LOCAL — это цвет последней буквы градиента "LOCAL" (#4169E1),
// который просачивается в текст сообщения.
// Решение: всегда явно задаём цвет сообщения.
// • lochat.chat.colors → парсим &7 / &#RRGGBB / MiniMessage из самого текста
// • без права → берём message-color из конфига
LoChat loChat = (LoChat) plugin;
// ── Цвет текста сообщения ({message}) ─────────────────────────────────
// Если стиль не задан явно, Paper может унаследовать цвет последнего символа градиента
// префикса в строке чата (например у LOCAL последняя буква давала синеватый #4169E1) —
// из‑за этого весь текст сообщения «подтягивался» под этот тон. Намеренный цвет текста
// задаётся в appearance.yml: prefixes.global.message-color / prefixes.local.message-color (#E8E0F0 по умолчанию).
// Решение: всегда выставлять стиль тела сообщения:
// • lochat.chat.colors → MiniMessage / &# / & из текста
// • иначе → parseWithDefaultMessageColor(..., message-color) + экранирование < (см. ChatFormatter)
Component messageComponent;
if (sender.hasPermission("lochat.chat.colors")) {
@ -68,32 +95,42 @@ public class ChatEventListener implements Listener {
String defaultColor = isGlobal
? loChat.getConfigManager().getAppearanceConfig().getGlobalMessageColor()
: loChat.getConfigManager().getAppearanceConfig().getLocalMessageColor();
String hex = defaultColor.startsWith("#") ? defaultColor : "#" + defaultColor;
messageComponent = ChatFormatter.parse("<color:" + hex + ">" + filteredMessage + "</color>");
messageComponent = ChatFormatter.parseWithDefaultMessageColor(filteredMessage, defaultColor);
}
event.message(messageComponent);
// Радиус для локального чата
// ✅ FIX: Радиус для локального чата - кэшируем локации в async потоке
// Paper позволяет читать Location в async, но не модифицировать World
if (!isGlobal) {
int radius = loChat.getConfigManager().getAppearanceConfig().getLocalRadius();
event.viewers().removeIf(v ->
v instanceof Player p && !com.loki.lochat.util.DistanceUtil.isInRange(sender, p, radius)
);
// Безопасно: AsyncChatEvent.viewers() thread-safe, Location.distance() тоже
event.viewers().removeIf(v -> {
if (!(v instanceof Player p)) return false;
// Проверяем мир и расстояние (чтение Location безопасно в async)
try {
return !com.loki.lochat.util.DistanceUtil.isInRange(sender, p, radius);
} catch (Exception e) {
// На всякий случай ловим исключения
return false;
}
});
long recipients = event.viewers().stream()
.filter(v -> v instanceof Player p && !p.equals(sender))
.count();
if (recipients == 0) {
String msg = loChat.getConfigManager().getHardcodedMessages().getNobodyHeard();
String msg = loChat.getConfigManager().getMessagesConfig().getNobodyHeard();
sender.getScheduler().run(plugin, t ->
sender.sendMessage(ChatFormatter.parse(msg)), null
);
}
}
event.renderer(new EnhancedChatRenderer(plugin, sender, messageComponent, isGlobal));
event.renderer(new EnhancedChatRenderer(plugin, isGlobal));
loChat.getDiscordIntegration().sendChatMessage(sender, filteredMessage, isGlobal);

View file

@ -78,4 +78,4 @@ public class DiscordEventListener implements Listener {
discord.sendPlayerDeath(player, deathMessage);
}
}
}

View file

@ -0,0 +1,33 @@
package com.loki.lochat.listener;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.data.model.BanRecord;
import com.loki.lochat.core.registry.ServiceRegistry;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
/**
* Блокировка входа забаненным игрокам
*/
public class ModerationListener implements Listener {
private final PunishmentService punishmentService;
public ModerationListener(ServiceRegistry registry) {
this.punishmentService = registry.get(PunishmentService.class);
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPreLogin(AsyncPlayerPreLoginEvent event) {
if (!punishmentService.isBanned(event.getUniqueId())) {
return;
}
BanRecord ban = punishmentService.getActiveBan(event.getUniqueId());
if (ban == null) {
return;
}
event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED, punishmentService.buildBanKickMessage(ban));
}
}

View file

@ -3,13 +3,13 @@ package com.loki.lochat.managers;
import com.loki.lochat.LoChat;
import com.loki.lochat.util.FoliaUtil;
import com.loki.lochat.utils.ChatFormatter;
import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.scheduler.BukkitTask;
import java.io.File;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
@ -18,7 +18,9 @@ public class AutoMessageManager {
private final LoChat plugin;
private final Random random = new Random();
private final AtomicInteger currentIndex = new AtomicInteger(0);
private boolean running = false;
// ✅ FIX: Сохраняем ссылку на задачу для отмены
private Object scheduledTask; // BukkitTask или ScheduledTask
private Map<String, List<String>> messages;
private List<String> messageOrder;
@ -75,18 +77,45 @@ public class AutoMessageManager {
return;
}
// ✅ FIX: Отменяем старую задачу перед созданием новой
stop();
int intervalSeconds = plugin.getConfigManager().getAutoMessagesInterval();
long intervalTicks = intervalSeconds * 20L;
// Используем FoliaUtil для совместимости с Paper и Folia
FoliaUtil.runTimerAsync(plugin, this::broadcastNextMessage, intervalTicks, intervalTicks);
running = true;
if (FoliaUtil.isFolia()) {
long delayMs = intervalTicks * 50;
long periodMs = intervalTicks * 50;
scheduledTask = Bukkit.getAsyncScheduler().runAtFixedRate(
plugin,
task -> broadcastNextMessage(),
delayMs,
periodMs,
java.util.concurrent.TimeUnit.MILLISECONDS
);
} else {
scheduledTask = Bukkit.getScheduler().runTaskTimerAsynchronously(
plugin,
this::broadcastNextMessage,
intervalTicks,
intervalTicks
);
}
plugin.getLogger().info("Автосообщения запущены (интервал: " + intervalSeconds + " сек, режим: " + mode + ")");
}
public void stop() {
running = false;
// ✅ FIX: Отменяем scheduled task
if (scheduledTask != null) {
if (scheduledTask instanceof ScheduledTask) {
((ScheduledTask) scheduledTask).cancel();
} else if (scheduledTask instanceof BukkitTask) {
((BukkitTask) scheduledTask).cancel();
}
scheduledTask = null;
}
}
private void broadcastNextMessage() {

View file

@ -10,12 +10,9 @@ import org.bukkit.command.CommandMap;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import java.io.File;
import java.lang.reflect.Field;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
@ -318,4 +315,4 @@ public class CustomCommandManager {
public record CustomCommandData(String name, String permission, String message, List<String> aliases, String type,
String target, boolean enabled) {
}
}
}

View file

@ -25,29 +25,22 @@ public class EnhancedChatRenderer implements ChatRenderer {
private static final TextColor FALLBACK_COLOR = NamedTextColor.WHITE;
private final JavaPlugin plugin;
private final Player sender;
private final boolean isGlobal;
private final MentionHandler mentionHandler;
private final com.loki.lochat.config.AppearanceConfig cfg;
private final com.loki.lochat.config.HardcodedMessages hm;
private final com.loki.lochat.config.ConfigManager cfg;
public EnhancedChatRenderer(JavaPlugin plugin, Player sender, Component originalMessage, boolean isGlobal) {
public EnhancedChatRenderer(JavaPlugin plugin, boolean isGlobal) {
this.plugin = plugin;
this.sender = sender;
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();
this.cfg = loChat.getConfigManager();
}
@Override
public Component render(Player source, Component sourceDisplayName, Component message, Audience viewer) {
Component prefix = buildPrefix();
Component separator = buildSeparator();
Component player = buildPlayerComponent(source);
// Обрабатываем сообщение
Component processed = message;
processed = TextFormatter.formatMarkdown(processed);
processed = processUrls(processed);
@ -57,11 +50,94 @@ public class EnhancedChatRenderer implements ChatRenderer {
processed = mentionHandler.processMentions(processed, source, vp);
}
return prefix
.append(separator)
.append(player)
.append(Component.text(": ", NamedTextColor.WHITE))
.append(processed);
// ✅ Получаем кастомный формат из конфига
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
String format = isGlobal ? appearanceCfg.getGlobalChatFormat() : appearanceCfg.getLocalChatFormat();
// Строим компоненты
Component emoji = buildEmojiComponent();
Component prefix = buildPrefix();
Component separator = buildSeparator();
Component playerPrefix = buildPlayerPrefix(source);
Component player = buildPlayerComponent(source);
// Парсим формат и заменяем плейсхолдеры
return buildFormattedMessage(format, emoji, prefix, separator, playerPrefix, player, processed);
}
/**
* Строит финальное сообщение по формату с плейсхолдерами
*/
private Component buildFormattedMessage(String format, Component emoji, Component prefix,
Component separator, Component playerPrefix,
Component player, Component message) {
Component result = Component.empty();
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\{([^}]+)\\}");
java.util.regex.Matcher matcher = pattern.matcher(format);
int lastEnd = 0;
while (matcher.find()) {
// Добавляем текст до плейсхолдера
if (matcher.start() > lastEnd) {
result = result.append(Component.text(format.substring(lastEnd, matcher.start())));
}
// Заменяем плейсхолдер
String placeholder = matcher.group(1);
switch (placeholder) {
case "emoji" -> result = result.append(emoji);
case "prefix" -> result = result.append(prefix);
case "separator" -> result = result.append(separator);
case "player_prefix" -> result = result.append(playerPrefix);
case "player" -> result = result.append(player);
case "message" -> result = result.append(message);
default -> result = result.append(Component.text("{" + placeholder + "}"));
}
lastEnd = matcher.end();
}
// Добавляем оставшийся текст
if (lastEnd < format.length()) {
result = result.append(Component.text(format.substring(lastEnd)));
}
return result;
}
/**
* Строит компонент эмодзи
*/
private Component buildEmojiComponent() {
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
String emoji = isGlobal ? appearanceCfg.getGlobalEmoji() : appearanceCfg.getLocalEmoji();
return emoji.isEmpty() ? Component.empty() : Component.text(emoji);
}
/**
* Строит префикс игрока из LuckPerms/градиента
*/
private Component buildPlayerPrefix(Player player) {
// Получаем градиентный модуль
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.gradient.GradientModule gradientModule = loChat.getGradientModule();
if (gradientModule != null) {
com.loki.lochat.gradient.data.GradientPlayerData data =
gradientModule.getDataManager().getPlayerData(player.getUniqueId());
if (data != null && data.isPrefixEnabled() && data.hasPrefix()) {
String prefixText = data.getPrefix();
if (data.hasColors() && data.isColorEnabled()) {
// Применяем градиент к префиксу
return buildGradientText(prefixText, data.getColors());
}
return Component.text(prefixText);
}
}
return Component.empty();
}
// ──────────────────────────────────────────────────────────────────────────
@ -69,18 +145,13 @@ public class EnhancedChatRenderer implements ChatRenderer {
// ──────────────────────────────────────────────────────────────────────────
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();
// ✅ FIX: Получаем AppearanceConfig через ConfigManager
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
String text = isGlobal ? appearanceCfg.getGlobalText() : appearanceCfg.getLocalText();
List<String> colors = isGlobal ? appearanceCfg.getGlobalColors() : appearanceCfg.getLocalColors();
Component prefix = Component.empty();
if (!emoji.isEmpty()) {
prefix = prefix.append(Component.text(emoji + " "));
}
prefix = prefix.append(buildGradientText(text, colors));
return prefix;
return buildGradientText(text, colors);
}
/**
@ -145,8 +216,9 @@ public class EnhancedChatRenderer implements ChatRenderer {
// ──────────────────────────────────────────────────────────────────────────
private Component buildSeparator() {
String text = isGlobal ? cfg.getGlobalSeparatorText() : cfg.getLocalSeparatorText();
String color = isGlobal ? cfg.getGlobalSeparatorColor() : cfg.getLocalSeparatorColor();
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
String text = isGlobal ? appearanceCfg.getGlobalSeparatorText() : appearanceCfg.getLocalSeparatorText();
String color = isGlobal ? appearanceCfg.getGlobalSeparatorColor() : appearanceCfg.getLocalSeparatorColor();
return Component.text(text, parseColor(color));
}
@ -157,11 +229,28 @@ public class EnhancedChatRenderer implements ChatRenderer {
private Component buildPlayerComponent(Player player) {
Component displayName = player.displayName();
if (!cfg.isHoverEnabled()) {
// ✅ FIX: Получаем AppearanceConfig для проверки hover
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
if (!appearanceCfg.isHoverEnabled()) {
return displayName.clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " "));
}
List<String> hoverLines = cfg.getHoverFormat();
List<String> hoverLines = appearanceCfg.getHoverFormat();
if (hoverLines.isEmpty()) {
hoverLines = List.of(
"&#7858E9▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬",
"&#B798A8✦ &f{player}",
"",
"&#9878C9⏱ &fПинг: &#7858E9{ping}ms",
"&#9878C9♥ &fЗдоровье: &#7858E9{health}/20",
"&#9878C9🍖 &fГолод: &#7858E9{food}/20",
"",
"&#7858E9▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬",
"&#B798A8✦ &fНажми чтобы написать"
);
}
net.kyori.adventure.text.TextComponent.Builder hoverBuilder = Component.text();
for (int i = 0; i < hoverLines.size(); i++) {
@ -169,7 +258,7 @@ public class EnhancedChatRenderer implements ChatRenderer {
.replace("{player}", player.getName())
.replace("{world}", player.getWorld().getName())
.replace("{ping}", String.valueOf(player.getPing()))
.replace("{gamemode}", hm.getGamemodeName(player.getGameMode().name().toLowerCase()))
.replace("{gamemode}", player.getGameMode().name())
.replace("{health}", String.valueOf(Math.round(player.getHealth())))
.replace("{food}", String.valueOf(player.getFoodLevel()));
@ -204,8 +293,11 @@ public class EnhancedChatRenderer implements ChatRenderer {
}
private Component processEmojis(Component message) {
// ✅ FIX: Получаем AppearanceConfig для эмодзи
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
Component result = message;
for (java.util.Map.Entry<String, String> e : cfg.getEmojis().entrySet()) {
for (java.util.Map.Entry<String, String> e : appearanceCfg.getEmojis().entrySet()) {
result = result.replaceText(b -> b.matchLiteral(e.getKey()).replacement(e.getValue()));
}
return result;

View file

@ -85,4 +85,4 @@ public class AdventureManager {
public static boolean isAdventureAvailable() {
return adventureAvailable;
}
}
}

View file

@ -23,6 +23,33 @@ public final class ChatFormatter {
return MM.deserialize(converted);
}
/**
* Default message color for players without {@code lochat.chat.colors}.
* Escapes {@code \} and {@code <} in the raw text so a literal {@code </color>} (or any {@code <})
* cannot close the outer MiniMessage wrapper early — which used to leave a stray {@code </color>}
* visible in-game and in the server console log.
* <p>
* Legacy hex ({@code &#RRGGBB}) and ampersand color codes in the message are still applied after escaping
* (see {@link #convertLegacyFormats} inside {@link #parse}).
*/
public static Component parseWithDefaultMessageColor(String message, String hexCode) {
if (message == null) message = "";
String hex = (hexCode == null || hexCode.isBlank())
? "#FFFFFF"
: (hexCode.startsWith("#") ? hexCode : "#" + hexCode);
String escaped = escapeForMiniMessageOuterWrap(message);
return parse("<color:" + hex + ">" + escaped + "</color>");
}
/**
* Escape user text before it is embedded in a MiniMessage template with outer tags.
* MiniMessage treats {@code \<} as a literal angle bracket.
*/
public static String escapeForMiniMessageOuterWrap(String text) {
if (text == null) return "";
return text.replace("\\", "\\\\").replace("<", "\\<");
}
public static Component parse(String message, String... replacements) {
if (message == null) return Component.empty();
String result = message;

View file

@ -23,4 +23,4 @@ public final class Constants {
private Constants() {
}
}
}

View file

@ -17,7 +17,7 @@ public final class PlayerUtil {
/**
* Safely parses a sound name from config.
* Uses Registry API for modern sound parsing with fallback to reflection.
* Uses Paper Registry API for modern sound parsing with fallback to reflection.
*
* @param soundName the sound name to parse
* @param defaultSound the default sound to return if parsing fails
@ -29,8 +29,9 @@ public final class PlayerUtil {
}
try {
// Try to get sound by key using Registry (modern way)
Sound sound = org.bukkit.Registry.SOUNDS.match(soundName);
// ✅ FIX: Use Paper's NamespacedKey instead of deprecated match()
org.bukkit.NamespacedKey key = org.bukkit.NamespacedKey.minecraft(soundName.toLowerCase());
Sound sound = org.bukkit.Registry.SOUNDS.get(key);
if (sound != null) {
return sound;
}
@ -105,4 +106,4 @@ public final class PlayerUtil {
Player player = Bukkit.getPlayer(uuid);
return player != null && player.isOnline();
}
}
}