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

View file

@ -1,181 +1,30 @@
# ╔══════════════════════════════════════════════════════════════════════════════╗
# ║ LoChat Configuration ║
# ║ Версия: 1.5.5-1.20.1 ║
# ╚══════════════════════════════════════════════════════════════════════════════╝
# LoChat Configuration v1.5.5
config-version: 4
prefix: "&#00D4FF[LoChat]"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ДОПОЛНИТЕЛЬНЫЕ КОНФИГИ │
# └─────────────────────────────────────────────────────────────────────────────┘
# Загружать дополнительные конфигурационные файлы
load-additional-configs: true
# Список дополнительных конфигов для загрузки
additional-configs:
- "config/chat-formats.yml" # Форматы чата и сообщений
- "config/filters.yml" # Настройки фильтров
- "config/gradient-config.yml" # Конфигурация градиентов
- "config/automessages.yml" # Автосообщения
- "config/mute-config.yml" # Система мутов
- "config/sounds.yml" # Звуковые эффекты
- "config/database.yml" # База данных
- "config/integrations.yml" # Интеграции с другими плагинами
- "config/custom-commands.yml" # Кастомные команды
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ СТРУКТУРА ДАННЫХ │
# └─────────────────────────────────────────────────────────────────────────────┘
# Настройки хранения данных
data-storage:
# Основная папка для данных
data-folder: "data/"
# Использовать отдельные файлы для разных типов данных
separate-files: true
# Файлы данных
files:
main: "data.yml" # Основные данные (если separate-files: false)
players: "players.yml" # Данные игроков
gradients: "gradients.yml" # Градиентные ники
mutes: "mutes.yml" # Муты и история
ignores: "ignores.yml" # Игнорирование
statistics: "statistics.yml" # Статистика
filter-words: "filter-words.json" # Запрещенные слова
# Автосохранение (секунды)
auto-save-interval: 300
# Создавать резервные копии
backup:
enabled: true
interval: 3600 # Каждый час
max-backups: 24
folder: "backups/"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ HUB COMMAND │
# └─────────────────────────────────────────────────────────────────────────────┘
hub_command:
# Enable/disable /hub command
enabled: true
# Target server name for /hub command
# Sends player to this server via BungeeCord
server: "hub"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ КАСТОМНЫЕ КОМАНДЫ │
# └─────────────────────────────────────────────────────────────────────────────┘
custom-commands:
# Включить кастомные команды
enabled: true
# Настройки Discord команды
discord:
invite-link: "discord.gg/example"
# Настройки Website команды
website:
url: "https://example.com"
# Настройки координат
coords:
# Показывать координаты в actionbar
show-in-actionbar: true
# Время показа в actionbar (тики)
actionbar-duration: 60
# Настройки /me команды
me:
# Радиус для локального действия
radius: 100
# Цвет сообщения
color: "gray"
# Настройки звуков
sounds:
# Разрешить игрокам переключать звуки
allow-toggle: true
# Звук при включении/выключении
toggle-sound: "UI_BUTTON_CLICK"
# Настройки статистики
stats:
# Показывать статистику сервера
show-server-stats: true
# Показывать статистику игроков
show-player-stats: true
# Обновлять статистику каждые N секунд
update-interval: 300
# Настройки пинга
ping:
# Цвета для разных значений пинга
colors:
excellent: "green" # < 50ms
good: "yellow" # 50-100ms
fair: "gold" # 100-200ms
poor: "red" # > 200ms
# Настройки времени
time:
# Формат времени (12 или 24 часа)
format: 24
# Показывать дату
show-date: true
# Настройки списка игроков
list:
# Группировать по мирам
group-by-world: false
# Показывать AFK игроков
show-afk: true
# Максимум игроков в списке
max-players: 50
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ НАСТРОЙКИ ЧАТА │
# └─────────────────────────────────────────────────────────────────────────────┘
# Базовые настройки чата
# Детальные настройки (префиксы, цвета, форматы) в config/*.yml
chat:
# Режим чата:
# - Оба включены = локальный (по умолчанию) + глобальный (с "!")
# - Только global = единый чат без префиксов для всех
global:
enabled: true
cooldown: 3
prefix: "&#FFD700[G]"
local:
enabled: true
radius: 100
cooldown: 1
prefix: "&#87CEEB[L]"
pm:
enabled: true
cooldown: 2
sound: true
sound-type: "BLOCK_NOTE_BLOCK_PLING"
log: false
hover:
enabled: true
format:
- "<gray>Игрок: <white>{player}"
- "<gray>Мир: <green>{world}"
- "<gray>Пинг: <yellow>{ping}ms"
- ""
- "<yellow>▶ Клик: написать ЛС"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ГРАДИЕНТНЫЕ НИКИ │
# └─────────────────────────────────────────────────────────────────────────────┘
# Hub команда
hub_command:
enabled: true
server: "hub"
# Градиентные ники
lopreff:
enabled: true
use-gradient-name: true
@ -185,107 +34,7 @@ display:
update-display-name: true
update-tab-list: true
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ДОПОЛНИТЕЛЬНЫЕ ФУНКЦИИ │
# └─────────────────────────────────────────────────────────────────────────────┘
mentions:
enabled: true
sound: true
sound-type: "BLOCK_NOTE_BLOCK_PLING"
highlight: "&#FFFF00@{player}"
self-highlight: "&#FFD700{player}"
everyone:
enabled: true
permission: "lochat.mention.everyone"
here:
enabled: true
permission: "lochat.mention.here"
role:
enabled: true
permission: "lochat.mention.role"
automessages:
enabled: true
interval: 300
random: false
announcements:
show-title: true
title-header: "&#FF6B6BОБЪЯВЛЕНИЕ"
show-actionbar: true
title-duration: 3
clearchat:
message:
enabled: true
text: "&#FF6B6BЧат очищен администратором {player}"
head-emoji:
enabled: true
allow-player-heads: true
max-heads-per-message: 3
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ СИСТЕМА МУТОВ │
# └─────────────────────────────────────────────────────────────────────────────┘
mute:
default-reason: "Нарушение правил"
default-duration: "7d"
durations:
- "10m"
- "30m"
- "1h"
- "6h"
- "12h"
- "1d"
- "3d"
- "7d"
- "14d"
- "30d"
- "perm"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ФИЛЬТРЫ СООБЩЕНИЙ │
# └─────────────────────────────────────────────────────────────────────────────┘
filters:
caps:
enabled: true
max-percent: 70
min-length: 5
auto-lowercase: true
advertising:
enabled: true
block-hidden-urls: true
block-domains: true
whitelist:
- "youtube.com"
- "youtu.be"
- "discord.gg"
- "twitch.tv"
blacklist:
- "scam-site.com"
ip:
enabled: true
block: false
replacement: "[IP скрыт]"
repeat:
enabled: true
max-repeats: 3
# Форматы (не трогать)
formats:
announcement: "&#FF6B6B[ОБЪЯВЛЕНИЕ] <message>"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ НАСТРОЙКИ ГРАДИЕНТОВ │
# └─────────────────────────────────────────────────────────────────────────────┘
# Градиенты (детали в config/gradient.yml)
gradient:
storage-type: "file"
min-colors: 1
@ -297,66 +46,21 @@ gradient:
max-prefix-length: 16
color-cooldown: 60
prefix-cooldown: 120
# Форматирование
prefix-format: "[{prefix}] "
gradient-on-prefix: true
gradient-on-luckperms-prefix: true
continuous-gradient: true
use-legacy-rgb-format: true
presets:
fire: [ "#FF4500", "#FF6347", "#FFD700" ]
ocean: [ "#00CED1", "#1E90FF", "#4169E1" ]
forest: [ "#228B22", "#32CD32", "#90EE90" ]
sunset: [ "#FF69B4", "#FF1493", "#DC143C" ]
rainbow: [ "#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3" ]
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ АВТОСООБЩЕНИЯ │
# └─────────────────────────────────────────────────────────────────────────────┘
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ROLEPLAY НАСТРОЙКИ │
# └─────────────────────────────────────────────────────────────────────────────┘
# RP команды (детали в config/rp.yml)
rp:
# Радиус видимости RP-команд (в блоках)
radius: 100
# Кулдауны для RP-команд (в секундах, 0 = отключено)
cooldowns:
me: 3
try: 5
do: 3
roll: 2
# Форматы сообщений (поддерживают MiniMessage + цвета)
# {player} — отображаемое имя игрока
# {action} — текст действия (/me)
# {description} — текст сцены (/do)
# {result} — итог (/try)
# {max} — максимум кубика, {result} — результат (/roll)
me-format: "<gray>* {player} {action}</gray>"
try-format: "<gray>* {player} пытается {action}...</gray> {result}"
try-success: "<green><bold>[Успех!]</bold></green>"
try-fail: "<red><bold>[Провал!]</bold></red>"
do-format: "<dark_gray>[<gray>Сцена</gray>]</dark_gray> <white>{description}</white> <dark_gray>({player})</dark_gray>"
roll-format: "<gray>* {player} бросает кубик и получает </gray><yellow><bold>{result}</bold></yellow><gray> из </gray><yellow>{max}</yellow>"
automessages-list:
welcome:
- "&#FFD700Добро пожаловать на наш сервер!"
- "&#87CEEBИспользуйте /help для получения помощи"
rules:
- "&#FF6B6BСоблюдайте правила сервера!"
- "&#87CEEBПодробнее: /rules"
discord:
- "&#7289DAПрисоединяйтесь к нашему Discord!"
- "&#87CEEBСсылка: discord.gg/example"
website:
- "&#00FF88Посетите наш сайт!"
- "&#87CEEBАдрес: example.com"
# Модерация: после /lmute выполнить команду от консоли (SimpleVoiceChat: voice mute {player})
moderation:
voice-mute-console-command: ""

View file

@ -1,22 +1,20 @@
# ╔══════════════════════════════════════════════════════════════════════════════╗
# ║ LoChat Chat Appearance Config ║
# ║ Настройки внешнего вида чата ║
# ║ Цветовая гамма: Spacelegacy ║
# ║ #B798A8 → #7858E9 (розово-фиолетовый) ║
# ║ LoChat — config/appearance.yml ║
# ║ In-game chat look: prefixes, format, hover, emoji shortcuts, Discord UI ║
# ║ Palette (Spacelegacy): #B798A8 (dusty rose) → #7858E9 (violet) ║
# ╚══════════════════════════════════════════════════════════════════════════════╝
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ПРЕФИКСЫ ЧАТА │
# │ Chat prefixes (gradient label + separator + default message color) │
# └─────────────────────────────────────────────────────────────────────────────┘
#
# Градиент: ровно 2 цвета — от и до. Интерполяция плавная между ними.
# Вся гамма сервера: светлый #B798A8 (пыльная роза) → тёмный #7858E9 (фиолет)
# Gradient: use 2+ hex colors; letters are blended smoothly along the label.
# message-color: used when the player does NOT have lochat.chat.colors (see plugin.yml).
prefixes:
global:
emoji: "✦"
text: "GLOBAL"
# 2 цвета — светлый конец гаммы → тёмный
colors:
- "#B798A8"
- "#7858E9"
@ -28,7 +26,6 @@ prefixes:
local:
emoji: "◆"
text: "LOCAL"
# Для локала — чуть темнее, акцент на фиолетовом конце
colors:
- "#9878C9"
- "#7858E9"
@ -38,7 +35,28 @@ prefixes:
message-color: "#E8E0F0"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ HOVER ЭФФЕКТЫ │
# │ Rendered chat line layout │
# └─────────────────────────────────────────────────────────────────────────────┘
#
# Placeholders:
# {emoji} — prefix emoji (✦ / ◆)
# {prefix} — gradient prefix text (GLOBAL / LOCAL)
# {separator} — separator (e.g. ▶)
# {player_prefix} — LuckPerms / gradient prefix from LoPreff
# {player} — display name + hover (click to /msg)
# {message} — message body
#
# Examples:
# Default: {emoji} {prefix} {separator} {player} : {message}
# Minimal: {player_prefix} {player} ▶ {message}
# Custom: ✦ {player_prefix} {player} ▶ {message}
chat-format:
global: "{emoji} {prefix} {separator} {player} : {message}"
local: "{emoji} {prefix} {separator} {player} : {message}"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ Hover shown when pointing at the player name in chat │
# └─────────────────────────────────────────────────────────────────────────────┘
hover:
@ -55,7 +73,7 @@ hover:
- "&#B798A8✦ &fНажми чтобы написать"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ЭМОДЗИ ЗАМЕНЫ │
# │ Text shortcuts → unicode (applied to outgoing chat before URLs / mentions) │
# └─────────────────────────────────────────────────────────────────────────────┘
emojis:
@ -79,7 +97,7 @@ emojis:
":shrug:": "¯\\_(ツ)_/¯"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ НАСТРОЙКИ ЧАТА │
# │ Radius, length limits, and misc chat UI colors (plugin internals) │
# └─────────────────────────────────────────────────────────────────────────────┘
chat:
@ -95,7 +113,7 @@ chat:
info: "#A485BC"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ DISCORD ИНТЕГРАЦИЯ │
# │ Discord webhook embeds (see also config/discord.yml for webhooks) │
# └─────────────────────────────────────────────────────────────────────────────┘
discord:

View file

@ -1,255 +0,0 @@
# ╔══════════════════════════════════════════════════════════════════════════════╗
# ║ LoChat Auto Messages ║
# ║ Автосообщения ║
# ║ Версия: 1.5.5-1.20.1 ║
# ╚══════════════════════════════════════════════════════════════════════════════╝
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОСНОВНЫЕ НАСТРОЙКИ │
# └─────────────────────────────────────────────────────────────────────────────┘
settings:
# Включить автосообщения
enabled: true
# Интервал между сообщениями (секунды)
interval: 300
# Случайный порядок сообщений
random: true
# Минимальное количество игроков для отправки
min-players: 1
# Префикс для автосообщений
prefix: "&#00D4FF[Авто]"
# Отправлять в определенные миры
worlds:
- "world"
- "world_nether"
- "world_the_end"
# Тип отправки: chat, actionbar, title, bossbar
type: "chat"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ СООБЩЕНИЯ │
# └─────────────────────────────────────────────────────────────────────────────┘
messages:
welcome:
enabled: true
weight: 10 # Вес для случайного выбора (больше = чаще)
permission: "" # Пустое = для всех
worlds: [] # Пустое = во всех мирах
content:
- "&#FFD700Добро пожаловать на наш сервер!"
- "&#87CEEBИспользуйте /help для получения помощи"
- "&#6BCB77Приятной игры!"
rules:
enabled: true
weight: 8
permission: ""
worlds: []
content:
- "&#FF6B6BСоблюдайте правила сервера!"
- "&#87CEEBПодробнее: /rules"
- "&#FFA726Нарушения караются мутом или баном"
discord:
enabled: true
weight: 7
permission: ""
worlds: []
content:
- "&#7289DAПрисоединяйтесь к нашему Discord!"
- "&#87CEEBСсылка: discord.gg/example"
- "&#6BCB77Общайтесь с игроками вне игры!"
website:
enabled: true
weight: 6
permission: ""
worlds: []
content:
- "&#00FF88Посетите наш сайт!"
- "&#87CEEBАдрес: example.com"
- "&#6BCB77Карта сервера, статистика и многое другое!"
donate:
enabled: true
weight: 5
permission: ""
worlds: []
content:
- "&#FFD700Поддержите сервер донатом!"
- "&#87CEEBПолучите VIP статус и привилегии"
- "&#6BCB77Подробнее: /donate"
tips:
enabled: true
weight: 9
permission: ""
worlds: []
content:
- "&#87CEEB💡 Совет: Используйте /coords для просмотра координат"
- "&#87CEEB💡 Совет: /ignore <игрок> - игнорировать игрока"
- "&#87CEEB💡 Совет: /msg <игрок> <сообщение> - личное сообщение"
- "&#87CEEB💡 Совет: /reply <сообщение> - ответить на ЛС"
- "&#87CEEB💡 Совет: /color <цвет> - изменить цвет ника"
events:
enabled: false
weight: 3
permission: ""
worlds: []
content:
- "&#FF69B4🎉 Скоро событие на сервере!"
- "&#87CEEBСледите за новостями в Discord"
- "&#6BCB77Участвуйте и получайте призы!"
economy:
enabled: false
weight: 4
permission: ""
worlds: []
content:
- "&#FFD700💰 Зарабатывайте деньги на сервере!"
- "&#87CEEBПродавайте ресурсы в магазине"
- "&#6BCB77Команды: /shop, /sell, /balance"
pvp:
enabled: false
weight: 2
permission: ""
worlds: ["world_pvp"]
content:
- "&#FF6B6B⚔️ Вы в PvP зоне!"
- "&#FFA726Будьте осторожны с другими игроками"
- "&#87CEEBИспользуйте /spawn для выхода"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ СПЕЦИАЛЬНЫЕ СООБЩЕНИЯ │
# └─────────────────────────────────────────────────────────────────────────────┘
special:
# Сообщения для новичков (первый вход)
newbie:
enabled: true
delay: 30 # Задержка после входа (секунды)
content:
- "&#6BCB77Добро пожаловать на сервер, {player}!"
- "&#87CEEBЭто ваш первый визит. Изучите /help"
- "&#FFD700Желаем приятной игры!"
# Сообщения при входе
join:
enabled: false
delay: 5
content:
- "&#6BCB77С возвращением, {player}!"
- "&#87CEEBНе забудьте проверить новости: /news"
# Сообщения при достижениях
achievement:
enabled: false
content:
- "&#FFD700🏆 Поздравляем с достижением!"
- "&#87CEEBПродолжайте исследовать мир"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ НАСТРОЙКИ ОТОБРАЖЕНИЯ │
# └─────────────────────────────────────────────────────────────────────────────┘
display:
# Настройки для title сообщений
title:
fade-in: 10
stay: 40
fade-out: 10
title: "&#FFD700Информация"
subtitle: "{message}"
# Настройки для actionbar сообщений
actionbar:
duration: 60 # Время показа (тики)
# Настройки для bossbar сообщений
bossbar:
color: "BLUE" # BLUE, GREEN, PINK, PURPLE, RED, WHITE, YELLOW
style: "SOLID" # SOLID, SEGMENTED_6, SEGMENTED_10, SEGMENTED_12, SEGMENTED_20
duration: 100 # Время показа (тики)
# Звуковые эффекты
sound:
enabled: false
type: "BLOCK_NOTE_BLOCK_PLING"
volume: 0.5
pitch: 1.0
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ УСЛОВИЯ ОТПРАВКИ │
# └─────────────────────────────────────────────────────────────────────────────┘
conditions:
# Время суток (24-часовой формат)
time:
enabled: false
start: "09:00"
end: "23:00"
# Дни недели (1-7, где 1 = понедельник)
days:
enabled: false
allowed: [1, 2, 3, 4, 5, 6, 7]
# Исключить игроков в AFK
exclude-afk: false
# Исключить игроков в творческом режиме
exclude-creative: false
# Исключить игроков с определенными правами
exclude-permissions:
- "lochat.automessage.ignore"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ПЕРСОНАЛИЗАЦИЯ │
# └─────────────────────────────────────────────────────────────────────────────┘
personalization:
# Использовать имя игрока в сообщениях
use-player-name: true
# Адаптировать сообщения под группу игрока
group-specific: false
# Сообщения для разных групп
groups:
vip:
prefix: "&#FFD700[VIP Авто]"
messages:
- "&#FFD700Спасибо за поддержку сервера, {player}!"
- "&#87CEEBВы получаете эксклюзивные привилегии"
newbie:
prefix: "&#6BCB77[Помощь]"
messages:
- "&#6BCB77Нужна помощь, {player}? Спросите в чате!"
- "&#87CEEBИспользуйте /help для списка команд"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ СТАТИСТИКА │
# └─────────────────────────────────────────────────────────────────────────────┘
statistics:
# Вести статистику отправленных сообщений
enabled: true
# Файл для сохранения статистики
file: "plugins/LoChat/automessage-stats.yml"
# Сбрасывать статистику каждый месяц
reset-monthly: true

View file

@ -1,230 +0,0 @@
# ╔══════════════════════════════════════════════════════════════════════════════╗
# ║ LoChat Beautiful Chat Formats ║
# ║ Красивые форматы чата ║
# ║ Версия: 1.5.5-1.20.1 ║
# ╚══════════════════════════════════════════════════════════════════════════════╝
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ КРАСИВЫЕ ФОРМАТЫ ЧАТА │
# └─────────────────────────────────────────────────────────────────────────────┘
# Настройки отображения чата
chat:
# Включить красивые префиксы
beautiful_prefixes: true
# Включить hover эффекты на никах
hover:
enabled: true
# Формат hover подсказки (поддерживает MiniMessage)
format:
- "<gradient:#FFD700:#FFA500><bold>▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬</bold></gradient>"
- "<gradient:#87CEEB:#4169E1><bold>👤 Информация об игроке</bold></gradient>"
- ""
- "<gray>🏷️ Имя: <white><bold>{player}</bold>"
- "<gray>📶 Пинг: <yellow><bold>{ping}ms</bold>"
- "<gray>❤️ Здоровье: <red><bold>{health}/20</bold>"
- "<gray>🍖 Голод: <gold><bold>{food}/20</bold>"
- ""
- "<gradient:#FFD700:#FFA500><bold>▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬</bold></gradient>"
- "<yellow>💬 <bold>Клик:</bold> <gray>написать личное сообщение"
# Настройки глобального чата
global:
# Префикс глобального чата
prefix:
# Использовать эмодзи
emoji: "🌍"
# Текст с градиентом
text: "GLOBAL"
# Цвета градиента (от -> к)
gradient:
from: "#FFD700"
to: "#FF4500"
# Разделитель между префиксом и ником
separator:
text: " ▶ "
color: "#FFD700"
# Настройки локального чата
local:
# Префикс локального чата
prefix:
# Использовать эмодзи
emoji: "📍"
# Текст с градиентом
text: "LOCAL"
# Цвета градиента (от -> к)
gradient:
from: "#87CEEB"
to: "#4169E1"
# Разделитель между префиксом и ником
separator:
text: " ▶ "
color: "#87CEEB"
# Радиус локального чата (блоки)
radius: 100
# Настройки эмодзи
emojis:
# Включить обработку эмодзи
enabled: true
# Список доступных эмодзи (символы для Minecraft)
list:
# Основные
":heart:": "♥"
":star:": "★"
":check:": "✓"
":cross:": "✗"
":arrow:": "→"
":fire:": "▲"
":snowflake:": "❄"
":sun:": "☀"
":moon:": "☽"
# Minecraft
":diamond:": "◆"
":sword:": "⚔"
":pickaxe:": "⛏"
":bow:": "⌐"
":shield:": "▣"
":potion:": "⚗"
":book:": "▤"
":crown:": "♔"
":emerald:": "◈"
":gold:": "◉"
":iron:": "◎"
# Эмоции (текстовые)
":thumbsup:": "▲"
":thumbsdown:": "▼"
":clap:": "※"
":wave:": "~"
":ok:": "◯"
":thinking:": "?"
":laugh:": "xD"
":cry:": ":'("
":rage:": ">:("
":cool:": "B)"
":wink:": ";)"
# Настройки URL обработки
urls:
# Включить кликабельные ссылки
enabled: true
# Цвет ссылок
color: "#00BFFF"
# Подчеркивание
underlined: true
# Hover текст
hover_text: "🔗 Открыть ссылку: {url}"
# Настройки упоминаний (@player)
mentions:
# Включить упоминания
enabled: true
# Цвет упоминания
color: "#FFD700"
# Звук при упоминании
sound:
enabled: true
type: "BLOCK_NOTE_BLOCK_PLING"
volume: 1.0
pitch: 2.0
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ФОРМАТЫ ПО ГРУППАМ │
# └─────────────────────────────────────────────────────────────────────────────┘
groups:
owner:
priority: 1000
prefix: "&#FF0000[OWNER]"
suffix: ""
name-color: "&#FF0000"
admin:
priority: 900
prefix: "&#FF6B6B[ADMIN]"
suffix: ""
name-color: "&#FF6B6B"
moderator:
priority: 800
prefix: "&#FFA726[MOD]"
suffix: ""
name-color: "&#FFA726"
helper:
priority: 700
prefix: "&#87CEEB[HELPER]"
suffix: ""
name-color: "&#87CEEB"
vip:
priority: 600
prefix: "&#FFD700[VIP]"
suffix: ""
name-color: "&#FFD700"
premium:
priority: 500
prefix: "&#6BCB77[PREMIUM]"
suffix: ""
name-color: "&#6BCB77"
default:
priority: 0
prefix: ""
suffix: ""
name-color: "&#FFFFFF"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ СПЕЦИАЛЬНЫЕ ФОРМАТЫ │
# └─────────────────────────────────────────────────────────────────────────────┘
special:
# Формат для мутов
mute:
message: "&#FF6B6BВы замучены! Осталось: {time}"
permanent: "&#FF6B6BВы замучены навсегда!"
# Формат для упоминаний
mention:
format: "&#FFFF00@{player}"
sound: "BLOCK_NOTE_BLOCK_PLING"
volume: 1.0
pitch: 1.0
# Формат для ссылок
url:
format: "<click:open_url:'{url}'><hover:show_text:'<gray>🔗 Открыть ссылку'><u><#00BFFF>{url}</u></hover></click>"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ НАСТРОЙКИ ОТОБРАЖЕНИЯ │
# └─────────────────────────────────────────────────────────────────────────────┘
display:
# Максимальная длина сообщения
max-message-length: 256
# Показывать время в чате
show-time: false
time-format: "[HH:mm]"
# Показывать мир в чате
show-world: false
world-format: "[{world}]"
# Компактный режим (убирает лишние пробелы)
compact-mode: false
# Автоматически переносить длинные сообщения
word-wrap: true
# Цвет сообщений по умолчанию
default-message-color: "&#FFFFFF"

View file

@ -1,346 +0,0 @@
# ╔══════════════════════════════════════════════════════════════════════════════╗
# ║ LoChat Database Configuration ║
# ║ Конфигурация базы данных ║
# ║ Версия: 1.5.5-1.20.1 ║
# ╚══════════════════════════════════════════════════════════════════════════════╝
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОСНОВНЫЕ НАСТРОЙКИ │
# └─────────────────────────────────────────────────────────────────────────────┘
settings:
# Тип базы данных: file, sqlite, mysql, postgresql
type: "file"
# Префикс для таблиц
table-prefix: "lochat_"
# Автоматически создавать таблицы
auto-create-tables: true
# Автоматически обновлять структуру таблиц
auto-update-tables: true
# Кодировка для MySQL
charset: "utf8mb4"
# Collation для MySQL
collation: "utf8mb4_unicode_ci"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ФАЙЛОВОЕ ХРАНЕНИЕ │
# └─────────────────────────────────────────────────────────────────────────────┘
file:
# Папка для хранения файлов данных
data-folder: "plugins/LoChat/data/"
# Формат файлов: yaml, json
format: "yaml"
# Автоматическое сохранение (секунды)
auto-save-interval: 300
# Создавать резервные копии
backup:
enabled: true
interval: 3600 # Каждый час
max-backups: 24 # Хранить 24 копии
folder: "plugins/LoChat/backups/"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ SQLITE │
# └─────────────────────────────────────────────────────────────────────────────┘
sqlite:
# Путь к файлу базы данных
file: "plugins/LoChat/database.db"
# Настройки соединения
connection:
# Таймаут соединения (миллисекунды)
timeout: 30000
# Включить WAL режим (Write-Ahead Logging)
wal-mode: true
# Размер кэша (KB)
cache-size: 2000
# Синхронизация: OFF, NORMAL, FULL
synchronous: "NORMAL"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ MYSQL │
# └─────────────────────────────────────────────────────────────────────────────┘
mysql:
# Хост базы данных
host: "localhost"
# Порт базы данных
port: 3306
# Имя базы данных
database: "lochat"
# Имя пользователя
username: "root"
# Пароль
password: "password"
# Дополнительные параметры подключения
properties:
useSSL: false
allowPublicKeyRetrieval: true
serverTimezone: "UTC"
characterEncoding: "utf8"
useUnicode: true
autoReconnect: true
failOverReadOnly: false
maxReconnects: 3
initialTimeout: 1
# Настройки пула соединений (HikariCP)
pool:
# Максимальное количество соединений
maximum-pool-size: 10
# Минимальное количество простаивающих соединений
minimum-idle: 2
# Таймаут получения соединения (миллисекунды)
connection-timeout: 30000
# Время жизни простаивающего соединения (миллисекунды)
idle-timeout: 600000
# Максимальное время жизни соединения (миллисекунды)
max-lifetime: 1800000
# Интервал проверки соединений (миллисекунды)
leak-detection-threshold: 60000
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ POSTGRESQL │
# └─────────────────────────────────────────────────────────────────────────────┘
postgresql:
# Хост базы данных
host: "localhost"
# Порт базы данных
port: 5432
# Имя базы данных
database: "lochat"
# Имя пользователя
username: "postgres"
# Пароль
password: "password"
# Схема базы данных
schema: "public"
# Дополнительные параметры подключения
properties:
ssl: false
sslmode: "disable"
serverTimezone: "UTC"
characterEncoding: "utf8"
autoReconnect: true
# Настройки пула соединений
pool:
maximum-pool-size: 10
minimum-idle: 2
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ СТРУКТУРА ТАБЛИЦ │
# └─────────────────────────────────────────────────────────────────────────────┘
tables:
# Таблица игроков
players:
name: "players"
columns:
uuid: "VARCHAR(36) PRIMARY KEY"
name: "VARCHAR(16) NOT NULL"
display_name: "TEXT"
first_join: "BIGINT NOT NULL"
last_join: "BIGINT NOT NULL"
last_quit: "BIGINT"
total_playtime: "BIGINT DEFAULT 0"
created_at: "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
updated_at: "TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
# Таблица градиентов
gradients:
name: "gradients"
columns:
uuid: "VARCHAR(36) PRIMARY KEY"
colors: "TEXT"
prefix: "TEXT"
enabled: "BOOLEAN DEFAULT TRUE"
created_at: "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
updated_at: "TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
# Таблица мутов
mutes:
name: "mutes"
columns:
id: "INT AUTO_INCREMENT PRIMARY KEY"
uuid: "VARCHAR(36) NOT NULL"
moderator_uuid: "VARCHAR(36)"
reason: "TEXT NOT NULL"
start_time: "BIGINT NOT NULL"
end_time: "BIGINT"
active: "BOOLEAN DEFAULT TRUE"
silent: "BOOLEAN DEFAULT FALSE"
created_at: "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
# История мутов
mute_history:
name: "mute_history"
columns:
id: "INT AUTO_INCREMENT PRIMARY KEY"
uuid: "VARCHAR(36) NOT NULL"
moderator_uuid: "VARCHAR(36)"
reason: "TEXT NOT NULL"
duration: "BIGINT NOT NULL"
start_time: "BIGINT NOT NULL"
end_time: "BIGINT"
unmute_reason: "TEXT"
unmute_moderator: "VARCHAR(36)"
created_at: "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
# Таблица игнорирования
ignores:
name: "ignores"
columns:
id: "INT AUTO_INCREMENT PRIMARY KEY"
player_uuid: "VARCHAR(36) NOT NULL"
ignored_uuid: "VARCHAR(36) NOT NULL"
created_at: "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
# Таблица сообщений (если включено логирование)
messages:
name: "messages"
columns:
id: "INT AUTO_INCREMENT PRIMARY KEY"
uuid: "VARCHAR(36) NOT NULL"
type: "VARCHAR(20) NOT NULL"
content: "TEXT NOT NULL"
server: "VARCHAR(50)"
world: "VARCHAR(50)"
created_at: "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
# Настройки игроков
player_settings:
name: "player_settings"
columns:
uuid: "VARCHAR(36) PRIMARY KEY"
sounds_enabled: "BOOLEAN DEFAULT TRUE"
sound_volume: "FLOAT DEFAULT 1.0"
auto_messages: "BOOLEAN DEFAULT TRUE"
mentions_enabled: "BOOLEAN DEFAULT TRUE"
pm_sounds: "BOOLEAN DEFAULT TRUE"
created_at: "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
updated_at: "TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ИНДЕКСЫ │
# └─────────────────────────────────────────────────────────────────────────────┘
indexes:
# Индексы для оптимизации запросов
players:
- "CREATE INDEX idx_players_name ON {prefix}players(name)"
- "CREATE INDEX idx_players_last_join ON {prefix}players(last_join)"
mutes:
- "CREATE INDEX idx_mutes_uuid ON {prefix}mutes(uuid)"
- "CREATE INDEX idx_mutes_active ON {prefix}mutes(active)"
- "CREATE INDEX idx_mutes_end_time ON {prefix}mutes(end_time)"
mute_history:
- "CREATE INDEX idx_mute_history_uuid ON {prefix}mute_history(uuid)"
- "CREATE INDEX idx_mute_history_moderator ON {prefix}mute_history(moderator_uuid)"
- "CREATE INDEX idx_mute_history_start_time ON {prefix}mute_history(start_time)"
ignores:
- "CREATE INDEX idx_ignores_player ON {prefix}ignores(player_uuid)"
- "CREATE INDEX idx_ignores_ignored ON {prefix}ignores(ignored_uuid)"
- "CREATE UNIQUE INDEX idx_ignores_unique ON {prefix}ignores(player_uuid, ignored_uuid)"
messages:
- "CREATE INDEX idx_messages_uuid ON {prefix}messages(uuid)"
- "CREATE INDEX idx_messages_type ON {prefix}messages(type)"
- "CREATE INDEX idx_messages_created_at ON {prefix}messages(created_at)"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОЧИСТКА ДАННЫХ │
# └─────────────────────────────────────────────────────────────────────────────┘
cleanup:
# Автоматическая очистка старых данных
enabled: true
# Интервал очистки (секунды)
interval: 86400 # Раз в день
# Настройки очистки для каждой таблицы
tables:
mute_history:
# Удалять записи старше N дней
max-age-days: 90
# Оставлять минимум записей на игрока
min-records-per-player: 5
messages:
max-age-days: 30
# Максимальное количество записей
max-records: 100000
players:
# Удалять неактивных игроков (дни)
inactive-days: 365
# Не удалять игроков с определенными данными
keep-with-data: true
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ МИГРАЦИИ │
# └─────────────────────────────────────────────────────────────────────────────┘
migrations:
# Автоматически выполнять миграции
auto-migrate: true
# Создавать резервные копии перед миграцией
backup-before-migrate: true
# Версия схемы базы данных
schema-version: 1
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОТЛАДКА │
# └─────────────────────────────────────────────────────────────────────────────┘
debug:
# Логировать SQL запросы
log-queries: false
# Логировать медленные запросы (миллисекунды)
slow-query-threshold: 1000
# Показывать статистику соединений
show-connection-stats: false
# Интервал вывода статистики (секунды)
stats-interval: 300

View file

@ -1,243 +0,0 @@
# ╔══════════════════════════════════════════════════════════════════════════════╗
# ║ LoChat Gradient Configuration ║
# ║ Конфигурация градиентов ║
# ║ Версия: 1.5.5-1.20.1 ║
# ╚══════════════════════════════════════════════════════════════════════════════╝
#
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ СИСТЕМА ОПЛАТЫ │
# └─────────────────────────────────────────────────────────────────────────────┘
#
# Есть два способа контролировать доступ к градиентам:
#
# 1. ЧЕРЕЗ PLAYERPOINTS (по умолчанию):
# - Установите use-permission-instead-of-cost: false
# - Игроки платят поинтами за установку градиентов
# - price-per-color: стоимость за каждый цвет
# - prefix-price: стоимость префикса
# - Игроки с правом "gradient.bypass.cost" могут использовать бесплатно
#
# 2. ЧЕРЕЗ ПЕРМИШЕНЫ (бесплатно):
# - Установите use-permission-instead-of-cost: true
# - Установите price-per-color: 0 и prefix-price: 0
# - Выдайте игрокам право "gradient.bypass.cost" для бесплатного использования
# - Или выдайте "gradient.color" для доступа к командам
#
# ОСНОВНЫЕ ПЕРМИШЕНЫ:
# - gradient.color - доступ к команде /color
# - gradient.prefix - доступ к команде /prefix
# - gradient.bypass.cost - бесплатное использование (обход стоимости)
# - gradient.bypass.cooldown - обход кулдауна
# - gradient.admin - админские команды
#
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОСНОВНЫЕ НАСТРОЙКИ │
# └─────────────────────────────────────────────────────────────────────────────┘
gradient:
# Тип хранения данных: file, mysql, sqlite
storage-type: "file"
# Минимальное количество цветов в градиенте
min-colors: 1
# Максимальное количество цветов в градиенте
max-colors: 7
# Стоимость за каждый цвет (PlayerPoints)
# Установите 0 для бесплатного использования
price-per-color: 50
# Стоимость префикса
# Установите 0 для бесплатного использования
prefix-price: 500
# Одноразовая покупка префикса (true) или за каждое изменение (false)
prefix-one-time-purchase: true
# Использовать пермишены вместо поинтов
# Если true, игроки с правом gradient.bypass.cost могут использовать бесплатно
# Если false, используется система PlayerPoints
use-permission-instead-of-cost: false
# Максимальная длина префикса
max-prefix-length: 16
# Кулдаун на смену цвета (секунды)
color-cooldown: 60
# Кулдаун на смену префикса (секунды)
prefix-cooldown: 120
# Автоматически применять градиент к нику
auto-apply-gradient: true
# Обновлять отображаемое имя
update-display-name: true
# Обновлять таб-лист
update-tab-list: true
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ГОТОВЫЕ ПРЕСЕТЫ │
# └─────────────────────────────────────────────────────────────────────────────┘
presets:
fire:
name: "Огонь"
colors: [ "#FF4500", "#FF6347", "#FFD700" ]
permission: "gradient.preset.fire"
price: 100
ocean:
name: "Океан"
colors: [ "#00CED1", "#1E90FF", "#4169E1" ]
permission: "gradient.preset.ocean"
price: 100
forest:
name: "Лес"
colors: [ "#228B22", "#32CD32", "#90EE90" ]
permission: "gradient.preset.forest"
price: 100
sunset:
name: "Закат"
colors: [ "#FF69B4", "#FF1493", "#DC143C" ]
permission: "gradient.preset.sunset"
price: 150
rainbow:
name: "Радуга"
colors: [ "#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3" ]
permission: "gradient.preset.rainbow"
price: 300
gold:
name: "Золото"
colors: [ "#FFD700", "#FFA500", "#FF8C00" ]
permission: "gradient.preset.gold"
price: 200
silver:
name: "Серебро"
colors: [ "#C0C0C0", "#A9A9A9", "#808080" ]
permission: "gradient.preset.silver"
price: 150
purple:
name: "Фиолетовый"
colors: [ "#9400D3", "#8A2BE2", "#DA70D6" ]
permission: "gradient.preset.purple"
price: 120
ice:
name: "Лёд"
colors: [ "#B0E0E6", "#87CEEB", "#4682B4" ]
permission: "gradient.preset.ice"
price: 120
lava:
name: "Лава"
colors: [ "#DC143C", "#B22222", "#8B0000" ]
permission: "gradient.preset.lava"
price: 150
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ НАСТРОЙКИ БАЗЫ ДАННЫХ │
# └─────────────────────────────────────────────────────────────────────────────┘
database:
mysql:
host: "localhost"
port: 3306
database: "lochat"
username: "root"
password: "password"
table-prefix: "lochat_"
# Настройки пула соединений
pool:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
sqlite:
file: "plugins/LoChat/gradient-data.db"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ДОПОЛНИТЕЛЬНЫЕ ФУНКЦИИ │
# └─────────────────────────────────────────────────────────────────────────────┘
features:
# Анимированные градиенты
animated-gradients:
enabled: false
speed: 1000 # миллисекунды
# Случайные градиенты
random-gradients:
enabled: true
change-interval: 3600 # секунды (1 час)
# Градиенты в чате
chat-gradients:
enabled: true
apply-to-messages: true
# Градиенты в табе
tab-gradients:
enabled: true
update-interval: 20 # тики
# Сохранение градиентов при выходе
save-on-quit: true
# Восстановление градиентов при входе
restore-on-join: true
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ПРАВА ДОСТУПА │
# └─────────────────────────────────────────────────────────────────────────────┘
permissions:
# Базовые права
use-gradient: "gradient.use" # Право использовать градиенты
use-color: "gradient.color" # Право использовать команду /color
use-prefix: "gradient.prefix" # Право использовать команду /prefix
admin: "gradient.admin" # Админские права
# Обход ограничений
bypass-cooldown: "gradient.bypass.cooldown" # Обход кулдауна
bypass-cost: "gradient.bypass.cost" # Бесплатное использование (обход стоимости)
bypass-length: "gradient.bypass.length" # Обход ограничения длины префикса
# Дополнительные цвета
# Игроки с этими правами могут использовать больше цветов
extra-colors:
"gradient.colors.5": 5
"gradient.colors.10": 10
"gradient.colors.15": 15
"gradient.colors.unlimited": 999
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ФОРМАТИРОВАНИЕ │
# └─────────────────────────────────────────────────────────────────────────────┘
formatting:
# Формат ника с градиентом
nick-format: "{gradient}{nick}"
# Формат префикса с градиентом
prefix-format: "{gradient}{prefix}"
# Формат в табе
tab-format: "{prefix} {gradient}{nick}"
# Формат в чате
chat-format: "{prefix} {gradient}{nick}&r: {message}"
# Разделитель между префиксом и ником
separator: " "

View file

@ -1,380 +0,0 @@
# ╔══════════════════════════════════════════════════════════════════════════════╗
# ║ LoChat Integrations ║
# ║ Интеграции ║
# ║ Версия: 1.5.5-1.20.1 ║
# ╚══════════════════════════════════════════════════════════════════════════════╝
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ PLACEHOLDERAPI │
# └─────────────────────────────────────────────────────────────────────────────┘
placeholderapi:
# Включить интеграцию с PlaceholderAPI
enabled: true
# Регистрировать собственные плейсхолдеры
register-placeholders: true
# Доступные плейсхолдеры LoChat:
# %lochat_player_muted% - замучен ли игрок (true/false)
# %lochat_player_mute_time% - оставшееся время мута
# %lochat_player_mute_reason% - причина мута
# %lochat_player_gradient% - градиентный ник
# %lochat_player_prefix% - кастомный префикс
# %lochat_player_ignored_count% - количество игнорируемых игроков
# %lochat_player_messages_sent% - отправлено сообщений
# %lochat_player_last_message% - последнее сообщение
# %lochat_server_muted_count% - количество замученных игроков
# %lochat_server_messages_today% - сообщений сегодня
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ LUCKPERMS │
# └─────────────────────────────────────────────────────────────────────────────┘
luckperms:
# Включить интеграцию с LuckPerms
enabled: true
# Использовать префиксы из LuckPerms
use-prefix: true
# Использовать суффиксы из LuckPerms
use-suffix: true
# Использовать мета-данные из LuckPerms
use-meta: true
# Приоритет групп для форматирования чата
group-priority: true
# Кэшировать данные LuckPerms
cache-data: true
# Время кэширования (секунды)
cache-duration: 300
# Синхронизировать изменения прав
sync-changes: true
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ VAULT │
# └─────────────────────────────────────────────────────────────────────────────┘
vault:
# Включить интеграцию с Vault
enabled: true
# Использовать Vault для прав
use-permissions: true
# Использовать Vault для экономики
use-economy: true
# Использовать Vault для чата (префиксы/суффиксы)
use-chat: true
# Приоритет Vault над другими системами
priority: 1
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ PLAYERPOINTS │
# └─────────────────────────────────────────────────────────────────────────────┘
playerpoints:
# Включить интеграцию с PlayerPoints
enabled: true
# Использовать поинты для покупки градиентов
use-for-gradients: true
# Использовать поинты для покупки префиксов
use-for-prefixes: true
# Автоматически списывать поинты
auto-deduct: true
# Уведомлять о списании поинтов
notify-deduction: true
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ LIBERTYBANS │
# └─────────────────────────────────────────────────────────────────────────────┘
libertybans:
# Включить интеграцию с LibertyBans
enabled: false
# Синхронизировать муты с LibertyBans
sync-mutes: true
# Использовать LibertyBans для мутов вместо встроенной системы
use-for-mutes: false
# Импортировать существующие муты из LibertyBans
import-existing: false
# Экспортировать муты в LibertyBans
export-mutes: true
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ SKINSRESTORER │
# └─────────────────────────────────────────────────────────────────────────────┘
skinsrestorer:
# Включить интеграцию с SkinsRestorer
enabled: true
# Обновлять отображение при смене скина
update-on-skin-change: true
# Использовать скины в head emoji
use-in-head-emoji: true
# Кэшировать данные скинов
cache-skins: true
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ DISCORDSRV │
# └─────────────────────────────────────────────────────────────────────────────┘
discordsrv:
# Включить интеграцию с DiscordSRV
enabled: false
# Отправлять сообщения чата в Discord
send-chat-to-discord: true
# Отправлять сообщения из Discord в игру
send-discord-to-chat: true
# Канал для глобального чата
global-chat-channel: "global-chat"
# Канал для локального чата
local-chat-channel: "local-chat"
# Канал для объявлений
announcement-channel: "announcements"
# Формат сообщений в Discord
discord-format: "**{player}**: {message}"
# Формат сообщений из Discord
minecraft-format: "&7[Discord] &b{user}&7: {message}"
# Использовать градиентные ники в Discord
use-gradients-in-discord: false
# Webhook для отправки сообщений
webhook:
enabled: false
url: ""
username: "LoChat"
avatar-url: ""
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ DYNMAP │
# └─────────────────────────────────────────────────────────────────────────────┘
dynmap:
# Включить интеграцию с Dynmap
enabled: false
# Отправлять сообщения чата на веб-карту
send-chat-to-web: true
# Отправлять сообщения с веб-карты в игру
send-web-to-chat: true
# Формат сообщений на веб-карте
web-format: "{player}: {message}"
# Формат сообщений с веб-карты
minecraft-format: "&7[Web] &a{player}&7: {message}"
# Использовать градиентные ники на веб-карте
use-gradients-on-web: false
# Фильтровать сообщения для веб-карты
filter-messages: true
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ BLUEMAP │
# └─────────────────────────────────────────────────────────────────────────────┘
bluemap:
# Включить интеграцию с BlueMap
enabled: false
# Отправлять сообщения чата на карту
send-chat-to-map: true
# Формат сообщений на карте
map-format: "{player}: {message}"
# Использовать градиентные ники на карте
use-gradients-on-map: false
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ PLAN │
# └─────────────────────────────────────────────────────────────────────────────┘
plan:
# Включить интеграцию с Plan
enabled: false
# Отправлять статистику чата в Plan
send-chat-stats: true
# Отправлять статистику мутов в Plan
send-mute-stats: true
# Интервал отправки статистики (секунды)
stats-interval: 300
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ COREPROTECT │
# └─────────────────────────────────────────────────────────────────────────────┘
coreprotect:
# Включить интеграцию с CoreProtect
enabled: false
# Логировать сообщения чата
log-chat-messages: true
# Логировать команды
log-commands: true
# Логировать муты
log-mutes: true
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ AUTHME │
# └─────────────────────────────────────────────────────────────────────────────┘
authme:
# Включить интеграцию с AuthMe
enabled: false
# Блокировать чат для неавторизованных игроков
block-chat-for-unregistered: true
# Сообщение для неавторизованных игроков
unregistered-message: "&#FF6B6BВойдите в аккаунт для использования чата!"
# Разрешенные команды для неавторизованных
allowed-commands:
- "/login"
- "/register"
- "/l"
- "/reg"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ TOWNY │
# └─────────────────────────────────────────────────────────────────────────────┘
towny:
# Включить интеграцию с Towny
enabled: false
# Чат города
town-chat:
enabled: true
format: "&6[Town] {prefix}{player}&r: {message}"
command: "/tc"
# Чат нации
nation-chat:
enabled: true
format: "&5[Nation] {prefix}{player}&r: {message}"
command: "/nc"
# Показывать город в чате
show-town-in-chat: true
# Формат города в чате
town-format: "&7[{town}]"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ FACTIONS │
# └─────────────────────────────────────────────────────────────────────────────┘
factions:
# Включить интеграцию с Factions
enabled: false
# Чат фракции
faction-chat:
enabled: true
format: "&c[Faction] {prefix}{player}&r: {message}"
command: "/fc"
# Чат альянса
alliance-chat:
enabled: true
format: "&a[Alliance] {prefix}{player}&r: {message}"
command: "/ac"
# Показывать фракцию в чате
show-faction-in-chat: true
# Формат фракции в чате
faction-format: "&7[{faction}]"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ MULTIVERSE │
# └─────────────────────────────────────────────────────────────────────────────┘
multiverse:
# Включить интеграцию с Multiverse
enabled: false
# Разделять чат по мирам
separate-chat-by-world: false
# Показывать мир в чате
show-world-in-chat: true
# Формат мира в чате
world-format: "&7[{world}]"
# Настройки чата для конкретных миров
world-settings:
world_nether:
chat-color: "&c"
prefix: "&4[Nether]"
world_the_end:
chat-color: "&5"
prefix: "&d[End]"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОБЩИЕ НАСТРОЙКИ │
# └─────────────────────────────────────────────────────────────────────────────┘
general:
# Автоматически определять доступные плагины
auto-detect-plugins: true
# Интервал проверки плагинов (секунды)
plugin-check-interval: 60
# Логировать интеграции
log-integrations: true
# Уведомлять о подключении/отключении интеграций
notify-integration-changes: true
# Приоритет интеграций (чем выше число, тем выше приоритет)
integration-priority:
luckperms: 100
vault: 90
placeholderapi: 80
playerpoints: 70
libertybans: 60
skinsrestorer: 50
discordsrv: 40
dynmap: 30
plan: 20
coreprotect: 10

View file

@ -116,3 +116,22 @@ general:
disabled: "&#B798A8Отключено"
online: "&#9878C9В сети"
offline: "&#B798A8Не в сети"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ВАРНЫ / БАНЫ (LoChat) │
# └─────────────────────────────────────────────────────────────────────────────┘
moderation:
default_reason: "Без причины"
duration_permanent: "навсегда"
warn_received: "&#CF6679⚠ Вам выдано предупреждение! &#B798A8Причина: &#7858E9{reason}"
warn_staff_confirm: "&#9878C9Игроку &#7858E9{player} &#9878C9выдан варн. &#B798A8Причина: &#7858E9{reason}"
silent_warn_staff: "&#B798A8[&#9878C9ТИХИЙ ВАРН&#B798A8] &#7858E9{player}&#9878C9: &#B798A8{reason}"
ban_usage: "&#B798A8Использование: &#7858E9/lban <ник> [время|perm] [причина]"
ban_confirm: "&#9878C9Игрок &#7858E9{player} &#9878C9забанен. &#B798A8Срок: &#7858E9{duration}"
unban_confirm: "&#9878C9Бан снят с &#7858E9{player}"
not_banned: "&#CF6679Игрок не забанен!"
already_banned: "&#CF6679Игрок уже забанен!"
warn_usage: "&#B798A8Использование: &#7858E9/warn <ник> [причина]"
silent_warn_usage: "&#B798A8Использование: &#7858E9/silentwarn <ник> [причина]"
ban_kick_message: "<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>"

View file

@ -1,20 +1,19 @@
# ╔══════════════════════════════════════════════════════════════════════════════╗
# ║ LoChat Messages ║
# ║ Цветовая гамма: Spacelegacy ║
# ║ #B798A8 (роза) → #7858E9 (фиолет) ║
# ║ LoChat — messages.yml ║
# ║ Default palette (Spacelegacy): #B798A8 (dusty rose) → #7858E9 (violet) ║
# ╚══════════════════════════════════════════════════════════════════════════════╝
# Роли цветов:
# &#B798A8 — основной текст / лейблы (пыльная роза, светлый)
# &#9878C9 — акцент / разделители (средний фиолет)
# &#7858E9 — важные данные / значения (насыщенный фиолет, тёмный)
# &f / #E8E0F0 — белый текст сообщений
# &#CF6679 — ошибки (приглушённый красный, вписывается в гамму)
#
# Color roles (reference when editing strings below):
# &#B798A8 — primary labels / body (dusty rose)
# &#9878C9 — accents / separators (mid violet)
# &#7858E9 — emphasis / values (deep violet)
# &f / #E8E0F0 — message body white
# &#CF6679 — errors (muted red, fits the palette)
prefix: "&#B798A8[&#9878C9L&#8B6BD6o&#7858E9C&#8565DCh&#8B6BD6a&#9878C9t&#B798A8]"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ГЛОБАЛЬНЫЙ ЧАТ │
# │ Global chat (toggle messages, server-disabled notice) │
# └─────────────────────────────────────────────────────────────────────────────┘
global:
@ -23,7 +22,7 @@ global:
disabled: "&#CF6679Глобальный чат отключён на этом сервере"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ЛОКАЛЬНЫЙ ЧАТ │
# │ Local chat (no recipients nearby) │
# └─────────────────────────────────────────────────────────────────────────────┘
local:
@ -31,7 +30,7 @@ local:
nobody-heard: "&#9878C9Вас никто не услышал &#B798A8— рядом нет игроков"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ЛИЧНЫЕ СООБЩЕНИЯ │
# │ Private messages (/msg, /reply) │
# └─────────────────────────────────────────────────────────────────────────────┘
pm:
@ -46,7 +45,7 @@ pm:
self: "&#CF6679Нельзя писать самому себе"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ИГНОРИРОВАНИЕ │
# │ Ignore list │
# └─────────────────────────────────────────────────────────────────────────────┘
ignore:
@ -60,14 +59,14 @@ ignore:
list-footer: "&#B798A8Всего: &#7858E9{count} &#B798A8игроков"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОБЪЯВЛЕНИЯ │
# │ Announcements │
# └─────────────────────────────────────────────────────────────────────────────┘
announce:
reloaded: "&#9878C9Конфигурация &#7858E9перезагружена"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ШПИОН РЕЖИМ │
# │ Chat spy (moderation) │
# └─────────────────────────────────────────────────────────────────────────────┘
spy:
@ -77,7 +76,7 @@ spy:
chat-format: "&#B798A8[&#9878C9SPY&#B798A8] &#B798A8{type} &#7858E9{sender}&#B798A8: &f{message}"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ФИЛЬТРЫ │
# │ Message filters │
# └─────────────────────────────────────────────────────────────────────────────┘
filter:
@ -85,14 +84,14 @@ filter:
warning: "&#B798A8Предупреждение"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ КУЛДАУНЫ │
# │ Cooldowns │
# └─────────────────────────────────────────────────────────────────────────────┘
cooldown:
wait: "&#B798A8Подождите &#7858E9{time} &#B798A8сек"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ АНТИ-СПАМ │
# │ Anti-spam (caps, repeat, similar messages) │
# └─────────────────────────────────────────────────────────────────────────────┘
antispam:
@ -101,14 +100,14 @@ antispam:
similar: "&#CF6679Не отправляйте одинаковые сообщения"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОЧИСТКА ЧАТА │
# │ Clear chat │
# └─────────────────────────────────────────────────────────────────────────────┘
clearchat:
cleared: "&#9878C9Чат очищен &#B798A8администратором &#7858E9{player}"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОШИБКИ │
# │ Command / usage errors │
# └─────────────────────────────────────────────────────────────────────────────┘
errors:
@ -117,7 +116,7 @@ errors:
invalid-usage: "&#B798A8Использование: &#7858E9{usage}"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОБЩИЕ СООБЩЕНИЯ │
# │ General (permissions, reload, hub, etc.) │
# └─────────────────────────────────────────────────────────────────────────────┘
general:
@ -132,7 +131,7 @@ hub:
connecting: "&#9878C9Подключение к &#7858E9{server}&#9878C9..."
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ СИСТЕМА МУТОВ │
# │ Mutes │
# └─────────────────────────────────────────────────────────────────────────────┘
mute:
@ -144,7 +143,7 @@ mute:
permanent: "&#CF6679Вы замучены навсегда!"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ГРАДИЕНТНЫЕ НИКИ │
# │ Gradient nick module (LoPreff) │
# └─────────────────────────────────────────────────────────────────────────────┘
gradient:
@ -191,7 +190,7 @@ gradient:
info-prefix-purchased: "&#B798A8Куплен: &f{purchased}"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ КАСТОМНЫЕ НИКИ │
# │ Custom nicknames (/nick) │
# └─────────────────────────────────────────────────────────────────────────────┘
nick:
@ -211,7 +210,7 @@ nick:
error: "&#CF6679Не удалось установить ник!"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ROLEPLAY КОМАНДЫ │
# │ Roleplay commands (/me, /try, /do, /roll) │
# └─────────────────────────────────────────────────────────────────────────────┘
rp:

View file

@ -36,7 +36,7 @@ commands:
description: Send private message
usage: /msg <player> <message>
permission: chat.pm.use
aliases: [ tell, w, pm, m, ьып, ееддж, цж, зьж, ьж ]
aliases: [ tell, pm, m, ьып, ееддж, цж, зьж, ьж ]
reply:
description: Reply to last message
@ -146,6 +146,36 @@ commands:
permission: lochat.muteblame
aliases: [ muteblame, мутблейм, ьгеуидфьу ]
warn:
description: Warn a player (visible to them)
usage: /warn <player> [reason]
permission: lochat.warn
aliases: [ w, варн ]
silentwarn:
description: Silent warn (logged; staff with lochat.warn.notify see)
usage: /silentwarn <player> [reason]
permission: lochat.warn.silent
aliases: [ siw ]
lban:
description: Ban a player
usage: /lban <player> [time|perm] [reason]
permission: lochat.ban
aliases: [ ban, бан ]
lunban:
description: Unban a player
usage: /lunban <player>
permission: lochat.unban
aliases: [ unban, разбан ]
playerinfo:
description: Player info and moderation stats
usage: /playerinfo <player>
permission: lochat.playerinfo
aliases: [ pi, инфо ]
# ═══════════════════════════════════════════════════════════════════════════════
# КАСТОМНЫЕ КОМАНДЫ
# ═══════════════════════════════════════════════════════════════════════════════
@ -284,6 +314,34 @@ permissions:
lochat.mute.dur.perm:
description: Can mute permanently
default: op
lochat.warn:
description: Issue a visible warning
default: op
lochat.warn.silent:
description: Issue a silent warning
default: op
lochat.warn.notify:
description: See silent warn notifications
default: op
lochat.ban:
description: Ban players
default: op
lochat.ban.override:
description: Ban players with lochat.bypass.ban
default: op
lochat.unban:
description: Unban players
default: op
lochat.bypass.warn:
description: Immune to /warn
default: false
lochat.bypass.ban:
description: Immune to /lban (unless staff has lochat.ban.override)
default: false
lochat.playerinfo:
description: View player moderation stats
default: op
lochat.clearchat.config:
description: Can configure clear chat message
default: op