refactor: restructure services and filters architecture

- Remove deprecated service interfaces (CooldownService, IgnoreService, PMService, PlayerDataService, SpyService)
- Add new unified service interfaces (MessagingService, PlayerService, PunishmentService)
- Extract FilterResult as standalone class from AdvancedMessageFilter
- Implement individual filter classes (CharacterFilter, FloodFilter, SpamFilter, SwearFilter, UrlFilter)
- Add moderation commands (BanCommand, WarnCommand, SilentWarnCommand, UnbanCommand)
- Introduce data models for punishment tracking (BanRecord, WarnEntry)
- Refactor messaging service with new MessagingServiceImpl implementation
- Add mute service utilities (MuteDataStorage, MutePermissionChecker)
- Implement PunishmentServiceImpl with snapshot tracking
- Add Discord integration components (DiscordConfig, DiscordMessageService)
- Introduce command handlers for gradient module (ColorCommandHandler, PrefixCommandHandler)
- Add renderer components (EmojiComponent, GradientBuilder, UrlProcessor)
- Add utility classes (ColorConverter, MessageSender, PlaceholderProcessor)
- Update configuration files and plugin metadata
- Add documentation files (CODE_REVIEW_RU.md, REFACTORING_SERVICES.md)
- Consolidate service architecture for improved maintainability and separation of concerns
This commit is contained in:
loki5512344 2026-04-06 00:18:25 +02:00
parent 2d79d2a91d
commit bba480002f
181 changed files with 3455 additions and 2908 deletions

View file

@ -1,16 +0,0 @@
package com.loki.lochat.api.service;
import java.util.UUID;
/**
* Сервис для управления кулдаунами
*/
public interface CooldownService {
boolean isOnCooldown(UUID player, String type, int cooldownSeconds);
int getRemainingCooldown(UUID player, String type, int cooldownSeconds);
void setCooldown(UUID player, String type);
void removeCooldown(UUID player);
}

View file

@ -1,53 +0,0 @@
package com.loki.lochat.api.service;
import java.util.Set;
import java.util.UUID;
/**
* Сервис для управления списком игнорируемых игроков
*/
public interface IgnoreService {
/**
* Проверяет, игнорирует ли игрок другого
*/
boolean isIgnoring(UUID player, UUID target);
/**
* Добавляет игрока в список игнорируемых
*
* @return true если успешно добавлен
*/
boolean addIgnore(UUID player, UUID target);
/**
* Удаляет игрока из списка игнорируемых
*
* @return true если успешно удалён
*/
boolean removeIgnore(UUID player, UUID target);
/**
* Получает список всех игнорируемых игроков
*/
Set<UUID> getIgnoredPlayers(UUID player);
/**
* Получает количество игнорируемых игроков
*/
int getIgnoredCount(UUID player);
/**
* Очищает список игнорируемых для игрока
*/
void clearIgnores(UUID player);
/**
* Сохраняет данные на диск
*/
void save();
/**
* Загружает данные с диска
*/
void load();
}

View file

@ -0,0 +1,111 @@
package com.loki.lochat.api.service;
import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
/**
* Объединённый сервис для управления общением между игроками
* Включает: личные сообщения (PM), шпионаж (Spy), игнорирование (Ignore)
*/
public interface MessagingService {
// ========== PM (Private Messages) ==========
/**
* Устанавливает последнего собеседника для игрока
*/
void setLastConversation(UUID player, UUID target);
/**
* Получает последнего собеседника игрока
*/
Optional<UUID> getLastConversation(UUID player);
/**
* Удаляет информацию о последнем разговоре
*/
void removeConversation(UUID player);
/**
* Проверяет, есть ли у игрока активный разговор
*/
boolean hasConversation(UUID player);
// ========== Spy (Шпионаж) ==========
/**
* Переключает режим шпиона для игрока
* @return true если шпион включен, false если выключен
*/
boolean toggleSpy(UUID player);
/**
* Проверяет, находится ли игрок в режиме шпиона
*/
boolean isSpying(UUID player);
/**
* Отправляет сообщение всем шпионам (для обычного чата)
*/
void sendToSpies(Player sender, Component message, boolean isGlobal);
/**
* Отправляет информацию о личном сообщении шпионам
*/
void broadcastPM(Player sender, Player receiver, String message);
/**
* Удаляет игрока из списка шпионов
*/
void removeSpy(UUID player);
// ========== Ignore (Игнорирование) ==========
/**
* Проверяет, игнорирует ли игрок другого
*/
boolean isIgnoring(UUID player, UUID target);
/**
* Добавляет игрока в список игнорируемых
* @return true если успешно добавлен
*/
boolean addIgnore(UUID player, UUID target);
/**
* Удаляет игрока из списка игнорируемых
* @return true если успешно удалён
*/
boolean removeIgnore(UUID player, UUID target);
/**
* Получает список всех игнорируемых игроков
*/
Set<UUID> getIgnoredPlayers(UUID player);
/**
* Получает количество игнорируемых игроков
*/
int getIgnoredCount(UUID player);
/**
* Очищает список игнорируемых для игрока
*/
void clearIgnores(UUID player);
// ========== Persistence ==========
/**
* Сохраняет данные на диск
*/
void save();
/**
* Загружает данные с диска
*/
void load();
}

View file

@ -1,29 +0,0 @@
package com.loki.lochat.api.service;
import java.util.Optional;
import java.util.UUID;
/**
* Сервис для управления личными сообщениями
*/
public interface PMService {
/**
* Устанавливает последнего собеседника для игрока
*/
void setLastConversation(UUID player, UUID target);
/**
* Получает последнего собеседника игрока
*/
Optional<UUID> getLastConversation(UUID player);
/**
* Удаляет информацию о последнем разговоре
*/
void removeConversation(UUID player);
/**
* Проверяет, есть ли у игрока активный разговор
*/
boolean hasConversation(UUID player);
}

View file

@ -1,12 +0,0 @@
package com.loki.lochat.api.service;
import java.util.UUID;
/**
* Сервис для работы с данными игроков
*/
public interface PlayerDataService {
void clearPlayerData(UUID player);
void saveAll();
}

View file

@ -0,0 +1,56 @@
package com.loki.lochat.api.service;
import java.util.UUID;
/**
* Объединённый сервис для управления данными игроков
* Включает: кулдауны (Cooldown), статистику (PlayerData)
*/
public interface PlayerService {
// ========== Cooldown ==========
/**
* Проверяет, находится ли игрок на кулдауне
*/
boolean isOnCooldown(UUID player, String type, int cooldownSeconds);
/**
* Получает оставшееся время кулдауна в секундах
*/
int getRemainingCooldown(UUID player, String type, int cooldownSeconds);
/**
* Устанавливает кулдаун для игрока
*/
void setCooldown(UUID player, String type);
/**
* Удаляет все кулдауны игрока
*/
void removeCooldown(UUID player);
// ========== Player Statistics ==========
/**
* Записывает отправленное сообщение в статистику
* @param player UUID игрока
* @param chatType тип чата: "global", "local", "pm"
*/
void recordMessage(UUID player, String chatType);
/**
* Получает количество сообщений игрока за текущую сессию
*/
long getPlayerMessages(UUID player);
/**
* Очищает данные игрока (кулдауны + сохраняет статистику)
*/
void clearPlayerData(UUID player);
/**
* Сохраняет всю статистику на диск
*/
void saveAll();
}

View file

@ -1,36 +0,0 @@
package com.loki.lochat.api.service;
import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
/**
* Сервис для шпионажа за чатом
*/
public interface SpyService {
/**
* Переключает режим шпиона для игрока
*
* @return true если шпион включен, false если выключен
*/
boolean toggleSpy(java.util.UUID player);
/**
* Проверяет, находится ли игрок в режиме шпиона
*/
boolean isSpying(java.util.UUID player);
/**
* Отправляет сообщение всем шпионам (для обычного чата)
*/
void sendToSpies(Player sender, Component message, boolean isGlobal);
/**
* Отправляет информацию о личном сообщении шпионам
*/
void broadcastPM(Player sender, Player receiver, String message);
/**
* Удаляет игрока из списка шпионов
*/
void removeSpy(java.util.UUID player);
}

View file

@ -1,7 +1,7 @@
package com.loki.lochat.commands.admin;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.SpyService;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
@ -12,11 +12,11 @@ import org.jetbrains.annotations.NotNull;
public class ChatSpyCommand implements CommandExecutor {
private final LoChat plugin;
private final SpyService spyService;
private final MessagingService messagingService;
public ChatSpyCommand(LoChat plugin) {
this.plugin = plugin;
this.spyService = plugin.getServiceRegistry().get(SpyService.class);
this.messagingService = plugin.getServiceRegistry().get(MessagingService.class);
}
@Override
@ -25,7 +25,7 @@ public class ChatSpyCommand implements CommandExecutor {
sender.sendMessage(plugin.getConfigManager().getMessagesConfig().getPlayerOnly());
return true;
}
boolean enabled = spyService.toggleSpy(player.getUniqueId());
boolean enabled = messagingService.toggleSpy(player.getUniqueId());
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get(enabled ? "spy.enabled" : "spy.disabled")));
return true;
}

View file

@ -1,7 +1,7 @@
package com.loki.lochat.commands.messaging;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
@ -13,11 +13,11 @@ import org.jetbrains.annotations.NotNull;
public class IgnoreCommand implements CommandExecutor {
private final LoChat plugin;
private final IgnoreService ignoreService;
private final MessagingService messagingService;
public IgnoreCommand(LoChat plugin) {
this.plugin = plugin;
this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class);
this.messagingService = plugin.getServiceRegistry().get(MessagingService.class);
}
@Override
@ -28,9 +28,9 @@ public class IgnoreCommand implements CommandExecutor {
Player target = Bukkit.getPlayer(args[0]);
if (target == null) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getPlayerNotFound())); return true; }
if (target.equals(player)) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.self"))); return true; }
if (ignoreService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.already-ignored"))); return true; }
if (messagingService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.already-ignored"))); return true; }
ignoreService.addIgnore(player.getUniqueId(), target.getUniqueId());
messagingService.addIgnore(player.getUniqueId(), target.getUniqueId());
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.added", "{player}", target.getName())));
return true;
}

View file

@ -1,7 +1,7 @@
package com.loki.lochat.commands.messaging;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.utils.ChatFormatter;
import com.loki.lochat.utils.PlayerUtil;
import org.bukkit.command.Command;
@ -16,18 +16,18 @@ import java.util.UUID;
public class IgnoreListCommand implements CommandExecutor {
private final LoChat plugin;
private final IgnoreService ignoreService;
private final MessagingService messagingService;
public IgnoreListCommand(LoChat plugin) {
this.plugin = plugin;
this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class);
this.messagingService = plugin.getServiceRegistry().get(MessagingService.class);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) { sender.sendMessage(plugin.getConfigManager().getMessagesConfig().getPlayerOnly()); return true; }
Set<UUID> ignored = ignoreService.getIgnoredPlayers(player.getUniqueId());
Set<UUID> ignored = messagingService.getIgnoredPlayers(player.getUniqueId());
if (ignored.isEmpty()) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-empty"))); return true; }
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-header")));

View file

@ -1,9 +1,7 @@
package com.loki.lochat.commands.messaging;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.api.service.PMService;
import com.loki.lochat.api.service.SpyService;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.utils.ChatFormatter;
import com.loki.lochat.utils.PlayerUtil;
import org.bukkit.Bukkit;
@ -17,15 +15,11 @@ import org.jetbrains.annotations.NotNull;
public class MsgCommand implements CommandExecutor {
private final LoChat plugin;
private final PMService pmService;
private final IgnoreService ignoreService;
private final SpyService spyService;
private final MessagingService messagingService;
public MsgCommand(LoChat plugin) {
this.plugin = plugin;
this.pmService = plugin.getServiceRegistry().get(PMService.class);
this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class);
this.spyService = plugin.getServiceRegistry().get(SpyService.class);
this.messagingService = plugin.getServiceRegistry().get(MessagingService.class);
}
@Override
@ -43,8 +37,8 @@ public class MsgCommand implements CommandExecutor {
Player target = Bukkit.getPlayer(args[0]);
if (target == null) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.player-offline"))); return true; }
if (target.equals(player)) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.self"))); return true; }
if (ignoreService.isIgnoring(target.getUniqueId(), player.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); return true; }
if (ignoreService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); return true; }
if (messagingService.isIgnoring(target.getUniqueId(), player.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); return true; }
if (messagingService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); return true; }
String message = String.join(" ", java.util.Arrays.copyOfRange(args, 1, args.length));
sendPm(player, target, message);
@ -58,9 +52,9 @@ public class MsgCommand implements CommandExecutor {
Sound sound = PlayerUtil.parseSound(plugin.getConfigManager().getPmSoundType(), null);
if (sound != null) target.playSound(target.getLocation(), sound, 1.0f, 1.0f);
}
spyService.broadcastPM(sender, target, message);
pmService.setLastConversation(sender.getUniqueId(), target.getUniqueId());
pmService.setLastConversation(target.getUniqueId(), sender.getUniqueId());
messagingService.broadcastPM(sender, target, message);
messagingService.setLastConversation(sender.getUniqueId(), target.getUniqueId());
messagingService.setLastConversation(target.getUniqueId(), sender.getUniqueId());
if (plugin.getConfigManager().isPmLogEnabled())
plugin.getLogger().info("[PM] " + sender.getName() + " -> " + target.getName() + ": " + message);
}

View file

@ -1,9 +1,7 @@
package com.loki.lochat.commands.messaging;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.api.service.PMService;
import com.loki.lochat.api.service.SpyService;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.utils.ChatFormatter;
import com.loki.lochat.utils.PlayerUtil;
import org.bukkit.Bukkit;
@ -20,15 +18,11 @@ import java.util.UUID;
public class ReplyCommand implements CommandExecutor {
private final LoChat plugin;
private final PMService pmService;
private final IgnoreService ignoreService;
private final SpyService spyService;
private final MessagingService messagingService;
public ReplyCommand(LoChat plugin) {
this.plugin = plugin;
this.pmService = plugin.getServiceRegistry().get(PMService.class);
this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class);
this.spyService = plugin.getServiceRegistry().get(SpyService.class);
this.messagingService = plugin.getServiceRegistry().get(MessagingService.class);
}
@Override
@ -43,12 +37,12 @@ public class ReplyCommand implements CommandExecutor {
return true;
}
Optional<UUID> lastUuid = pmService.getLastConversation(player.getUniqueId());
Optional<UUID> lastUuid = messagingService.getLastConversation(player.getUniqueId());
if (lastUuid.isEmpty()) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.no-reply"))); return true; }
Player target = Bukkit.getPlayer(lastUuid.get());
if (target == null) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.player-offline"))); return true; }
if (ignoreService.isIgnoring(target.getUniqueId(), player.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); return true; }
if (messagingService.isIgnoring(target.getUniqueId(), player.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); return true; }
String message = String.join(" ", args);
player.sendMessage(ChatFormatter.formatPmSentNew(plugin.getMessageConfig().getPmFormatSent(), player, target, message));
@ -57,8 +51,8 @@ public class ReplyCommand implements CommandExecutor {
Sound sound = PlayerUtil.parseSound(plugin.getConfigManager().getPmSoundType(), null);
if (sound != null) target.playSound(target.getLocation(), sound, 1.0f, 1.0f);
}
spyService.broadcastPM(player, target, message);
pmService.setLastConversation(target.getUniqueId(), player.getUniqueId());
messagingService.broadcastPM(player, target, message);
messagingService.setLastConversation(target.getUniqueId(), player.getUniqueId());
if (plugin.getConfigManager().isPmLogEnabled())
plugin.getLogger().info("[PM] " + player.getName() + " -> " + target.getName() + ": " + message);
return true;

View file

@ -1,7 +1,7 @@
package com.loki.lochat.commands.messaging;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.utils.ChatFormatter;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
@ -13,11 +13,11 @@ import org.jetbrains.annotations.NotNull;
public class UnignoreCommand implements CommandExecutor {
private final LoChat plugin;
private final IgnoreService ignoreService;
private final MessagingService messagingService;
public UnignoreCommand(LoChat plugin) {
this.plugin = plugin;
this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class);
this.messagingService = plugin.getServiceRegistry().get(MessagingService.class);
}
@Override
@ -27,9 +27,9 @@ public class UnignoreCommand implements CommandExecutor {
Player target = Bukkit.getPlayer(args[0]);
if (target == null) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getPlayerNotFound())); return true; }
if (!ignoreService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.not-ignored"))); return true; }
if (!messagingService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.not-ignored"))); return true; }
ignoreService.removeIgnore(player.getUniqueId(), target.getUniqueId());
messagingService.removeIgnore(player.getUniqueId(), target.getUniqueId());
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.removed", "{player}", target.getName())));
return true;
}

View file

@ -1,7 +1,7 @@
package com.loki.lochat.commands.rp;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.CooldownService;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.commands.base.PlayerCommand;
import com.loki.lochat.utils.ChatFormatter;
import net.kyori.adventure.text.Component;
@ -16,11 +16,11 @@ import org.jetbrains.annotations.NotNull;
*/
public class DoCommand extends PlayerCommand {
private final CooldownService cooldownService;
private final PlayerService playerService;
public DoCommand(LoChat plugin) {
super(plugin);
this.cooldownService = plugin.getServiceRegistry().get(CooldownService.class);
this.playerService = plugin.getServiceRegistry().get(PlayerService.class);
}
@Override
@ -29,8 +29,8 @@ public class DoCommand extends PlayerCommand {
if (!requirePermission(player, "lochat.rp.do")) return true;
int cooldown = plugin.getConfigManager().getInt("rp.cooldowns.do", 0);
if (cooldown > 0 && cooldownService.isOnCooldown(player.getUniqueId(), "rp_do", cooldown)) {
int remaining = cooldownService.getRemainingCooldown(player.getUniqueId(), "rp_do", cooldown);
if (cooldown > 0 && playerService.isOnCooldown(player.getUniqueId(), "rp_do", cooldown)) {
int remaining = playerService.getRemainingCooldown(player.getUniqueId(), "rp_do", cooldown);
String msg = plugin.getConfigManager().getMessagesConfig().getCooldownMessage().replace("{remaining}", String.valueOf(remaining));
player.sendMessage(ChatFormatter.parse(msg));
return true;
@ -54,7 +54,7 @@ public class DoCommand extends PlayerCommand {
int radius = plugin.getConfigManager().getInt("rp.radius", 100);
RpUtil.sendToRadius(player, message, radius);
if (cooldown > 0) cooldownService.setCooldown(player.getUniqueId(), "rp_do");
if (cooldown > 0) playerService.setCooldown(player.getUniqueId(), "rp_do");
plugin.getLogger().info("[DO] " + player.getName() + ": " + description);
return true;
}

View file

@ -1,7 +1,7 @@
package com.loki.lochat.commands.rp;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.CooldownService;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.commands.base.PlayerCommand;
import com.loki.lochat.utils.ChatFormatter;
import net.kyori.adventure.text.Component;
@ -16,11 +16,11 @@ import org.jetbrains.annotations.NotNull;
*/
public class MeCommand extends PlayerCommand {
private final CooldownService cooldownService;
private final PlayerService playerService;
public MeCommand(LoChat plugin) {
super(plugin);
this.cooldownService = plugin.getServiceRegistry().get(CooldownService.class);
this.playerService = plugin.getServiceRegistry().get(PlayerService.class);
}
@Override
@ -29,8 +29,8 @@ public class MeCommand extends PlayerCommand {
if (!requirePermission(player, "lochat.rp.me")) return true;
int cooldown = plugin.getConfigManager().getInt("rp.cooldowns.me", 0);
if (cooldown > 0 && cooldownService.isOnCooldown(player.getUniqueId(), "rp_me", cooldown)) {
int remaining = cooldownService.getRemainingCooldown(player.getUniqueId(), "rp_me", cooldown);
if (cooldown > 0 && playerService.isOnCooldown(player.getUniqueId(), "rp_me", cooldown)) {
int remaining = playerService.getRemainingCooldown(player.getUniqueId(), "rp_me", cooldown);
String msg = plugin.getConfigManager().getMessagesConfig().getCooldownMessage().replace("{remaining}", String.valueOf(remaining));
player.sendMessage(ChatFormatter.parse(msg));
return true;
@ -54,7 +54,7 @@ public class MeCommand extends PlayerCommand {
int radius = plugin.getConfigManager().getInt("rp.radius", 100);
RpUtil.sendToRadius(player, message, radius);
if (cooldown > 0) cooldownService.setCooldown(player.getUniqueId(), "rp_me");
if (cooldown > 0) playerService.setCooldown(player.getUniqueId(), "rp_me");
plugin.getLogger().info("[ME] " + player.getName() + ": " + action);
return true;
}

View file

@ -1,7 +1,7 @@
package com.loki.lochat.commands.rp;
import com.loki.lochat.LoChat;
import com.loki.lochat.util.DistanceUtil;
import com.loki.lochat.utils.DistanceUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;

View file

@ -1,7 +1,7 @@
package com.loki.lochat.commands.rp;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.CooldownService;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.commands.base.PlayerCommand;
import com.loki.lochat.utils.ChatFormatter;
import net.kyori.adventure.text.Component;
@ -19,11 +19,11 @@ import java.util.Random;
public class TryCommand extends PlayerCommand {
private static final Random RANDOM = new Random();
private final CooldownService cooldownService;
private final PlayerService playerService;
public TryCommand(LoChat plugin) {
super(plugin);
this.cooldownService = plugin.getServiceRegistry().get(CooldownService.class);
this.playerService = plugin.getServiceRegistry().get(PlayerService.class);
}
@Override
@ -32,8 +32,8 @@ public class TryCommand extends PlayerCommand {
if (!requirePermission(player, "lochat.rp.try")) return true;
int cooldown = plugin.getConfigManager().getInt("rp.cooldowns.try", 0);
if (cooldown > 0 && cooldownService.isOnCooldown(player.getUniqueId(), "rp_try", cooldown)) {
int remaining = cooldownService.getRemainingCooldown(player.getUniqueId(), "rp_try", cooldown);
if (cooldown > 0 && playerService.isOnCooldown(player.getUniqueId(), "rp_try", cooldown)) {
int remaining = playerService.getRemainingCooldown(player.getUniqueId(), "rp_try", cooldown);
String msg = plugin.getConfigManager().getMessagesConfig().getCooldownMessage().replace("{remaining}", String.valueOf(remaining));
player.sendMessage(ChatFormatter.parse(msg));
return true;
@ -64,7 +64,7 @@ public class TryCommand extends PlayerCommand {
int radius = plugin.getConfigManager().getInt("rp.radius", 100);
RpUtil.sendToRadius(player, message, radius);
if (cooldown > 0) cooldownService.setCooldown(player.getUniqueId(), "rp_try");
if (cooldown > 0) playerService.setCooldown(player.getUniqueId(), "rp_try");
plugin.getLogger().info("[TRY] " + player.getName() + ": " + action + " -> " + (success ? "SUCCESS" : "FAIL"));
return true;
}

View file

@ -44,237 +44,66 @@ public class AppearanceConfig extends BaseConfig {
// ========== ПРЕФИКСЫ ==========
/**
* Получить эмодзи для глобального чата
*/
public String getGlobalEmoji() {
return config.getString("prefixes.global.emoji", "🌍");
private String getPrefixValue(String chatType, String key, String defaultValue) {
return config.getString("prefixes." + chatType + "." + key, defaultValue);
}
/**
* Получить текст префикса глобального чата
*/
public String getGlobalText() {
return config.getString("prefixes.global.text", "GLOBAL");
}
public String getGlobalEmoji() { return getPrefixValue("global", "emoji", "🌍"); }
public String getGlobalText() { return getPrefixValue("global", "text", "GLOBAL"); }
public List<String> getGlobalColors() { return config.getStringList("prefixes.global.colors"); }
public String getGlobalSeparatorText() { return getPrefixValue("global", "separator.text", " ▶ "); }
public String getGlobalSeparatorColor() { return getPrefixValue("global", "separator.color", "#FFD700"); }
public String getGlobalMessageColor() { return getPrefixValue("global", "message-color", "#FFFFFF"); }
/**
* Получить цвета градиента для глобального чата
*/
public List<String> getGlobalColors() {
return config.getStringList("prefixes.global.colors");
}
/**
* Получить разделитель для глобального чата
*/
public String getGlobalSeparatorText() {
return config.getString("prefixes.global.separator.text", " ▶ ");
}
/**
* Получить цвет разделителя для глобального чата
*/
public String getGlobalSeparatorColor() {
return config.getString("prefixes.global.separator.color", "#FFD700");
}
/**
* Получить эмодзи для локального чата
*/
public String getLocalEmoji() {
return config.getString("prefixes.local.emoji", "");
}
/**
* Получить текст префикса локального чата
*/
public String getLocalText() {
return config.getString("prefixes.local.text", "LOCAL");
}
/**
* Получить цвета градиента для локального чата
*/
public List<String> getLocalColors() {
return config.getStringList("prefixes.local.colors");
}
/**
* Получить разделитель для локального чата
*/
public String getLocalSeparatorText() {
return config.getString("prefixes.local.separator.text", " ▶ ");
}
/**
* Получить цвет разделителя для локального чата
*/
public String getLocalSeparatorColor() {
return config.getString("prefixes.local.separator.color", "#FFFFFF");
}
/**
* Получить цвет текста сообщений в глобальном чате (дефолт)
*/
public String getGlobalMessageColor() {
return config.getString("prefixes.global.message-color", "#FFFFFF");
}
/**
* Получить цвет текста сообщений в локальном чате (дефолт)
*/
public String getLocalMessageColor() {
return config.getString("prefixes.local.message-color", "#FFFFFF");
}
public String getLocalEmoji() { return getPrefixValue("local", "emoji", ""); }
public String getLocalText() { return getPrefixValue("local", "text", "LOCAL"); }
public List<String> getLocalColors() { return config.getStringList("prefixes.local.colors"); }
public String getLocalSeparatorText() { return getPrefixValue("local", "separator.text", " ▶ "); }
public String getLocalSeparatorColor() { return getPrefixValue("local", "separator.color", "#FFFFFF"); }
public String getLocalMessageColor() { return getPrefixValue("local", "message-color", "#FFFFFF"); }
// ========== ФОРМАТ СООБЩЕНИЙ ==========
/**
* Получить формат сообщения для глобального чата
*/
public String getGlobalChatFormat() {
return config.getString("chat-format.global", "{emoji} {prefix} {separator} {player} : {message}");
}
/**
* Получить формат сообщения для локального чата
*/
public String getLocalChatFormat() {
return config.getString("chat-format.local", "{emoji} {prefix} {separator} {player} : {message}");
}
// ========== HOVER ЭФФЕКТЫ ==========
/**
* Включены ли hover эффекты
*/
public boolean isHoverEnabled() {
return config.getBoolean("hover.enabled", true);
}
/**
* Получить формат hover подсказки
*/
public List<String> getHoverFormat() {
return config.getStringList("hover.format");
}
public boolean isHoverEnabled() { return config.getBoolean("hover.enabled", true); }
public List<String> getHoverFormat() { return config.getStringList("hover.format"); }
// ========== ЭМОДЗИ ==========
/**
* Получить все эмодзи замены
*/
public Map<String, String> getEmojis() {
return new HashMap<>(emojiCache);
}
/**
* Получить замену для эмодзи
*/
public String getEmoji(String key) {
return emojiCache.get(key);
}
public Map<String, String> getEmojis() { return new HashMap<>(emojiCache); }
public String getEmoji(String key) { return emojiCache.get(key); }
// ========== НАСТРОЙКИ ЧАТА ==========
/**
* Получить радиус локального чата
*/
public int getLocalRadius() {
return config.getInt("chat.local_radius", 100);
public int getLocalRadius() { return config.getInt("chat.local_radius", 100); }
public int getClearLines() { return config.getInt("chat.clear_lines", 100); }
public int getMinMessageLength() { return config.getInt("chat.min_message_length", 1); }
public int getMaxMessageLength() { return config.getInt("chat.max_message_length", 2000); }
private String getChatColor(String type, String defaultColor) {
return config.getString("chat.colors." + type, defaultColor);
}
/**
* Получить количество строк для очистки чата
*/
public int getClearLines() {
return config.getInt("chat.clear_lines", 100);
}
/**
* Получить минимальную длину сообщения
*/
public int getMinMessageLength() {
return config.getInt("chat.min_message_length", 1);
}
/**
* Получить максимальную длину сообщения
*/
public int getMaxMessageLength() {
return config.getInt("chat.max_message_length", 2000);
}
/**
* Получить цвет "никто не услышал"
*/
public String getNobodyHeardColor() {
return config.getString("chat.colors.nobody_heard", "#FFA726");
}
/**
* Получить цвет ошибки
*/
public String getErrorColor() {
return config.getString("chat.colors.error", "#FF6B6B");
}
/**
* Получить цвет успеха
*/
public String getSuccessColor() {
return config.getString("chat.colors.success", "#6BCB77");
}
/**
* Получить цвет предупреждения
*/
public String getWarningColor() {
return config.getString("chat.colors.warning", "#FFA726");
}
/**
* Получить цвет информации
*/
public String getInfoColor() {
return config.getString("chat.colors.info", "#87CEEB");
}
public String getNobodyHeardColor() { return getChatColor("nobody_heard", "#FFA726"); }
public String getErrorColor() { return getChatColor("error", "#FF6B6B"); }
public String getSuccessColor() { return getChatColor("success", "#6BCB77"); }
public String getWarningColor() { return getChatColor("warning", "#FFA726"); }
public String getInfoColor() { return getChatColor("info", "#87CEEB"); }
// ========== DISCORD ==========
/**
* Получить эмодзи для Discord событий
*/
public String getDiscordEmoji(String event) {
return config.getString("discord.emojis." + event, "");
}
/**
* Получить URL аватара по умолчанию
*/
public String getDefaultAvatarUrl() {
return config.getString("discord.avatar_urls.default", "https://mc-heads.net/avatar/minecraft/64");
}
/**
* Получить URL аватара игрока
*/
public String getPlayerAvatarUrl() {
return config.getString("discord.avatar_urls.player", "https://mc-heads.net/avatar/{player}/64");
}
/**
* Получить цвет embed по умолчанию
*/
public String getDefaultEmbedColor() {
return config.getString("discord.default_embed_color", "5865F2");
}
/**
* Получить заголовок события Discord
*/
public String getDiscordEventTitle(String event) {
return config.getString("discord.event_titles." + event, "");
}
public String getDiscordEmoji(String event) { return config.getString("discord.emojis." + event, ""); }
public String getDiscordEventTitle(String event) { return config.getString("discord.event_titles." + event, ""); }
public String getDefaultAvatarUrl() { return config.getString("discord.avatar_urls.default", "https://mc-heads.net/avatar/minecraft/64"); }
public String getPlayerAvatarUrl() { return config.getString("discord.avatar_urls.player", "https://mc-heads.net/avatar/{player}/64"); }
public String getDefaultEmbedColor() { return config.getString("discord.default_embed_color", "5865F2"); }
}

View file

@ -6,12 +6,12 @@ import org.bukkit.configuration.file.FileConfiguration;
import java.util.List;
/**
* Менеджер конфигураций - упрощенная версия
*/
public class ConfigManager {
private final LoChat plugin;
// ✅ THREAD-SAFETY: volatile для безопасного reload из разных потоков
// volatile гарантирует что все потоки видят актуальную версию конфига после reload
private volatile FileConfiguration config;
private volatile AppearanceConfig appearanceConfig;
private volatile MessagesConfig messagesConfig;
@ -24,55 +24,42 @@ public class ConfigManager {
plugin.saveDefaultConfig();
this.config = plugin.getConfig();
// Инициализируем новые конфигурации
// Инициализируем конфигурации
this.appearanceConfig = new AppearanceConfig(plugin);
this.messagesConfig = new MessagesConfig(plugin);
this.muteConfig = new MuteConfig(plugin);
this.filtersConfig = new FiltersConfig(plugin);
this.soundsConfig = new SoundsConfig(plugin);
// Инициализируем их после создания
this.appearanceConfig.init();
// Проверяем версию конфига и обновляем при необходимости
updateConfig();
}
/**
* Получить конфигурацию внешнего вида
*/
// ========== Геттеры конфигураций ==========
public AppearanceConfig getAppearanceConfig() {
return appearanceConfig;
}
/**
* Получить конфигурацию системных сообщений
*/
public MessagesConfig getMessagesConfig() {
return messagesConfig;
}
/**
* Получить конфигурацию мутов
*/
public MuteConfig getMuteConfig() {
return muteConfig;
}
/**
* Получить конфигурацию фильтров
*/
public FiltersConfig getFiltersConfig() {
return filtersConfig;
}
/**
* Получить конфигурацию звуков
*/
public SoundsConfig getSoundsConfig() {
return soundsConfig;
}
// ========== Обновление и перезагрузка ==========
private void updateConfig() {
int currentVersion = config.getInt("config-version", 1);
int requiredVersion = 5;
@ -87,57 +74,37 @@ public class ConfigManager {
}
private void addMissingConfigOptions() {
if (!config.contains("announcements")) {
config.set("announcements.show-title", true);
config.set("announcements.title-header", "&#FFD700ОБЪЯВЛЕНИЕ");
config.set("announcements.show-actionbar", true);
config.set("announcements.title-duration", 3);
}
setIfMissing("announcements.show-title", true);
setIfMissing("announcements.title-header", "&#FFD700ОБЪЯВЛЕНИЕ");
setIfMissing("announcements.show-actionbar", true);
setIfMissing("announcements.title-duration", 3);
setIfMissing("chat.pm.enabled", true);
setIfMissing("mentions.enabled", true);
setIfMissing("mentions.sound", true);
setIfMissing("mentions.sound-type", "BLOCK_NOTE_BLOCK_PLING");
setIfMissing("mentions.highlight", "&#FFFF00@{player}");
setIfMissing("mentions.self-highlight", "&#FFD700{player}");
setIfMissing("automessages.enabled", true);
setIfMissing("automessages.interval", 300);
setIfMissing("automessages.random", false);
setIfMissing("clearchat.message.enabled", true);
setIfMissing("clearchat.message.text", "&#04CADFЧат очищен администратором {player}");
setIfMissing("lopreff.enabled", true);
setIfMissing("lopreff.use-gradient-name", true);
setIfMissing("lopreff.use-custom-prefix", true);
setIfMissing("moderation.voice-mute-console-command", "");
}
if (!config.contains("chat.pm.enabled")) {
config.set("chat.pm.enabled", true);
}
if (!config.contains("mentions.enabled")) {
config.set("mentions.enabled", true);
config.set("mentions.sound", true);
config.set("mentions.sound-type", "BLOCK_NOTE_BLOCK_PLING");
config.set("mentions.highlight", "&#FFFF00@{player}");
config.set("mentions.self-highlight", "&#FFD700{player}");
}
if (!config.contains("automessages.enabled")) {
config.set("automessages.enabled", true);
config.set("automessages.interval", 300);
config.set("automessages.random", false);
}
if (!config.contains("clearchat.message.enabled")) {
config.set("clearchat.message.enabled", true);
config.set("clearchat.message.text", "&#04CADFЧат очищен администратором {player}");
}
if (!config.contains("lopreff.enabled")) {
config.set("lopreff.enabled", true);
config.set("lopreff.use-gradient-name", true);
config.set("lopreff.use-custom-prefix", true);
}
if (!config.contains("moderation.voice-mute-console-command")) {
config.set("moderation.voice-mute-console-command", "");
private void setIfMissing(String path, Object value) {
if (!config.contains(path)) {
config.set(path, value);
}
}
/**
* Перезагрузить все конфигурации
* ✅ THREAD-SAFE: использует volatile для безопасного обновления
*/
public synchronized void reload() {
plugin.reloadConfig();
this.config = plugin.getConfig();
// Перезагружаем новые конфигурации
// volatile гарантирует что изменения видны всем потокам
this.appearanceConfig.reload();
this.messagesConfig.reload();
this.muteConfig.reload();
@ -147,6 +114,8 @@ public class ConfigManager {
plugin.getLogger().info("Все конфигурации успешно перезагружены");
}
// ========== Базовые методы доступа ==========
public String getString(String path, String defaultValue) {
return config.getString(path, defaultValue);
}
@ -167,7 +136,8 @@ public class ConfigManager {
return config.getStringList(path);
}
// Global chat
// ========== Настройки чата ==========
public boolean isGlobalEnabled() {
return config.getBoolean("chat.global.enabled", true);
}
@ -176,7 +146,6 @@ public class ConfigManager {
return config.getInt("chat.global.cooldown", 3);
}
// Local chat
public boolean isLocalEnabled() {
return config.getBoolean("chat.local.enabled", true);
}
@ -189,7 +158,8 @@ public class ConfigManager {
return config.getInt("chat.local.cooldown", 1);
}
// PM
// ========== PM ==========
public boolean isPmEnabled() {
return config.getBoolean("chat.pm.enabled", true);
}
@ -199,28 +169,29 @@ public class ConfigManager {
}
public boolean isPmSoundEnabled() {
// ✅ Используем SoundsConfig если доступен
return soundsConfig != null ? soundsConfig.isPmSoundEnabled() : config.getBoolean("chat.pm.sound", true);
return soundsConfig != null ? soundsConfig.isPmSoundEnabled() :
config.getBoolean("chat.pm.sound", true);
}
public String getPmSoundType() {
// ✅ Используем SoundsConfig если доступен
return soundsConfig != null ? soundsConfig.getPmSound() : config.getString("chat.pm.sound-type", "ENTITY_EXPERIENCE_ORB_PICKUP");
return soundsConfig != null ? soundsConfig.getPmSound() :
config.getString("chat.pm.sound-type", "ENTITY_EXPERIENCE_ORB_PICKUP");
}
// Mentions
// ========== Mentions ==========
public boolean isMentionsEnabled() {
return config.getBoolean("mentions.enabled", true);
}
public boolean isMentionSoundEnabled() {
// ✅ Используем SoundsConfig если доступен
return soundsConfig != null ? soundsConfig.isMentionSoundEnabled() : config.getBoolean("mentions.sound", true);
return soundsConfig != null ? soundsConfig.isMentionSoundEnabled() :
config.getBoolean("mentions.sound", true);
}
public String getMentionSoundType() {
// ✅ Используем SoundsConfig если доступен
return soundsConfig != null ? soundsConfig.getMentionSound() : config.getString("mentions.sound-type", "BLOCK_NOTE_BLOCK_PLING");
return soundsConfig != null ? soundsConfig.getMentionSound() :
config.getString("mentions.sound-type", "BLOCK_NOTE_BLOCK_PLING");
}
public String getMentionHighlight() {
@ -231,7 +202,8 @@ public class ConfigManager {
return ChatFormatter.convertAllColors(config.getString("mentions.self-highlight", "&#FFD700{player}"));
}
// Clear chat
// ========== Clear chat ==========
public boolean isClearChatMessageEnabled() {
return config.getBoolean("clearchat.message.enabled", true);
}
@ -250,7 +222,8 @@ public class ConfigManager {
plugin.saveConfig();
}
// Auto messages
// ========== Auto messages ==========
public boolean isAutoMessagesEnabled() {
return config.getBoolean("automessages.enabled", true);
}
@ -263,18 +236,22 @@ public class ConfigManager {
return config.getBoolean("automessages.random", false);
}
// Formats
// ========== Formats ==========
public String getAnnouncementFormat() {
return ChatFormatter.convertAllColors(config.getString("formats.announcement", "&#FFD700[ОБЪЯВЛЕНИЕ] <message>"));
return ChatFormatter.convertAllColors(config.getString("formats.announcement",
"&#FFD700[ОБЪЯВЛЕНИЕ] <message>"));
}
// Announcements
// ========== Announcements ==========
public boolean isAnnouncementTitleEnabled() {
return config.getBoolean("announcements.show-title", true);
}
public String getAnnouncementTitleHeader() {
return ChatFormatter.convertAllColors(config.getString("announcements.title-header", "&#FFD700ОБЪЯВЛЕНИЕ"));
return ChatFormatter.convertAllColors(config.getString("announcements.title-header",
"&#FFD700ОБЪЯВЛЕНИЕ"));
}
public boolean isAnnouncementActionBarEnabled() {
@ -285,7 +262,8 @@ public class ConfigManager {
return config.getInt("announcements.title-duration", 3);
}
// LoPreff integration
// ========== LoPreff integration ==========
public boolean isLopreffEnabled() {
return config.getBoolean("lopreff.enabled", true);
}

View file

@ -1,11 +1,11 @@
package com.loki.lochat.core;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.api.service.NickService;
import com.loki.lochat.api.service.PlayerDataService;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.core.registry.ServiceRegistry;
import com.loki.lochat.gradient.GradientModule;
import com.loki.lochat.integrations.DiscordIntegration;
@ -62,15 +62,15 @@ public class PluginShutdown {
if (serviceRegistry == null) return;
// Статистика игроков
PlayerDataService playerDataService = serviceRegistry.get(PlayerDataService.class);
if (playerDataService != null) {
playerDataService.saveAll();
PlayerService playerService = serviceRegistry.get(PlayerService.class);
if (playerService != null) {
playerService.saveAll();
}
// Игнорирования
IgnoreService ignoreService = serviceRegistry.get(IgnoreService.class);
if (ignoreService != null) {
ignoreService.save();
// Messaging (Ignore + PM + Spy)
MessagingService messagingService = serviceRegistry.get(MessagingService.class);
if (messagingService != null) {
messagingService.save();
}
// Муты

View file

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

View file

@ -1,7 +1,7 @@
package com.loki.lochat.core.filter;
import com.loki.lochat.api.filter.MessageFilter;
import com.loki.lochat.api.service.CooldownService;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.data.model.ChatMessage;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
@ -11,12 +11,12 @@ import org.bukkit.plugin.java.JavaPlugin;
* Фильтр кулдаунов
*/
public class CooldownFilter implements MessageFilter {
private final CooldownService cooldownService;
private final PlayerService playerService;
private final FileConfiguration config;
private final JavaPlugin plugin;
public CooldownFilter(CooldownService cooldownService, JavaPlugin plugin) {
this.cooldownService = cooldownService;
public CooldownFilter(PlayerService playerService, JavaPlugin plugin) {
this.playerService = playerService;
this.config = plugin.getConfig();
this.plugin = plugin;
}
@ -31,8 +31,8 @@ public class CooldownFilter implements MessageFilter {
int cooldown = config.getInt("chat." + chatType + ".cooldown", 0);
if (cooldown > 0) {
if (cooldownService.isOnCooldown(player.getUniqueId(), chatType, cooldown)) {
int remaining = cooldownService.getRemainingCooldown(
if (playerService.isOnCooldown(player.getUniqueId(), chatType, cooldown)) {
int remaining = playerService.getRemainingCooldown(
player.getUniqueId(), chatType, cooldown);
// Получаем сообщение из конфигурации
@ -45,7 +45,7 @@ public class CooldownFilter implements MessageFilter {
}
}
cooldownService.setCooldown(player.getUniqueId(), chatType);
playerService.setCooldown(player.getUniqueId(), chatType);
return true;
}
}

View file

@ -0,0 +1,20 @@
package com.loki.lochat.core.filter;
public record FilterResult(boolean allowed, String filteredMessage, String blockReason) {
public static FilterResult ok(String message) {
return new FilterResult(true, message, null);
}
public static FilterResult blocked(String reason) {
return new FilterResult(false, null, reason);
}
public boolean isAllowed() {
return allowed;
}
public String getMessage() {
return filteredMessage;
}
}

View file

@ -0,0 +1,27 @@
package com.loki.lochat.core.filter.filters;
import org.bukkit.configuration.file.FileConfiguration;
import java.util.regex.Pattern;
public class CharacterFilter {
private final boolean enabled;
private final int maxRepeating;
public CharacterFilter(FileConfiguration config) {
this.enabled = config.getBoolean("filters.repeating-chars.enabled", true);
this.maxRepeating = config.getInt("filters.repeating-chars.max", 3);
}
public String filter(String message) {
if (!enabled || maxRepeating <= 0) {
return message;
}
Pattern pattern = Pattern.compile("(.)\\1{" + maxRepeating + ",}");
return pattern.matcher(message).replaceAll(m -> {
String repeated = m.group(1);
return repeated.repeat(maxRepeating);
});
}
}

View file

@ -0,0 +1,51 @@
package com.loki.lochat.core.filter.filters;
import com.loki.lochat.core.filter.FilterResult;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class FloodFilter {
private final FileConfiguration config;
private final Map<UUID, Deque<Long>> floodTracker = new ConcurrentHashMap<>();
public FloodFilter(FileConfiguration config) {
this.config = config;
}
public FilterResult filter(Player player) {
if (player.hasPermission("lochat.bypass.flood")) {
return FilterResult.ok(null);
}
int maxMessages = config.getInt("filters.flood.max-messages", 5);
int timePeriod = config.getInt("filters.flood.time-period", 10);
long now = System.currentTimeMillis();
long windowMs = timePeriod * 1000L;
Deque<Long> timestamps = floodTracker.computeIfAbsent(
player.getUniqueId(), k -> new ArrayDeque<>());
// Удаляем старые timestamp'ы
while (!timestamps.isEmpty() && now - timestamps.peekFirst() > windowMs) {
timestamps.pollFirst();
}
if (timestamps.size() >= maxMessages) {
return FilterResult.blocked(config.getString("filters.flood.block-message",
"&#CF6679Не флудите!"));
}
timestamps.addLast(now);
return FilterResult.ok(null);
}
public void clearPlayer(UUID uuid) {
floodTracker.remove(uuid);
}
}

View file

@ -0,0 +1,83 @@
package com.loki.lochat.core.filter.filters;
import com.loki.lochat.core.filter.FilterResult;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class SpamFilter {
private final FileConfiguration config;
private final Map<UUID, Deque<String>> spamTracker = new ConcurrentHashMap<>();
public SpamFilter(FileConfiguration config) {
this.config = config;
}
public FilterResult filter(Player player, String message) {
if (player.hasPermission("lochat.bypass.spam")) {
return FilterResult.ok(message);
}
int maxSimilar = config.getInt("filters.spam.max-similar-messages", 3);
int threshold = config.getInt("filters.spam.similarity-threshold", 80);
Deque<String> history = spamTracker.computeIfAbsent(
player.getUniqueId(), k -> new ArrayDeque<>());
// Считаем похожие сообщения
long similarCount = history.stream()
.filter(prev -> similarity(prev, message) >= threshold)
.count();
if (similarCount >= maxSimilar) {
return FilterResult.blocked(config.getString("filters.spam.block-message",
"&#CF6679Не отправляйте одинаковые сообщения"));
}
// Добавляем в историю (храним последние 10)
history.addLast(message);
if (history.size() > 10) history.pollFirst();
return FilterResult.ok(null);
}
private int similarity(String a, String b) {
if (a.equals(b)) return 100;
int maxLen = Math.max(a.length(), b.length());
if (maxLen == 0) return 100;
int dist = levenshtein(
a.toLowerCase().substring(0, Math.min(a.length(), 50)),
b.toLowerCase().substring(0, Math.min(b.length(), 50)));
return 100 - (dist * 100 / maxLen);
}
private int levenshtein(String a, String b) {
int[] dp = new int[b.length() + 1];
for (int j = 0; j <= b.length(); j++) dp[j] = j;
for (int i = 1; i <= a.length(); i++) {
int prev = dp[0];
dp[0] = i;
for (int j = 1; j <= b.length(); j++) {
int temp = dp[j];
dp[j] = a.charAt(i - 1) == b.charAt(j - 1)
? prev
: 1 + Math.min(prev, Math.min(dp[j], dp[j - 1]));
prev = temp;
}
}
return dp[b.length()];
}
public void clearPlayer(UUID uuid) {
spamTracker.remove(uuid);
}
}

View file

@ -0,0 +1,72 @@
package com.loki.lochat.core.filter.filters;
import com.loki.lochat.core.filter.FilterResult;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
public class SwearFilter {
private final boolean enabled;
private final boolean replaceMode;
private final String replacement;
private final Set<Pattern> swearPatterns = new HashSet<>();
public SwearFilter(FileConfiguration config, JavaPlugin plugin) {
this.enabled = config.getBoolean("filters.swear.enabled", true);
this.replaceMode = config.getBoolean("filters.swear.replace", false);
this.replacement = config.getString("filters.swear.replacement", "***");
if (enabled) {
loadWordList(plugin);
}
}
private void loadWordList(JavaPlugin plugin) {
try (InputStream is = plugin.getResource("badwords.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
reader.lines()
.map(String::trim)
.filter(line -> !line.isEmpty() && !line.startsWith("#"))
.forEach(word -> swearPatterns.add(
Pattern.compile("\\b" + Pattern.quote(word) + "\\b", Pattern.CASE_INSENSITIVE)
));
} catch (Exception e) {
plugin.getLogger().warning("Failed to load badwords.txt: " + e.getMessage());
}
}
public FilterResult filter(Player player, String message) {
if (!enabled || player.hasPermission("lochat.bypass.swear")) {
return FilterResult.ok(message);
}
for (Pattern pattern : swearPatterns) {
if (pattern.matcher(message).find()) {
if (replaceMode) {
return FilterResult.ok(replaceSwear(message));
}
return FilterResult.blocked("Сообщение содержит запрещенные слова");
}
}
return FilterResult.ok(message);
}
private String replaceSwear(String message) {
String result = message;
for (Pattern pattern : swearPatterns) {
result = pattern.matcher(result).replaceAll(replacement);
}
return result;
}
}

View file

@ -0,0 +1,106 @@
package com.loki.lochat.core.filter.filters;
import com.loki.lochat.core.filter.FilterResult;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class UrlFilter {
private static final Pattern URL_PATTERN = Pattern.compile(
"(https?://)?([\\w-]+\\.)+[\\w-]+(/[\\w-./?%&=]*)?", Pattern.CASE_INSENSITIVE
);
private static final Pattern IP_PATTERN = Pattern.compile(
"\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b"
);
private static final Pattern HIDDEN_URL_PATTERN = Pattern.compile(
"(?i)(h\\s*t\\s*t\\s*p|w\\s*w\\s*w|d\\s*o\\s*t\\s*c\\s*o\\s*m)"
);
private final FileConfiguration config;
private final Set<String> whitelistedDomains;
private final Set<String> blacklistedDomains;
public UrlFilter(FileConfiguration config) {
this.config = config;
this.whitelistedDomains = new HashSet<>(config.getStringList("filters.advertising.whitelist"));
this.blacklistedDomains = new HashSet<>(config.getStringList("filters.advertising.blacklist"));
}
public FilterResult filterUrls(Player player, String message) {
if (player.hasPermission("lochat.bypass.urlfilter")) {
return FilterResult.ok(message);
}
Matcher matcher = URL_PATTERN.matcher(message);
while (matcher.find()) {
String domain = extractDomain(matcher.group());
if (!whitelistedDomains.isEmpty()) {
boolean ok = whitelistedDomains.stream()
.anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase()));
if (!ok) {
return FilterResult.blocked(config.getString("filters.url.blocked-message",
"&#CF6679Ссылки запрещены!"));
}
}
if (!blacklistedDomains.isEmpty()) {
boolean bad = blacklistedDomains.stream()
.anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase()));
if (bad) {
return FilterResult.blocked(config.getString("filters.url.blocked-message",
"&#CF6679Эта ссылка запрещена!"));
}
}
}
return FilterResult.ok(message);
}
public FilterResult filterIPs(Player player, String message) {
if (player.hasPermission("lochat.bypass.ipfilter")) {
return FilterResult.ok(message);
}
Matcher matcher = IP_PATTERN.matcher(message);
if (matcher.find()) {
if (config.getBoolean("filters.ip.block", false)) {
return FilterResult.blocked(config.getString("filters.ip.blocked-message",
"&#CF6679IP адреса запрещены!"));
}
return FilterResult.ok(matcher.replaceAll(
config.getString("filters.ip.replacement", "[IP скрыт]")));
}
return FilterResult.ok(message);
}
public FilterResult filterHiddenUrls(Player player, String message) {
if (!config.getBoolean("filters.advertising.block-hidden-urls", true)) {
return FilterResult.ok(message);
}
if (player.hasPermission("lochat.bypass.hiddenurls")) {
return FilterResult.ok(message);
}
if (HIDDEN_URL_PATTERN.matcher(message).find()) {
return FilterResult.blocked(config.getString("filters.advertising.blocked-message",
"&#CF6679Реклама запрещена!"));
}
return FilterResult.ok(message);
}
private String extractDomain(String url) {
url = url.replaceFirst("^https?://", "");
int slash = url.indexOf('/');
return slash != -1 ? url.substring(0, slash) : url;
}
}

View file

@ -27,19 +27,16 @@ public class ServiceRegistry {
register(ChatService.class, new ChatServiceImpl(plugin, this));
register(MuteService.class, new MuteServiceImpl(plugin));
// CooldownService регистрируем раньше PlayerDataService, чтобы передать его как зависимость
CooldownServiceImpl cooldownService = new CooldownServiceImpl();
register(CooldownService.class, cooldownService);
// ✅ NEW: Объединённый PlayerService (Cooldown + PlayerData)
PlayerService playerService = new PlayerServiceImpl(plugin);
register(PlayerService.class, playerService);
register(MessageService.class, new MessageServiceImpl(plugin, this));
// Передаём тот же экземпляр CooldownService, а не новый — иначе очистка при выходе не работает
register(PlayerDataService.class, new PlayerDataServiceImpl(plugin, cooldownService));
register(PMService.class, new PMServiceImpl());
register(IgnoreService.class, new IgnoreServiceImpl(plugin));
SpyService spyService = new SpyServiceImpl(messageConfig);
register(SpyService.class, spyService);
// ✅ NEW: Объединённый MessagingService (PM + Spy + Ignore)
MessagingService messagingService = new MessagingServiceImpl(plugin, messageConfig);
register(MessagingService.class, messagingService);
register(MentionService.class, new MentionServiceImpl(configManager));
register(NickService.class, new NickServiceImpl(plugin));
register(PunishmentService.class, new PunishmentServiceImpl(plugin, configManager.getMessagesConfig()));
@ -48,6 +45,14 @@ public class ServiceRegistry {
public <T> void register(Class<T> serviceClass, T implementation) {
services.put(serviceClass, implementation);
}
/**
* Регистрирует сервис с возможностью приведения типов (для обратной совместимости)
*/
@SuppressWarnings("unchecked")
public void registerCompat(Class<?> serviceClass, Object implementation) {
services.put(serviceClass, implementation);
}
@SuppressWarnings("unchecked")
public <T> T get(Class<T> serviceClass) {

View file

@ -3,8 +3,8 @@ package com.loki.lochat.core.service;
import com.loki.lochat.api.service.ChatService;
import com.loki.lochat.core.registry.ServiceRegistry;
import com.loki.lochat.renderer.EnhancedChatRenderer;
import com.loki.lochat.util.DistanceUtil;
import com.loki.lochat.util.FoliaUtil;
import com.loki.lochat.utils.DistanceUtil;
import com.loki.lochat.utils.FoliaUtil;
import com.loki.lochat.utils.ChatFormatter;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;

View file

@ -1,52 +0,0 @@
package com.loki.lochat.core.service;
import com.loki.lochat.api.service.CooldownService;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Реализация сервиса кулдаунов
*/
public class CooldownServiceImpl implements CooldownService {
private final Map<UUID, Long> globalCooldowns = new ConcurrentHashMap<>();
private final Map<UUID, Long> localCooldowns = new ConcurrentHashMap<>();
@Override
public boolean isOnCooldown(UUID player, String type, int cooldownSeconds) {
Map<UUID, Long> cooldowns = getCooldownMap(type);
Long lastTime = cooldowns.get(player);
if (lastTime == null) return false;
long elapsed = (System.currentTimeMillis() - lastTime) / 1000;
return elapsed < cooldownSeconds;
}
@Override
public int getRemainingCooldown(UUID player, String type, int cooldownSeconds) {
Map<UUID, Long> cooldowns = getCooldownMap(type);
Long lastTime = cooldowns.get(player);
if (lastTime == null) return 0;
long elapsed = (System.currentTimeMillis() - lastTime) / 1000;
return Math.max(0, cooldownSeconds - (int) elapsed);
}
@Override
public void setCooldown(UUID player, String type) {
getCooldownMap(type).put(player, System.currentTimeMillis());
}
@Override
public void removeCooldown(UUID player) {
globalCooldowns.remove(player);
localCooldowns.remove(player);
}
private Map<UUID, Long> getCooldownMap(String type) {
return type.equals("global") ? globalCooldowns : localCooldowns;
}
}

View file

@ -1,115 +0,0 @@
package com.loki.lochat.core.service;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.util.FoliaUtil;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Реализация сервиса игнорирования
*/
public class IgnoreServiceImpl implements IgnoreService {
private final JavaPlugin plugin;
private final File ignoreFile;
private final Map<UUID, Set<UUID>> ignoreMap = new ConcurrentHashMap<>();
public IgnoreServiceImpl(JavaPlugin plugin) {
this.plugin = plugin;
this.ignoreFile = new File(plugin.getDataFolder(), "ignores.yml");
load();
}
@Override
public void load() {
if (!ignoreFile.exists()) return;
YamlConfiguration config = YamlConfiguration.loadConfiguration(ignoreFile);
for (String key : config.getKeys(false)) {
try {
UUID uuid = UUID.fromString(key);
List<String> ignoredList = config.getStringList(key);
Set<UUID> ignored = ConcurrentHashMap.newKeySet();
for (String ignoredStr : ignoredList) {
try {
ignored.add(UUID.fromString(ignoredStr));
} catch (Exception ignored2) {
}
}
if (!ignored.isEmpty()) {
ignoreMap.put(uuid, ignored);
}
} catch (Exception ignored) {
}
}
}
@Override
public void save() {
YamlConfiguration config = new YamlConfiguration();
for (Map.Entry<UUID, Set<UUID>> entry : ignoreMap.entrySet()) {
List<String> ignoredList = new ArrayList<>();
for (UUID ignored : entry.getValue()) {
ignoredList.add(ignored.toString());
}
config.set(entry.getKey().toString(), ignoredList);
}
try {
config.save(ignoreFile);
} catch (IOException e) {
plugin.getLogger().severe("Не удалось сохранить игноры: " + e.getMessage());
}
}
@Override
public boolean isIgnoring(UUID player, UUID target) {
Set<UUID> ignored = ignoreMap.get(player);
return ignored != null && ignored.contains(target);
}
@Override
public boolean addIgnore(UUID player, UUID target) {
Set<UUID> ignored = ignoreMap.computeIfAbsent(player, k -> ConcurrentHashMap.newKeySet());
boolean added = ignored.add(target);
if (added) saveAsync();
return added;
}
@Override
public boolean removeIgnore(UUID player, UUID target) {
Set<UUID> ignored = ignoreMap.get(player);
if (ignored == null) return false;
boolean removed = ignored.remove(target);
if (removed) saveAsync();
return removed;
}
@Override
public Set<UUID> getIgnoredPlayers(UUID player) {
return ignoreMap.getOrDefault(player, Collections.emptySet());
}
@Override
public int getIgnoredCount(UUID player) {
Set<UUID> ignored = ignoreMap.get(player);
return ignored == null ? 0 : ignored.size();
}
@Override
public void clearIgnores(UUID player) {
ignoreMap.remove(player);
saveAsync();
}
private void saveAsync() {
FoliaUtil.runAsync(plugin, this::save);
}
}

View file

@ -1,7 +1,7 @@
package com.loki.lochat.core.service;
import com.loki.lochat.api.filter.MessageFilter;
import com.loki.lochat.api.service.CooldownService;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.api.service.MessageService;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.core.filter.CooldownFilter;
@ -26,7 +26,7 @@ public class MessageServiceImpl implements MessageService {
// Регистрируем фильтры в порядке применения
// MuteFilter теперь получает plugin для доступа к HardcodedMessages
filters.add(new MuteFilter(registry.get(MuteService.class), plugin));
filters.add(new CooldownFilter(registry.get(CooldownService.class), plugin));
filters.add(new CooldownFilter(registry.get(PlayerService.class), plugin));
}
@Override

View file

@ -0,0 +1,219 @@
package com.loki.lochat.core.service;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.config.MessageConfig;
import com.loki.lochat.utils.FoliaUtil;
import com.loki.lochat.utils.ChatFormatter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Объединённая реализация сервиса общения
* Включает: PM, Spy, Ignore
*/
public class MessagingServiceImpl implements MessagingService {
private final JavaPlugin plugin;
private final MessageConfig messageConfig;
private final File ignoreFile;
// PM state
private final Map<UUID, UUID> lastConversation = new ConcurrentHashMap<>();
// Spy state
private final Set<UUID> spyEnabled = ConcurrentHashMap.newKeySet();
// Ignore state
private final Map<UUID, Set<UUID>> ignoreMap = new ConcurrentHashMap<>();
public MessagingServiceImpl(JavaPlugin plugin, MessageConfig messageConfig) {
this.plugin = plugin;
this.messageConfig = messageConfig;
this.ignoreFile = new File(plugin.getDataFolder(), "ignores.yml");
load();
}
// ========== PM Implementation ==========
@Override
public void setLastConversation(UUID player, UUID target) {
lastConversation.put(player, target);
}
@Override
public Optional<UUID> getLastConversation(UUID player) {
return Optional.ofNullable(lastConversation.get(player));
}
@Override
public void removeConversation(UUID player) {
lastConversation.remove(player);
}
@Override
public boolean hasConversation(UUID player) {
return lastConversation.containsKey(player);
}
// ========== Spy Implementation ==========
@Override
public boolean toggleSpy(UUID player) {
if (spyEnabled.contains(player)) {
spyEnabled.remove(player);
return false;
} else {
spyEnabled.add(player);
return true;
}
}
@Override
public boolean isSpying(UUID player) {
return spyEnabled.contains(player);
}
@Override
public void broadcastPM(Player sender, Player receiver, String message) {
String format = messageConfig.get("spy.format");
for (UUID spyUuid : spyEnabled) {
Player spy = Bukkit.getPlayer(spyUuid);
if (spy != null && spy.isOnline() && !spy.equals(sender) && !spy.equals(receiver)) {
spy.sendMessage(ChatFormatter.parse(format
.replace("{sender}", sender.getName())
.replace("{receiver}", receiver.getName())
.replace("{message}", message)));
}
}
}
@Override
public void sendToSpies(Player sender, Component message, boolean isGlobal) {
if (spyEnabled.isEmpty()) return;
String chatType = isGlobal ? "Global" : "Local";
String plainMessage = PlainTextComponentSerializer.plainText().serialize(message);
String format = messageConfig.get("spy.chat-format",
"§7[SPY] §e{type} §7{sender}: §f{message}");
for (UUID spyUuid : spyEnabled) {
Player spy = Bukkit.getPlayer(spyUuid);
if (spy != null && spy.isOnline() && !spy.equals(sender)) {
spy.sendMessage(ChatFormatter.parse(format
.replace("{type}", chatType)
.replace("{sender}", sender.getName())
.replace("{message}", plainMessage)));
}
}
}
@Override
public void removeSpy(UUID player) {
spyEnabled.remove(player);
}
// ========== Ignore Implementation ==========
@Override
public boolean isIgnoring(UUID player, UUID target) {
Set<UUID> ignored = ignoreMap.get(player);
return ignored != null && ignored.contains(target);
}
@Override
public boolean addIgnore(UUID player, UUID target) {
Set<UUID> ignored = ignoreMap.computeIfAbsent(player, k -> ConcurrentHashMap.newKeySet());
boolean added = ignored.add(target);
if (added) saveAsync();
return added;
}
@Override
public boolean removeIgnore(UUID player, UUID target) {
Set<UUID> ignored = ignoreMap.get(player);
if (ignored == null) return false;
boolean removed = ignored.remove(target);
if (removed) saveAsync();
return removed;
}
@Override
public Set<UUID> getIgnoredPlayers(UUID player) {
return ignoreMap.getOrDefault(player, Collections.emptySet());
}
@Override
public int getIgnoredCount(UUID player) {
Set<UUID> ignored = ignoreMap.get(player);
return ignored == null ? 0 : ignored.size();
}
@Override
public void clearIgnores(UUID player) {
ignoreMap.remove(player);
saveAsync();
}
// ========== Persistence ==========
@Override
public void load() {
if (!ignoreFile.exists()) return;
YamlConfiguration config = YamlConfiguration.loadConfiguration(ignoreFile);
for (String key : config.getKeys(false)) {
try {
UUID uuid = UUID.fromString(key);
List<String> ignoredList = config.getStringList(key);
Set<UUID> ignored = ConcurrentHashMap.newKeySet();
for (String ignoredStr : ignoredList) {
try {
ignored.add(UUID.fromString(ignoredStr));
} catch (Exception e) {
// Ignore invalid UUID
}
}
if (!ignored.isEmpty()) {
ignoreMap.put(uuid, ignored);
}
} catch (Exception e) {
// Ignore invalid entry
}
}
}
@Override
public void save() {
YamlConfiguration config = new YamlConfiguration();
for (Map.Entry<UUID, Set<UUID>> entry : ignoreMap.entrySet()) {
List<String> ignoredList = new ArrayList<>();
for (UUID ignored : entry.getValue()) {
ignoredList.add(ignored.toString());
}
config.set(entry.getKey().toString(), ignoredList);
}
try {
config.save(ignoreFile);
} catch (IOException e) {
plugin.getLogger().severe("Не удалось сохранить игноры: " + e.getMessage());
}
}
private void saveAsync() {
FoliaUtil.runAsync(plugin, this::save);
}
}

View file

@ -1,52 +1,66 @@
package com.loki.lochat.core.service;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.loki.lochat.api.service.MuteService;
import com.loki.lochat.data.model.MuteData;
import com.loki.lochat.util.FoliaUtil;
import com.loki.lochat.utils.FoliaUtil;
import com.loki.lochat.utils.TimeFormatter;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.*;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Сервис мутов с интеграцией Simple Voice Chat
*/
public class MuteServiceImpl implements MuteService {
private final JavaPlugin plugin;
private final Map<UUID, MuteData> mutes = new ConcurrentHashMap<>();
private final MuteHistoryManager historyManager;
private final File dataFile;
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
private final Map<UUID, MuteData> mutes = new HashMap<>();
private final Map<UUID, List<MuteData.MuteHistoryEntry>> history = new HashMap<>();
private Object voicechatService; // Используем Object для опциональной зависимости
public MuteServiceImpl(JavaPlugin plugin) {
this.plugin = plugin;
this.dataFile = new File(plugin.getDataFolder(), "mutes.json");
this.historyManager = new MuteHistoryManager(new File(plugin.getDataFolder(), "mute-history.json"));
initVoiceChat();
load();
}
private void initVoiceChat() {
if (Bukkit.getPluginManager().isPluginEnabled("voicechat")) {
try {
// Проверяем наличие класса через рефлексию
Class.forName("de.maxhenkel.voicechat.api.BukkitVoicechatService");
voicechatService = Bukkit.getServicesManager().load(
Class.forName("de.maxhenkel.voicechat.api.BukkitVoicechatService")
);
if (voicechatService != null) {
plugin.getLogger().info("Simple Voice Chat integration enabled!");
}
} catch (ClassNotFoundException e) {
plugin.getLogger().info("VoiceChat API not found, voice mute disabled");
} catch (Exception e) {
plugin.getLogger().warning("Failed to hook into Simple Voice Chat: " + e.getMessage());
}
}
}
@Override
public void mute(UUID uuid, String playerName, long duration, String reason, String mutedBy) {
if (uuid == null) {
plugin.getLogger().warning("Attempted to mute player with null UUID");
return;
}
if (playerName == null || playerName.isEmpty()) {
plugin.getLogger().warning("Attempted to mute player with null or empty name");
if (uuid == null || playerName == null || playerName.isEmpty()) {
plugin.getLogger().warning("Attempted to mute player with invalid data");
return;
}
long endTime = duration > 0 ? System.currentTimeMillis() + duration : 0;
MuteData data = new MuteData(uuid, playerName, endTime, reason, mutedBy);
mutes.put(uuid, data);
historyManager.addEntry(uuid, playerName, duration, reason, mutedBy);
addToHistory(uuid, playerName, duration, reason, mutedBy);
// Мутим в голосовом чате
muteVoiceChat(uuid, true);
saveAsync();
plugin.getLogger().info("Player " + playerName + " muted by " + mutedBy +
@ -63,7 +77,11 @@ public class MuteServiceImpl implements MuteService {
MuteData removed = mutes.remove(uuid);
if (removed != null) {
historyManager.updateUnmute(uuid, unmutedBy);
updateUnmuteInHistory(uuid, unmutedBy);
// Размутим в голосовом чате
muteVoiceChat(uuid, false);
saveAsync();
plugin.getLogger().info("Player " + removed.getPlayerName() + " unmuted by " + unmutedBy);
return true;
@ -80,13 +98,10 @@ public class MuteServiceImpl implements MuteService {
if (!isExpired(data)) return true;
// Атомарно удаляем только если значение не изменилось между get() и remove().
// ConcurrentHashMap.remove(key, value) удаляет запись лишь когда она точно та же.
boolean removed = mutes.remove(uuid, data);
if (removed) {
saveAsync();
plugin.getLogger().info("Mute expired for player " + data.getPlayerName());
}
mutes.remove(uuid);
muteVoiceChat(uuid, false);
saveAsync();
plugin.getLogger().info("Mute expired for player " + data.getPlayerName());
return false;
}
@ -98,12 +113,8 @@ public class MuteServiceImpl implements MuteService {
@Override
public long getRemainingTime(UUID uuid) {
MuteData data = mutes.get(uuid);
if (data == null) {
return 0;
}
if (data.getEndTime() == 0) {
return -1;
}
if (data == null) return 0;
if (data.getEndTime() == 0) return -1;
return Math.max(0, data.getEndTime() - System.currentTimeMillis());
}
@ -119,9 +130,7 @@ public class MuteServiceImpl implements MuteService {
@Override
public long getMaxDuration(Player player) {
if (player.hasPermission("lochat.mute.dur.perm")) {
return 0;
}
if (player.hasPermission("lochat.mute.dur.perm")) return 0;
String[] durations = {"30d", "14d", "7d", "3d", "1d", "12h", "6h", "1h", "30m", "10m"};
for (String dur : durations) {
@ -129,54 +138,44 @@ public class MuteServiceImpl implements MuteService {
return parseTime(dur);
}
}
return -1;
}
@Override
public boolean canMuteForDuration(Player player, long duration) {
if (player.hasPermission("lochat.mute.dur.perm")) {
return true;
}
if (duration == 0) {
return false;
}
if (player.hasPermission("lochat.mute.dur.perm")) return true;
if (duration == 0) return false;
long maxDuration = getMaxDuration(player);
if (maxDuration == -1) {
return false;
}
if (maxDuration == 0) {
return true;
}
if (maxDuration == -1) return false;
if (maxDuration == 0) return true;
return duration <= maxDuration;
}
@Override
public List<MuteData.MuteHistoryEntry> getPlayerHistory(UUID uuid) {
return historyManager.getHistory(uuid);
return history.getOrDefault(uuid, new ArrayList<>());
}
@Override
public List<MuteData.MuteHistoryEntry> getMutesByOperator(String operatorName) {
List<MuteData.MuteHistoryEntry> result = new ArrayList<>();
mutes.keySet().forEach(uuid -> {
historyManager.getHistory(uuid).stream()
.filter(entry -> entry.mutedBy != null && entry.mutedBy.equalsIgnoreCase(operatorName))
.forEach(result::add);
});
history.values().forEach(entries ->
entries.stream()
.filter(e -> e.mutedBy != null && e.mutedBy.equalsIgnoreCase(operatorName))
.forEach(result::add)
);
result.sort((a, b) -> Long.compare(b.mutedAt, a.mutedAt));
return result;
}
@Override
public UUID getUUIDByName(String name) {
for (UUID uuid : mutes.keySet()) {
for (MuteData.MuteHistoryEntry entry : historyManager.getHistory(uuid)) {
if (entry.playerName != null && entry.playerName.equalsIgnoreCase(name)) {
return uuid;
for (Map.Entry<UUID, List<MuteData.MuteHistoryEntry>> entry : history.entrySet()) {
for (MuteData.MuteHistoryEntry e : entry.getValue()) {
if (e.playerName != null && e.playerName.equalsIgnoreCase(name)) {
return entry.getKey();
}
}
}
@ -185,7 +184,7 @@ public class MuteServiceImpl implements MuteService {
@Override
public Map<UUID, MuteData> getActiveMutes() {
mutes.entrySet().removeIf(entry -> isExpired(entry.getValue()));
mutes.entrySet().removeIf(e -> isExpired(e.getValue()));
return new HashMap<>(mutes);
}
@ -200,54 +199,60 @@ public class MuteServiceImpl implements MuteService {
@Override
public void save() {
// TODO: Implement save to file
}
// ========== Voice Chat Integration ==========
private void muteVoiceChat(UUID uuid, boolean mute) {
if (voicechatService == null) return;
try {
ensureDataFolderExists();
saveMutes();
historyManager.save();
} catch (IOException e) {
plugin.getLogger().warning("Error saving mutes: " + e.getMessage());
Player player = Bukkit.getPlayer(uuid);
if (player != null && player.isOnline()) {
// Используем рефлексию для вызова метода
voicechatService.getClass()
.getMethod("getVoicechat")
.invoke(voicechatService)
.getClass()
.getMethod("setMuted", UUID.class, boolean.class)
.invoke(voicechatService.getClass().getMethod("getVoicechat").invoke(voicechatService),
player.getUniqueId(), mute);
}
} catch (Exception e) {
plugin.getLogger().warning("Failed to " + (mute ? "mute" : "unmute") +
" player in voice chat: " + e.getMessage());
}
}
// ========== Private Methods ==========
private boolean isExpired(MuteData data) {
return data.getEndTime() > 0 && System.currentTimeMillis() > data.getEndTime();
}
private void ensureDataFolderExists() {
if (!dataFile.getParentFile().exists()) {
dataFile.getParentFile().mkdirs();
}
private void addToHistory(UUID uuid, String playerName, long duration, String reason, String mutedBy) {
List<MuteData.MuteHistoryEntry> entries = history.computeIfAbsent(uuid, k -> new ArrayList<>());
entries.add(new MuteData.MuteHistoryEntry(
playerName, duration, reason, mutedBy, System.currentTimeMillis(), false, null, 0
));
}
private void saveMutes() throws IOException {
Map<String, MuteData> toSave = new ConcurrentHashMap<>();
mutes.forEach((uuid, data) -> toSave.put(uuid.toString(), data));
try (Writer writer = new FileWriter(dataFile)) {
gson.toJson(toSave, writer);
private void updateUnmuteInHistory(UUID uuid, String unmutedBy) {
List<MuteData.MuteHistoryEntry> entries = history.get(uuid);
if (entries != null && !entries.isEmpty()) {
MuteData.MuteHistoryEntry last = entries.get(entries.size() - 1);
if (!last.unmuted) {
entries.set(entries.size() - 1, new MuteData.MuteHistoryEntry(
last.playerName, last.duration, last.reason, last.mutedBy, last.mutedAt,
true, unmutedBy, System.currentTimeMillis()
));
}
}
}
private void load() {
if (!dataFile.exists()) {
return;
}
try (Reader reader = new FileReader(dataFile)) {
Type type = new TypeToken<Map<String, MuteData>>() {
}.getType();
Map<String, MuteData> loaded = gson.fromJson(reader, type);
if (loaded != null) {
loaded.forEach((key, value) -> {
try {
mutes.put(UUID.fromString(key), value);
} catch (IllegalArgumentException ignored) {
}
});
}
} catch (IOException e) {
plugin.getLogger().warning("Error loading mutes: " + e.getMessage());
}
// TODO: Implement load from file
}
private void saveAsync() {

View file

@ -1,35 +0,0 @@
package com.loki.lochat.core.service;
import com.loki.lochat.api.service.PMService;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Реализация сервиса личных сообщений
*/
public class PMServiceImpl implements PMService {
private final Map<UUID, UUID> lastConversation = new ConcurrentHashMap<>();
@Override
public void setLastConversation(UUID player, UUID target) {
lastConversation.put(player, target);
}
@Override
public Optional<UUID> getLastConversation(UUID player) {
return Optional.ofNullable(lastConversation.get(player));
}
@Override
public void removeConversation(UUID player) {
lastConversation.remove(player);
}
@Override
public boolean hasConversation(UUID player) {
return lastConversation.containsKey(player);
}
}

View file

@ -1,8 +1,7 @@
package com.loki.lochat.core.service;
import com.loki.lochat.api.service.CooldownService;
import com.loki.lochat.api.service.PlayerDataService;
import com.loki.lochat.util.FoliaUtil;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.utils.FoliaUtil;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
@ -14,61 +13,100 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* Реализация сервиса данных игроков.
* Хранит и сохраняет статистику сообщений в statistics.yml.
* Объединённая реализация сервиса данных игроков
* Включает: Cooldown, PlayerData (статистика)
*/
public class PlayerDataServiceImpl implements PlayerDataService {
public class PlayerServiceImpl implements PlayerService {
private final JavaPlugin plugin;
private final CooldownService cooldownService;
// Счётчики сообщений per-player (сбрасываются при выходе, сохраняются в файл)
// Cooldown state
private final Map<UUID, Long> globalCooldowns = new ConcurrentHashMap<>();
private final Map<UUID, Long> localCooldowns = new ConcurrentHashMap<>();
// Statistics state
private final Map<UUID, Long> playerMessages = new ConcurrentHashMap<>();
// Глобальные счётчики по типу чата
private final AtomicLong globalChatCount = new AtomicLong(0);
private final AtomicLong localChatCount = new AtomicLong(0);
private final AtomicLong pmCount = new AtomicLong(0);
private final AtomicLong totalMessages = new AtomicLong(0);
private final AtomicLong globalChatCount = new AtomicLong(0);
private final AtomicLong localChatCount = new AtomicLong(0);
private final AtomicLong pmCount = new AtomicLong(0);
private final AtomicLong totalMessages = new AtomicLong(0);
private final File statsFile;
private YamlConfiguration stats;
public PlayerDataServiceImpl(JavaPlugin plugin, CooldownService cooldownService) {
public PlayerServiceImpl(JavaPlugin plugin) {
this.plugin = plugin;
this.cooldownService = cooldownService;
this.statsFile = new File(plugin.getDataFolder(), "data/statistics.yml");
load();
}
// ── Публичные методы статистики ────────────────────────────────────────────
/** Вызывать при каждом сообщении в чате */
// ========== Cooldown Implementation ==========
@Override
public boolean isOnCooldown(UUID player, String type, int cooldownSeconds) {
Map<UUID, Long> cooldowns = getCooldownMap(type);
Long lastTime = cooldowns.get(player);
if (lastTime == null) return false;
long elapsed = (System.currentTimeMillis() - lastTime) / 1000;
return elapsed < cooldownSeconds;
}
@Override
public int getRemainingCooldown(UUID player, String type, int cooldownSeconds) {
Map<UUID, Long> cooldowns = getCooldownMap(type);
Long lastTime = cooldowns.get(player);
if (lastTime == null) return 0;
long elapsed = (System.currentTimeMillis() - lastTime) / 1000;
return Math.max(0, cooldownSeconds - (int) elapsed);
}
@Override
public void setCooldown(UUID player, String type) {
getCooldownMap(type).put(player, System.currentTimeMillis());
}
@Override
public void removeCooldown(UUID player) {
globalCooldowns.remove(player);
localCooldowns.remove(player);
}
private Map<UUID, Long> getCooldownMap(String type) {
return type.equals("global") ? globalCooldowns : localCooldowns;
}
// ========== Statistics Implementation ==========
@Override
public void recordMessage(UUID player, String chatType) {
playerMessages.compute(player, (k, v) -> (v == null ? 0L : v) + 1L);
totalMessages.incrementAndGet();
switch (chatType) {
case "global" -> globalChatCount.incrementAndGet();
case "local" -> localChatCount.incrementAndGet();
case "pm" -> pmCount.incrementAndGet();
case "local" -> localChatCount.incrementAndGet();
case "pm" -> pmCount.incrementAndGet();
}
}
@Override
public long getPlayerMessages(UUID player) {
return playerMessages.getOrDefault(player, 0L);
}
// ── PlayerDataService ──────────────────────────────────────────────────────
@Override
public void clearPlayerData(UUID player) {
cooldownService.removeCooldown(player);
removeCooldown(player);
// Сохраняем статистику игрока в файл при выходе
long msgs = playerMessages.getOrDefault(player, 0L);
if (msgs > 0) {
FoliaUtil.runAsync(plugin, () -> persistPlayerMessages(player, msgs));
}
}
@Override
public void saveAll() {
try {
@ -88,9 +126,9 @@ public class PlayerDataServiceImpl implements PlayerDataService {
plugin.getLogger().warning("[LoChat] Failed to save statistics: " + e.getMessage());
}
}
// ── Внутренние методы ──────────────────────────────────────────────────────
// ========== Private Methods ==========
private void persistPlayerMessages(UUID uuid, long count) {
try {
String path = "top-players.by-messages." + uuid;
@ -103,7 +141,7 @@ public class PlayerDataServiceImpl implements PlayerDataService {
plugin.getLogger().warning("[LoChat] Failed to persist player stats: " + e.getMessage());
}
}
private void load() {
ensureDir();
if (!statsFile.exists()) {
@ -111,7 +149,7 @@ public class PlayerDataServiceImpl implements PlayerDataService {
}
stats = YamlConfiguration.loadConfiguration(statsFile);
}
private void ensureDir() {
File dir = statsFile.getParentFile();
if (!dir.exists()) dir.mkdirs();

View file

@ -7,7 +7,7 @@ import com.loki.lochat.api.service.PunishmentService;
import com.loki.lochat.config.MessagesConfig;
import com.loki.lochat.data.model.BanRecord;
import com.loki.lochat.data.model.WarnEntry;
import com.loki.lochat.util.FoliaUtil;
import com.loki.lochat.utils.FoliaUtil;
import com.loki.lochat.utils.ChatFormatter;
import com.loki.lochat.utils.TimeFormatter;
import net.kyori.adventure.text.Component;

View file

@ -1,80 +0,0 @@
package com.loki.lochat.core.service;
import com.loki.lochat.api.service.SpyService;
import com.loki.lochat.config.MessageConfig;
import com.loki.lochat.utils.ChatFormatter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Реализация сервиса шпионажа за чатом
*/
public class SpyServiceImpl implements SpyService {
private final MessageConfig messageConfig;
private final Set<UUID> spyEnabled = ConcurrentHashMap.newKeySet();
public SpyServiceImpl(MessageConfig messageConfig) {
this.messageConfig = messageConfig;
}
@Override
public boolean toggleSpy(UUID player) {
if (spyEnabled.contains(player)) {
spyEnabled.remove(player);
return false;
} else {
spyEnabled.add(player);
return true;
}
}
@Override
public boolean isSpying(UUID player) {
return spyEnabled.contains(player);
}
@Override
public void broadcastPM(Player sender, Player receiver, String message) {
String format = messageConfig.get("spy.format");
for (UUID spyUuid : spyEnabled) {
Player spy = Bukkit.getPlayer(spyUuid);
if (spy != null && spy.isOnline() && !spy.equals(sender) && !spy.equals(receiver)) {
spy.sendMessage(ChatFormatter.parse(format
.replace("{sender}", sender.getName())
.replace("{receiver}", receiver.getName())
.replace("{message}", message)));
}
}
}
@Override
public void sendToSpies(Player sender, Component message, boolean isGlobal) {
if (spyEnabled.isEmpty()) return;
String chatType = isGlobal ? "Global" : "Local";
String plainMessage = PlainTextComponentSerializer.plainText().serialize(message);
String format = messageConfig.get("spy.chat-format",
"§7[SPY] §e{type} §7{sender}: §f{message}");
for (UUID spyUuid : spyEnabled) {
Player spy = Bukkit.getPlayer(spyUuid);
if (spy != null && spy.isOnline() && !spy.equals(sender)) {
spy.sendMessage(ChatFormatter.parse(format
.replace("{type}", chatType)
.replace("{sender}", sender.getName())
.replace("{message}", plainMessage)));
}
}
}
@Override
public void removeSpy(UUID player) {
spyEnabled.remove(player);
}
}

View file

@ -0,0 +1,117 @@
package com.loki.lochat.core.service.mute;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.loki.lochat.data.model.MuteData;
import com.loki.lochat.utils.FoliaUtil;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.*;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Хранилище данных о мутах
*/
public class MuteDataStorage {
private final JavaPlugin plugin;
private final Map<UUID, MuteData> mutes = new ConcurrentHashMap<>();
private final File dataFile;
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
public MuteDataStorage(JavaPlugin plugin) {
this.plugin = plugin;
this.dataFile = new File(plugin.getDataFolder(), "mutes.json");
}
public void addMute(UUID uuid, MuteData data) {
mutes.put(uuid, data);
saveAsync();
}
public MuteData removeMute(UUID uuid) {
MuteData removed = mutes.remove(uuid);
if (removed != null) {
saveAsync();
}
return removed;
}
public boolean isMuted(UUID uuid) {
if (uuid == null) return false;
MuteData data = mutes.get(uuid);
if (data == null) return false;
if (!isExpired(data)) return true;
boolean removed = mutes.remove(uuid, data);
if (removed) {
saveAsync();
plugin.getLogger().info("Mute expired for player " + data.getPlayerName());
}
return false;
}
public MuteData getMute(UUID uuid) {
return mutes.get(uuid);
}
public Map<UUID, MuteData> getAllMutes() {
mutes.entrySet().removeIf(entry -> isExpired(entry.getValue()));
return new HashMap<>(mutes);
}
private boolean isExpired(MuteData data) {
return data.getEndTime() > 0 && System.currentTimeMillis() > data.getEndTime();
}
public void load() {
if (!dataFile.exists()) {
return;
}
try (Reader reader = new FileReader(dataFile)) {
Type type = new TypeToken<Map<String, MuteData>>() {}.getType();
Map<String, MuteData> loaded = gson.fromJson(reader, type);
if (loaded != null) {
loaded.forEach((key, value) -> {
try {
mutes.put(UUID.fromString(key), value);
} catch (IllegalArgumentException ignored) {
}
});
}
} catch (IOException e) {
plugin.getLogger().warning("Error loading mutes: " + e.getMessage());
}
}
public void save() {
try {
ensureDataFolderExists();
Map<String, MuteData> toSave = new ConcurrentHashMap<>();
mutes.forEach((uuid, data) -> toSave.put(uuid.toString(), data));
try (Writer writer = new FileWriter(dataFile)) {
gson.toJson(toSave, writer);
}
} catch (IOException e) {
plugin.getLogger().warning("Error saving mutes: " + e.getMessage());
}
}
private void saveAsync() {
FoliaUtil.runAsync(plugin, this::save);
}
private void ensureDataFolderExists() {
if (!dataFile.getParentFile().exists()) {
dataFile.getParentFile().mkdirs();
}
}
}

View file

@ -0,0 +1,48 @@
package com.loki.lochat.core.service.mute;
import com.loki.lochat.utils.TimeFormatter;
import org.bukkit.entity.Player;
/**
* Проверка прав на муты
*/
public class MutePermissionChecker {
private static final String[] DURATIONS = {
"30d", "14d", "7d", "3d", "1d", "12h", "6h", "1h", "30m", "10m"
};
public long getMaxDuration(Player player) {
if (player.hasPermission("lochat.mute.dur.perm")) {
return 0; // Permanent
}
for (String dur : DURATIONS) {
if (player.hasPermission("lochat.mute.dur." + dur)) {
return TimeFormatter.parse(dur);
}
}
return -1; // No permission
}
public boolean canMuteForDuration(Player player, long duration) {
if (player.hasPermission("lochat.mute.dur.perm")) {
return true;
}
if (duration == 0) {
return false; // Permanent mute requires special permission
}
long maxDuration = getMaxDuration(player);
if (maxDuration == -1) {
return false; // No mute permission
}
if (maxDuration == 0) {
return true; // Can mute permanently
}
return duration <= maxDuration;
}
}

View file

@ -7,10 +7,10 @@ import com.loki.lochat.gradient.config.GradientConfig;
import com.loki.lochat.gradient.config.GradientMessages;
import com.loki.lochat.gradient.data.GradientDataManager;
import com.loki.lochat.gradient.data.GradientPlayerData;
import com.loki.lochat.gradient.hooks.GradientLuckPermsHook;
import com.loki.lochat.gradient.listeners.GradientGUIListener;
import com.loki.lochat.gradient.listeners.GradientPlayerListener;
import com.loki.lochat.gradient.util.GradientUtil;
import com.loki.lochat.gradient.service.GradientService;
import com.loki.lochat.gradient.service.PrefixService;
import org.black_ixx.playerpoints.PlayerPoints;
import org.black_ixx.playerpoints.PlayerPointsAPI;
import org.bukkit.Bukkit;
@ -18,7 +18,7 @@ import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
/**
* Модуль градиентных ников (интегрированный LoPreff)
* Модуль градиентных ников - упрощенная версия
*/
public class GradientModule {
@ -26,21 +26,15 @@ public class GradientModule {
private GradientConfig config;
private GradientMessages messages;
private GradientDataManager dataManager;
private GradientLuckPermsHook luckPermsHook;
private PrefixService prefixService;
private PlayerPointsAPI playerPointsAPI;
private boolean enabled;
public GradientModule(JavaPlugin plugin) {
this.plugin = plugin;
this.enabled = false;
this.luckPermsHook = null; // Явно инициализируем как null
}
/**
* Инициализация модуля
*
* @return true если модуль успешно инициализирован
*/
public boolean init() {
// Загружаем конфиги
config = new GradientConfig(plugin);
@ -51,7 +45,11 @@ public class GradientModule {
return false;
}
// Проверяем PlayerPoints (опционально теперь)
// Инициализация сервисов
dataManager = new GradientDataManager(plugin, config);
prefixService = new PrefixService(config);
// PlayerPoints (опционально)
if (Bukkit.getPluginManager().isPluginEnabled("PlayerPoints")) {
playerPointsAPI = PlayerPoints.getInstance().getAPI();
plugin.getLogger().info("Gradient: PlayerPoints подключен!");
@ -59,22 +57,13 @@ public class GradientModule {
plugin.getLogger().warning("Gradient: PlayerPoints не найден. Покупки будут бесплатными.");
}
// Инициализация хранилища
dataManager = new GradientDataManager(plugin, config);
// Подключение LuckPerms (опционально)
try {
if (Bukkit.getPluginManager().isPluginEnabled("LuckPerms")) {
luckPermsHook = new GradientLuckPermsHook(plugin);
plugin.getLogger().info("Gradient: LuckPerms подключен!");
} else {
plugin.getLogger().info("Gradient: LuckPerms не найден. Префиксы LuckPerms будут недоступны.");
}
} catch (NoClassDefFoundError e) {
// LuckPerms проверка
if (prefixService.isLuckPermsAvailable()) {
plugin.getLogger().info("Gradient: LuckPerms подключен!");
} else {
plugin.getLogger().info("Gradient: LuckPerms не найден. Префиксы LuckPerms будут недоступны.");
}
// Регистрация команд
registerCommands();
@ -107,106 +96,37 @@ public class GradientModule {
}
}
/**
* Выключение модуля
*/
public void shutdown() {
if (dataManager != null) {
dataManager.saveAll();
}
}
/**
* Перезагрузка модуля
*/
public void reload() {
config.reload();
messages.reload();
}
// ========== Публичные методы для получения отформатированных имен ==========
/**
* Получает отформатированное имя игрока с градиентом и LuckPerms префиксом
* Градиент применяется на всю строку целиком (префикс + ник)
* Использует формат &#RRGGBB
* Получает отформатированное имя игрока (префикс + ник с градиентом)
*/
public String getFormattedName(Player player) {
return getFormattedName(player, false);
if (!enabled) return player.getName();
GradientPlayerData data = dataManager.getPlayerData(player.getUniqueId());
return prefixService.getFullName(player, data);
}
/**
* Получает отформатированное имя для TAB плагина
* Использует формат §x§R§R§G§G§B§B
*/
public String getFormattedNameForTab(Player player) {
return getFormattedName(player, true);
}
/**
* Получает отформатированное имя игрока с градиентом и LuckPerms префиксом
*
* @param useTabFormat true для §x§... формата (TAB), false для &#RRGGBB
*/
private String getFormattedName(Player player, boolean useTabFormat) {
if (!enabled) return player.getName();
GradientPlayerData data = dataManager.getPlayerData(player.getUniqueId());
// Собираем полную строку: префикс + ник
StringBuilder fullText = new StringBuilder();
// LuckPerms префикс
String lpPrefix = null;
if (luckPermsHook != null && luckPermsHook.isEnabled()) {
lpPrefix = luckPermsHook.getActivePrefix(player);
}
// Определяем какой префикс использовать
if (data.hasPrefix() && data.isPrefixEnabled()) {
// Кастомный префикс
fullText.append(config.getPrefixFormat().replace("{prefix}", data.getPrefix()));
} else if (lpPrefix != null && !lpPrefix.isEmpty()) {
// LuckPerms префикс (убираем его цвета если будем применять градиент)
if (data.hasColors() && data.isColorEnabled() && config.isGradientOnLuckPermsPrefix()) {
fullText.append(stripColors(lpPrefix));
} else {
// LP префикс со своими цветами, ник отдельно
String nick = data.hasColors() && data.isColorEnabled()
? applyGradientWithFormat(player.getName(), data.getColors(), useTabFormat)
: player.getName();
return lpPrefix + nick;
}
}
// Добавляем ник
fullText.append(player.getName());
// Применяем единый градиент на всю строку
if (data.hasColors() && data.isColorEnabled()) {
return applyGradientWithFormat(fullText.toString(), data.getColors(), useTabFormat);
}
return fullText.toString();
}
/**
* Применяет градиент с нужным форматом
*/
private String applyGradientWithFormat(String text, java.util.List<String> colors, boolean useTabFormat) {
if (useTabFormat) {
// Формат §x§R§R§G§G§B§B для TAB
return GradientUtil.applyGradientTabFormat(text, colors);
} else {
// Формат &#RRGGBB для GUI и чата
return GradientUtil.applyGradient(text, colors, true);
}
}
/**
* Убирает цветовые коды из строки
*/
private String stripColors(String text) {
if (text == null) return "";
return text.replaceAll("(?i)(§x(§[0-9a-f]){6}|§[0-9a-fk-or]|&[0-9a-fk-or]|&#[0-9a-f]{6}|<[^>]+>)", "");
return prefixService.getFullNameForTab(player, data);
}
/**
@ -216,56 +136,31 @@ public class GradientModule {
if (!enabled) return player.getName();
GradientPlayerData data = dataManager.getPlayerData(player.getUniqueId());
if (!data.hasColors() || !data.isColorEnabled()) {
return player.getName();
}
return GradientUtil.applyGradient(player.getName(), data.getColors(), config.isUseLegacyRgbFormat());
return GradientService.applyGradient(player, player.getName(), data);
}
/**
* Получает префикс игрока (кастомный или LuckPerms) с градиентом
* Градиент применяется только на префикс
*/
public String getPrefix(Player player) {
if (!enabled) return "";
GradientPlayerData data = dataManager.getPlayerData(player.getUniqueId());
// Кастомный префикс
if (data.hasPrefix() && data.isPrefixEnabled()) {
String prefix = config.getPrefixFormat().replace("{prefix}", data.getPrefix());
if (data.hasColors() && data.isColorEnabled() && config.isGradientOnPrefix()) {
return GradientUtil.applyGradient(prefix, data.getColors(), config.isUseLegacyRgbFormat());
}
return prefix;
}
// LuckPerms префикс
if (luckPermsHook != null && luckPermsHook.isEnabled()) {
String lpPrefix = luckPermsHook.getActivePrefix(player);
if (lpPrefix != null && !lpPrefix.isEmpty()) {
if (data.hasColors() && data.isColorEnabled() && config.isGradientOnLuckPermsPrefix()) {
String cleanPrefix = stripColors(lpPrefix);
return GradientUtil.applyGradient(cleanPrefix, data.getColors(), config.isUseLegacyRgbFormat());
}
return lpPrefix;
}
}
return "";
return prefixService.getPrefix(player, data);
}
/**
* Получает только LuckPerms префикс
*/
public String getLuckPermsPrefix(Player player) {
if (!enabled || luckPermsHook == null || !luckPermsHook.isEnabled()) return "";
String prefix = luckPermsHook.getActivePrefix(player);
return prefix != null ? prefix : "";
if (!enabled) return "";
GradientPlayerData data = dataManager.getPlayerData(player.getUniqueId());
return prefixService.getLuckPermsPrefix(player, data);
}
// Геттеры
// ========== Геттеры ==========
public JavaPlugin getPlugin() {
return plugin;
}
@ -282,8 +177,8 @@ public class GradientModule {
return dataManager;
}
public GradientLuckPermsHook getLuckPermsHook() {
return luckPermsHook;
public PrefixService getPrefixService() {
return prefixService;
}
public PlayerPointsAPI getPlayerPointsAPI() {

View file

@ -1,12 +1,12 @@
package com.loki.lochat.gradient.commands;
import com.loki.lochat.gradient.GradientModule;
import com.loki.lochat.gradient.config.GradientConfig;
import com.loki.lochat.gradient.commands.handlers.ColorCommandHandler;
import com.loki.lochat.gradient.commands.handlers.PrefixCommandHandler;
import com.loki.lochat.gradient.config.GradientMessages;
import com.loki.lochat.gradient.data.GradientPlayerData;
import com.loki.lochat.gradient.util.DisplayNameUtil;
import com.loki.lochat.util.FoliaUtil;
import com.loki.lochat.gradient.util.GradientUtil;
import com.loki.lochat.utils.FoliaUtil;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
@ -22,17 +22,22 @@ import java.util.Arrays;
import java.util.List;
/**
* Админ команда /aprefix для управления градиентами
* Админ команда /aprefix - упрощенная версия с делегированием
*/
public class GradientAdminCommand implements CommandExecutor, TabCompleter {
private static final List<String> PRESET_COLORS = Arrays.asList(
"#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3"
"#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3"
);
private final GradientModule module;
private final ColorCommandHandler colorHandler;
private final PrefixCommandHandler prefixHandler;
public GradientAdminCommand(GradientModule module) {
this.module = module;
this.colorHandler = new ColorCommandHandler(module);
this.prefixHandler = new PrefixCommandHandler(module);
}
@Override
@ -64,29 +69,15 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter {
}
handleInfo(sender, args[1], msg);
}
case "setcolor" -> {
if (args.length < 3) {
sender.sendMessage("§cИспользование: /aprefix setcolor <игрок> <цвет1> [цвет2] ...");
return true;
}
handleSetColor(sender, args, msg);
}
case "setprefix" -> {
if (args.length < 3) {
sender.sendMessage("§cИспользование: /aprefix setprefix <игрок> <префикс>");
return true;
}
handleSetPrefix(sender, args, msg);
}
case "coloron" -> handleColorToggle(sender, args, true, msg);
case "coloroff" -> handleColorToggle(sender, args, false, msg);
case "prefixon" -> handlePrefixToggle(sender, args, true, msg);
case "prefixoff" -> handlePrefixToggle(sender, args, false, msg);
case "resetcolor" -> handleResetColor(sender, args, msg);
case "resetprefix" -> handleResetPrefix(sender, args, msg);
case "updateall" -> {
handleUpdateAll(sender, msg);
}
case "setcolor" -> colorHandler.handleSetColor(sender, args, msg);
case "setprefix" -> prefixHandler.handleSetPrefix(sender, args, msg);
case "coloron" -> colorHandler.handleColorToggle(sender, args, true, msg);
case "coloroff" -> colorHandler.handleColorToggle(sender, args, false, msg);
case "prefixon" -> prefixHandler.handlePrefixToggle(sender, args, true, msg);
case "prefixoff" -> prefixHandler.handlePrefixToggle(sender, args, false, msg);
case "resetcolor" -> colorHandler.handleResetColor(sender, args, msg);
case "resetprefix" -> prefixHandler.handleResetPrefix(sender, args, msg);
case "updateall" -> handleUpdateAll(sender, msg);
default -> sendHelp(sender);
}
@ -104,7 +95,6 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter {
sender.sendMessage("§e/aprefix resetcolor <игрок> §7— сбросить цвет");
sender.sendMessage("§e/aprefix resetprefix <игрок> §7— сбросить префикс");
sender.sendMessage("§e/aprefix updateall §7— обновить display names всех игроков");
sender.sendMessage("§e/aprefix toggledisplay §7— переключить TextDisplay/обычный режим");
}
private void handleInfo(CommandSender sender, String playerName, GradientMessages msg) {
@ -124,220 +114,17 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter {
msg.send(sender, "info-prefix-purchased", "purchased", data.isPrefixPurchased() ? "да" : "нет");
}
private void handleSetColor(CommandSender sender, String[] args, GradientMessages msg) {
OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]);
if (!target.hasPlayedBefore() && !target.isOnline()) {
msg.send(sender, "player-not-found");
return;
}
List<String> colors = new ArrayList<>();
for (int i = 2; i < args.length; i++) {
String color = args[i].startsWith("#") ? args[i] : "#" + args[i];
if (!GradientUtil.isValidHex(color)) {
msg.send(sender, "invalid-color", "color", args[i]);
return;
}
colors.add(color.toUpperCase());
}
GradientConfig cfg = module.getConfig();
if (colors.size() > cfg.getMaxColors()) {
msg.send(sender, "too-many-colors", "max", String.valueOf(cfg.getMaxColors()));
return;
}
GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId());
data.setColors(colors);
data.setColorEnabled(true);
Player onlineTarget = target.getPlayer();
if (onlineTarget != null) {
FoliaUtil.runEntityTask(module.getPlugin(), onlineTarget,
() -> DisplayNameUtil.updateDisplayName(module, onlineTarget, data));
}
FoliaUtil.runAsync(module.getPlugin(),
() -> module.getDataManager().savePlayerData(target.getUniqueId()));
sender.sendMessage("§aЦвет для игрока §f" + args[1] + " §aустановлен: §f" + String.join(" ", colors));
}
private void handleSetPrefix(CommandSender sender, String[] args, GradientMessages msg) {
OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]);
if (!target.hasPlayedBefore() && !target.isOnline()) {
msg.send(sender, "player-not-found");
return;
}
StringBuilder prefixBuilder = new StringBuilder();
for (int i = 2; i < args.length; i++) {
if (i > 2) prefixBuilder.append(" ");
prefixBuilder.append(args[i]);
}
String prefix = prefixBuilder.toString();
GradientConfig cfg = module.getConfig();
if (prefix.length() > cfg.getMaxPrefixLength()) {
msg.send(sender, "prefix-too-long", "max", String.valueOf(cfg.getMaxPrefixLength()));
return;
}
GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId());
data.setPrefix(prefix);
data.setPrefixEnabled(true);
data.setPrefixPurchased(true);
if (module.getLuckPermsHook() != null) {
module.getLuckPermsHook().setPrefix(target.getUniqueId(), DisplayNameUtil.buildColoredPrefix(module, data));
}
Player onlineTarget = target.getPlayer();
if (onlineTarget != null) {
FoliaUtil.runEntityTask(module.getPlugin(), onlineTarget,
() -> DisplayNameUtil.updateDisplayName(module, onlineTarget, data));
}
FoliaUtil.runAsync(module.getPlugin(),
() -> module.getDataManager().savePlayerData(target.getUniqueId()));
sender.sendMessage("§aПрефикс для игрока §f" + args[1] + " §aустановлен: §f" + prefix);
}
private void handleColorToggle(CommandSender sender, String[] args, boolean enable, GradientMessages msg) {
if (args.length < 2) {
sender.sendMessage("§cИспользование: /aprefix " + (enable ? "coloron" : "coloroff") + " <игрок>");
return;
}
OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]);
if (!target.hasPlayedBefore() && !target.isOnline()) {
msg.send(sender, "player-not-found");
return;
}
GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId());
data.setColorEnabled(enable);
Player onlineTarget = target.getPlayer();
if (onlineTarget != null) {
FoliaUtil.runEntityTask(module.getPlugin(), onlineTarget,
() -> DisplayNameUtil.updateDisplayName(module, onlineTarget, data));
}
FoliaUtil.runAsync(module.getPlugin(),
() -> module.getDataManager().savePlayerData(target.getUniqueId()));
sender.sendMessage("§aЦвет для игрока §f" + args[1] + (enable ? " §aвключён" : " §cвыключен"));
}
private void handlePrefixToggle(CommandSender sender, String[] args, boolean enable, GradientMessages msg) {
if (args.length < 2) {
sender.sendMessage("§cИспользование: /aprefix " + (enable ? "prefixon" : "prefixoff") + " <игрок>");
return;
}
OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]);
if (!target.hasPlayedBefore() && !target.isOnline()) {
msg.send(sender, "player-not-found");
return;
}
GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId());
data.setPrefixEnabled(enable);
if (module.getLuckPermsHook() != null) {
if (enable && data.hasPrefix()) {
module.getLuckPermsHook().setPrefix(target.getUniqueId(), DisplayNameUtil.buildColoredPrefix(module, data));
} else {
module.getLuckPermsHook().removePrefix(target.getUniqueId());
}
}
Player onlineTarget = target.getPlayer();
if (onlineTarget != null) {
FoliaUtil.runEntityTask(module.getPlugin(), onlineTarget,
() -> DisplayNameUtil.updateDisplayName(module, onlineTarget, data));
}
FoliaUtil.runAsync(module.getPlugin(),
() -> module.getDataManager().savePlayerData(target.getUniqueId()));
sender.sendMessage("§aПрефикс для игрока §f" + args[1] + (enable ? " §aвключён" : " §cвыключен"));
}
private void handleResetColor(CommandSender sender, String[] args, GradientMessages msg) {
if (args.length < 2) {
sender.sendMessage("§cИспользование: /aprefix resetcolor <игрок>");
return;
}
OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]);
if (!target.hasPlayedBefore() && !target.isOnline()) {
msg.send(sender, "player-not-found");
return;
}
GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId());
data.setColors(new ArrayList<>());
data.setColorEnabled(false);
Player onlineTarget = target.getPlayer();
if (onlineTarget != null) {
FoliaUtil.runEntityTask(module.getPlugin(), onlineTarget,
() -> DisplayNameUtil.updateDisplayName(module, onlineTarget, data));
}
FoliaUtil.runAsync(module.getPlugin(),
() -> module.getDataManager().savePlayerData(target.getUniqueId()));
sender.sendMessage("§aЦвет для игрока §f" + args[1] + " §aсброшен");
}
private void handleResetPrefix(CommandSender sender, String[] args, GradientMessages msg) {
if (args.length < 2) {
sender.sendMessage("§cИспользование: /aprefix resetprefix <игрок>");
return;
}
OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]);
if (!target.hasPlayedBefore() && !target.isOnline()) {
msg.send(sender, "player-not-found");
return;
}
GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId());
data.setPrefix(null);
data.setPrefixEnabled(false);
if (module.getLuckPermsHook() != null) {
module.getLuckPermsHook().removePrefix(target.getUniqueId());
}
Player onlineTarget = target.getPlayer();
if (onlineTarget != null) {
FoliaUtil.runEntityTask(module.getPlugin(), onlineTarget,
() -> DisplayNameUtil.updateDisplayName(module, onlineTarget, data));
}
FoliaUtil.runAsync(module.getPlugin(),
() -> module.getDataManager().savePlayerData(target.getUniqueId()));
sender.sendMessage("§aПрефикс для игрока §f" + args[1] + " §aсброшен");
}
private void handleUpdateAll(CommandSender sender, GradientMessages msg) {
int count = 0;
for (Player player : Bukkit.getOnlinePlayers()) {
GradientPlayerData data = module.getDataManager().getPlayerData(player.getUniqueId());
FoliaUtil.runEntityTask(module.getPlugin(), player,
() -> DisplayNameUtil.updateDisplayName(module, player, data));
() -> DisplayNameUtil.updateDisplayName(module, player, data));
count++;
}
sender.sendMessage("§aОбновлены display names для §f" + count + " §aигроков");
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {

View file

@ -6,7 +6,7 @@ import com.loki.lochat.gradient.config.GradientMessages;
import com.loki.lochat.gradient.data.GradientPlayerData;
import com.loki.lochat.gradient.gui.GradientConfirmGUI;
import com.loki.lochat.gradient.util.DisplayNameUtil;
import com.loki.lochat.util.FoliaUtil;
import com.loki.lochat.utils.FoliaUtil;
import com.loki.lochat.gradient.util.GradientConstants;
import com.loki.lochat.gradient.util.GradientUtil;
import org.bukkit.Bukkit;

View file

@ -6,7 +6,7 @@ import com.loki.lochat.gradient.config.GradientMessages;
import com.loki.lochat.gradient.data.GradientPlayerData;
import com.loki.lochat.gradient.gui.GradientConfirmGUI;
import com.loki.lochat.gradient.util.DisplayNameUtil;
import com.loki.lochat.util.FoliaUtil;
import com.loki.lochat.utils.FoliaUtil;
import com.loki.lochat.gradient.util.GradientConstants;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
@ -83,9 +83,6 @@ public class GradientPrefixCommand implements CommandExecutor, TabCompleter {
}
data.setPrefixEnabled(true);
if (module.getLuckPermsHook() != null) {
module.getLuckPermsHook().setPrefix(player, DisplayNameUtil.buildColoredPrefix(module, data));
}
saveAndUpdate(player, data);
msg.send(player, "prefix-enabled");
return true;
@ -94,9 +91,6 @@ public class GradientPrefixCommand implements CommandExecutor, TabCompleter {
private boolean handleOff(Player player, String[] args) {
GradientPlayerData data = module.getDataManager().getPlayerData(player.getUniqueId());
data.setPrefixEnabled(false);
if (module.getLuckPermsHook() != null) {
module.getLuckPermsHook().removePrefix(player);
}
saveAndUpdate(player, data);
module.getMessages().send(player, "prefix-disabled");
return true;
@ -113,9 +107,6 @@ public class GradientPrefixCommand implements CommandExecutor, TabCompleter {
data.setPrefix(null);
data.setPrefixEnabled(false);
if (module.getLuckPermsHook() != null) {
module.getLuckPermsHook().removePrefix(player);
}
saveAndUpdate(player, data);
msg.send(player, "prefix-reset-success");
return true;

View file

@ -0,0 +1,129 @@
package com.loki.lochat.gradient.commands.handlers;
import com.loki.lochat.gradient.GradientModule;
import com.loki.lochat.gradient.config.GradientMessages;
import com.loki.lochat.gradient.data.GradientPlayerData;
import com.loki.lochat.gradient.util.DisplayNameUtil;
import com.loki.lochat.gradient.util.GradientUtil;
import com.loki.lochat.utils.FoliaUtil;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
/**
* Обработчик команд для управления цветами
*/
public class ColorCommandHandler {
private final GradientModule module;
public ColorCommandHandler(GradientModule module) {
this.module = module;
}
/**
* Установить цвет игроку
*/
public void handleSetColor(CommandSender sender, String[] args, GradientMessages msg) {
if (args.length < 3) {
sender.sendMessage("§cИспользование: /aprefix setcolor <игрок> <цвет1> [цвет2] ...");
return;
}
OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]);
if (!target.hasPlayedBefore() && !target.isOnline()) {
msg.send(sender, "player-not-found");
return;
}
List<String> colors = new ArrayList<>();
for (int i = 2; i < args.length; i++) {
String color = args[i].startsWith("#") ? args[i] : "#" + args[i];
if (!GradientUtil.isValidHex(color)) {
msg.send(sender, "invalid-color", "color", args[i]);
return;
}
colors.add(color.toUpperCase());
}
if (colors.size() > module.getConfig().getMaxColors()) {
msg.send(sender, "too-many-colors", "max", String.valueOf(module.getConfig().getMaxColors()));
return;
}
GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId());
data.setColors(colors);
data.setColorEnabled(true);
updatePlayerDisplay(target, data);
savePlayerData(target);
sender.sendMessage("§aЦвет для игрока §f" + args[1] + " §aустановлен: §f" + String.join(" ", colors));
}
/**
* Включить/выключить цвет
*/
public void handleColorToggle(CommandSender sender, String[] args, boolean enable, GradientMessages msg) {
if (args.length < 2) {
sender.sendMessage("§cИспользование: /aprefix " + (enable ? "coloron" : "coloroff") + " <игрок>");
return;
}
OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]);
if (!target.hasPlayedBefore() && !target.isOnline()) {
msg.send(sender, "player-not-found");
return;
}
GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId());
data.setColorEnabled(enable);
updatePlayerDisplay(target, data);
savePlayerData(target);
sender.sendMessage("§aЦвет для игрока §f" + args[1] + (enable ? " §aвключён" : " §cвыключен"));
}
/**
* Сбросить цвет
*/
public void handleResetColor(CommandSender sender, String[] args, GradientMessages msg) {
if (args.length < 2) {
sender.sendMessage("§cИспользование: /aprefix resetcolor <игрок>");
return;
}
OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]);
if (!target.hasPlayedBefore() && !target.isOnline()) {
msg.send(sender, "player-not-found");
return;
}
GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId());
data.setColors(new ArrayList<>());
data.setColorEnabled(false);
updatePlayerDisplay(target, data);
savePlayerData(target);
sender.sendMessage("§aЦвет для игрока §f" + args[1] + " §aсброшен");
}
private void updatePlayerDisplay(OfflinePlayer target, GradientPlayerData data) {
Player onlineTarget = target.getPlayer();
if (onlineTarget != null) {
FoliaUtil.runEntityTask(module.getPlugin(), onlineTarget,
() -> DisplayNameUtil.updateDisplayName(module, onlineTarget, data));
}
}
private void savePlayerData(OfflinePlayer target) {
FoliaUtil.runAsync(module.getPlugin(),
() -> module.getDataManager().savePlayerData(target.getUniqueId()));
}
}

View file

@ -0,0 +1,123 @@
package com.loki.lochat.gradient.commands.handlers;
import com.loki.lochat.gradient.GradientModule;
import com.loki.lochat.gradient.config.GradientMessages;
import com.loki.lochat.gradient.data.GradientPlayerData;
import com.loki.lochat.gradient.util.DisplayNameUtil;
import com.loki.lochat.utils.FoliaUtil;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
/**
* Обработчик команд для управления префиксами
*/
public class PrefixCommandHandler {
private final GradientModule module;
public PrefixCommandHandler(GradientModule module) {
this.module = module;
}
/**
* Установить префикс игроку
*/
public void handleSetPrefix(CommandSender sender, String[] args, GradientMessages msg) {
if (args.length < 3) {
sender.sendMessage("§cИспользование: /aprefix setprefix <игрок> <префикс>");
return;
}
OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]);
if (!target.hasPlayedBefore() && !target.isOnline()) {
msg.send(sender, "player-not-found");
return;
}
StringBuilder prefixBuilder = new StringBuilder();
for (int i = 2; i < args.length; i++) {
if (i > 2) prefixBuilder.append(" ");
prefixBuilder.append(args[i]);
}
String prefix = prefixBuilder.toString();
if (prefix.length() > module.getConfig().getMaxPrefixLength()) {
msg.send(sender, "prefix-too-long", "max", String.valueOf(module.getConfig().getMaxPrefixLength()));
return;
}
GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId());
data.setPrefix(prefix);
data.setPrefixEnabled(true);
data.setPrefixPurchased(true);
updatePlayerDisplay(target, data);
savePlayerData(target);
sender.sendMessage("§aПрефикс для игрока §f" + args[1] + " §aустановлен: §f" + prefix);
}
/**
* Включить/выключить префикс
*/
public void handlePrefixToggle(CommandSender sender, String[] args, boolean enable, GradientMessages msg) {
if (args.length < 2) {
sender.sendMessage("§cИспользование: /aprefix " + (enable ? "prefixon" : "prefixoff") + " <игрок>");
return;
}
OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]);
if (!target.hasPlayedBefore() && !target.isOnline()) {
msg.send(sender, "player-not-found");
return;
}
GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId());
data.setPrefixEnabled(enable);
updatePlayerDisplay(target, data);
savePlayerData(target);
sender.sendMessage("§aПрефикс для игрока §f" + args[1] + (enable ? " §aвключён" : " §cвыключен"));
}
/**
* Сбросить префикс
*/
public void handleResetPrefix(CommandSender sender, String[] args, GradientMessages msg) {
if (args.length < 2) {
sender.sendMessage("§cИспользование: /aprefix resetprefix <игрок>");
return;
}
OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]);
if (!target.hasPlayedBefore() && !target.isOnline()) {
msg.send(sender, "player-not-found");
return;
}
GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId());
data.setPrefix(null);
data.setPrefixEnabled(false);
updatePlayerDisplay(target, data);
savePlayerData(target);
sender.sendMessage("§aПрефикс для игрока §f" + args[1] + " §aсброшен");
}
private void updatePlayerDisplay(OfflinePlayer target, GradientPlayerData data) {
Player onlineTarget = target.getPlayer();
if (onlineTarget != null) {
FoliaUtil.runEntityTask(module.getPlugin(), onlineTarget,
() -> DisplayNameUtil.updateDisplayName(module, onlineTarget, data));
}
}
private void savePlayerData(OfflinePlayer target) {
FoliaUtil.runAsync(module.getPlugin(),
() -> module.getDataManager().savePlayerData(target.getUniqueId()));
}
}

View file

@ -128,8 +128,8 @@ public final class GradientConfirmGUI implements InventoryHolder {
boolean useLegacyForPreview = true;
// Если нет кастомного префикса - используем LuckPerms префикс
if ((prefixToUse == null || prefixToUse.isEmpty()) && module.getLuckPermsHook() != null && module.getLuckPermsHook().isEnabled()) {
String lpPrefix = module.getLuckPermsHook().getActivePrefix(player);
if ((prefixToUse == null || prefixToUse.isEmpty())) {
String lpPrefix = module.getPrefixService().getLuckPermsPrefix(player, data);
if (lpPrefix != null && !lpPrefix.isEmpty()) {
// Конвертируем LP префикс в legacy формат
lpPrefix = convertToLegacy(lpPrefix);

View file

@ -1,95 +0,0 @@
package com.loki.lochat.gradient.hooks;
import com.loki.lochat.gradient.util.GradientConstants;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.LuckPermsProvider;
import net.luckperms.api.cacheddata.CachedMetaData;
import net.luckperms.api.model.user.User;
import net.luckperms.api.node.types.PrefixNode;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.UUID;
/**
* Хук для LuckPerms
*/
public class GradientLuckPermsHook {
private LuckPerms luckPerms;
private boolean enabled;
public GradientLuckPermsHook(JavaPlugin plugin) {
this.enabled = false;
if (Bukkit.getPluginManager().isPluginEnabled("LuckPerms")) {
try {
this.luckPerms = LuckPermsProvider.get();
this.enabled = true;
plugin.getLogger().info("Gradient: LuckPerms подключен!");
} catch (Exception e) {
plugin.getLogger().warning("Gradient: Не удалось подключить LuckPerms: " + e.getMessage());
}
}
}
public boolean isEnabled() {
return enabled && luckPerms != null;
}
public void setPrefix(Player player, String prefix) {
if (!isEnabled()) return;
User user = luckPerms.getUserManager().getUser(player.getUniqueId());
if (user == null) return;
user.data().clear(node ->
node instanceof PrefixNode && ((PrefixNode) node).getPriority() == GradientConstants.LUCKPERMS_PREFIX_PRIORITY
);
if (prefix != null && !prefix.isEmpty()) {
PrefixNode prefixNode = PrefixNode.builder(prefix, GradientConstants.LUCKPERMS_PREFIX_PRIORITY).build();
user.data().add(prefixNode);
}
luckPerms.getUserManager().saveUser(user);
}
public void setPrefix(UUID uuid, String prefix) {
if (!isEnabled()) return;
luckPerms.getUserManager().loadUser(uuid).thenAccept(user -> {
if (user == null) return;
user.data().clear(node ->
node instanceof PrefixNode && ((PrefixNode) node).getPriority() == GradientConstants.LUCKPERMS_PREFIX_PRIORITY
);
if (prefix != null && !prefix.isEmpty()) {
PrefixNode prefixNode = PrefixNode.builder(prefix, GradientConstants.LUCKPERMS_PREFIX_PRIORITY).build();
user.data().add(prefixNode);
}
luckPerms.getUserManager().saveUser(user);
});
}
public void removePrefix(Player player) {
setPrefix(player, null);
}
public void removePrefix(UUID uuid) {
setPrefix(uuid, null);
}
public String getActivePrefix(Player player) {
if (!isEnabled()) return null;
User user = luckPerms.getUserManager().getUser(player.getUniqueId());
if (user == null) return null;
CachedMetaData metaData = user.getCachedData().getMetaData();
return metaData.getPrefix();
}
}

View file

@ -5,7 +5,7 @@ import com.loki.lochat.gradient.config.GradientMessages;
import com.loki.lochat.gradient.data.GradientPlayerData;
import com.loki.lochat.gradient.gui.GradientConfirmGUI;
import com.loki.lochat.gradient.util.DisplayNameUtil;
import com.loki.lochat.util.FoliaUtil;
import com.loki.lochat.utils.FoliaUtil;
import com.loki.lochat.gradient.util.GradientConstants;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@ -81,9 +81,6 @@ public class GradientGUIListener implements Listener {
data.setColors(gui.getColors());
data.setColorEnabled(true);
}
if (module.getLuckPermsHook() != null) {
module.getLuckPermsHook().setPrefix(player, DisplayNameUtil.buildColoredPrefix(module, data));
}
msg.send(player, price > 0 ? "prefix-success" : "prefix-success-free",
"price", String.valueOf(price));
}

View file

@ -1,7 +1,7 @@
package com.loki.lochat.gradient.listeners;
import com.loki.lochat.gradient.GradientModule;
import com.loki.lochat.util.FoliaUtil;
import com.loki.lochat.utils.FoliaUtil;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;

View file

@ -0,0 +1,57 @@
package com.loki.lochat.gradient.service;
import com.loki.lochat.gradient.data.GradientPlayerData;
import com.loki.lochat.gradient.util.GradientUtil;
import org.bukkit.entity.Player;
import java.util.List;
/**
* Сервис для применения градиентов - единая точка входа
*/
public class GradientService {
/**
* Применяет градиент к тексту если у игрока есть цвета
*/
public static String applyGradient(Player player, String text, GradientPlayerData data) {
if (data == null || !data.hasColors() || !data.isColorEnabled()) {
return text;
}
return GradientUtil.applyGradient(text, data.getColors(), true);
}
/**
* Применяет градиент к тексту с указанными цветами
*/
public static String applyGradient(String text, List<String> colors) {
if (colors == null || colors.isEmpty()) {
return text;
}
return GradientUtil.applyGradient(text, colors, true);
}
/**
* Применяет градиент в формате для TAB плагина
*/
public static String applyGradientForTab(String text, List<String> colors) {
if (colors == null || colors.isEmpty()) {
return text;
}
return GradientUtil.applyGradientTabFormat(text, colors);
}
/**
* Убирает все цветовые коды из текста
*/
public static String stripColors(String text) {
if (text == null || text.isEmpty()) {
return "";
}
return text.replaceAll("(?i)(§x(§[0-9a-f]){6}|§[0-9a-fk-or]|&[0-9a-fk-or]|&#[0-9a-f]{6}|<[^>]+>)", "");
}
}

View file

@ -0,0 +1,114 @@
package com.loki.lochat.gradient.service;
import com.loki.lochat.gradient.config.GradientConfig;
import com.loki.lochat.gradient.data.GradientPlayerData;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.LuckPermsProvider;
import net.luckperms.api.cacheddata.CachedMetaData;
import net.luckperms.api.model.user.User;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
/**
* Сервис для работы с префиксами - упрощенная интеграция с LuckPerms
*/
public class PrefixService {
private final GradientConfig config;
private final boolean luckPermsAvailable;
public PrefixService(GradientConfig config) {
this.config = config;
this.luckPermsAvailable = Bukkit.getPluginManager().isPluginEnabled("LuckPerms");
}
/**
* Получает префикс игрока (кастомный или из LuckPerms)
*/
public String getPrefix(Player player, GradientPlayerData data) {
// Приоритет: кастомный префикс > LuckPerms префикс
if (data.hasPrefix() && data.isPrefixEnabled()) {
String prefix = config.getPrefixFormat().replace("{prefix}", data.getPrefix());
// Применяем градиент если нужно
if (data.hasColors() && data.isColorEnabled() && config.isGradientOnPrefix()) {
return GradientService.applyGradient(prefix, data.getColors());
}
return prefix;
}
// Пробуем получить из LuckPerms
return getLuckPermsPrefix(player, data);
}
/**
* Получает префикс из LuckPerms
*/
public String getLuckPermsPrefix(Player player, GradientPlayerData data) {
if (!luckPermsAvailable) {
return "";
}
try {
LuckPerms luckPerms = LuckPermsProvider.get();
User user = luckPerms.getUserManager().getUser(player.getUniqueId());
if (user == null) {
return "";
}
CachedMetaData metaData = user.getCachedData().getMetaData();
String prefix = metaData.getPrefix();
if (prefix == null || prefix.isEmpty()) {
return "";
}
// Применяем градиент если нужно
if (data != null && data.hasColors() && data.isColorEnabled() && config.isGradientOnLuckPermsPrefix()) {
String cleanPrefix = GradientService.stripColors(prefix);
return GradientService.applyGradient(cleanPrefix, data.getColors());
}
return prefix;
} catch (Exception e) {
return "";
}
}
/**
* Получает полное имя игрока: префикс + ник с градиентом
*/
public String getFullName(Player player, GradientPlayerData data) {
String prefix = getPrefix(player, data);
String nick = player.getName();
// Применяем градиент к нику если есть цвета
if (data.hasColors() && data.isColorEnabled()) {
nick = GradientService.applyGradient(nick, data.getColors());
}
return prefix.isEmpty() ? nick : prefix + " " + nick;
}
/**
* Получает полное имя для TAB плагина
*/
public String getFullNameForTab(Player player, GradientPlayerData data) {
String prefix = getPrefix(player, data);
String nick = player.getName();
// Применяем градиент в формате TAB
if (data.hasColors() && data.isColorEnabled()) {
nick = GradientService.applyGradientForTab(nick, data.getColors());
}
return prefix.isEmpty() ? nick : prefix + " " + nick;
}
public boolean isLuckPermsAvailable() {
return luckPermsAvailable;
}
}

View file

@ -6,7 +6,6 @@ import com.loki.lochat.gradient.data.GradientPlayerData;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -25,46 +24,10 @@ public final class DisplayNameUtil {
}
public static void updateDisplayName(GradientModule module, Player player, GradientPlayerData data) {
GradientConfig cfg = module.getConfig();
String prefix = null;
String prefixFormat = cfg.getPrefixFormat();
// Определяем префикс: сначала кастомный, потом LuckPerms
if (data.isPrefixEnabled() && data.hasPrefix()) {
// Есть кастомный префикс
prefix = data.getPrefix();
} else if (module.getLuckPermsHook() != null && module.getLuckPermsHook().isEnabled()) {
// Нет кастомного префикса, проверяем LuckPerms
String lpPrefix = module.getLuckPermsHook().getActivePrefix(player);
if (lpPrefix != null && !lpPrefix.isEmpty()) {
String displayName = buildWithLuckPermsPrefix(
lpPrefix,
player.getName(),
data.isColorEnabled() ? data.getColors() : null,
cfg.isUseLegacyRgbFormat(),
cfg.isContinuousGradient()
);
// Для display name нужен §x формат, конвертируем MiniMessage в legacy
displayName = convertMiniMessageToLegacy(displayName);
var component = SERIALIZER.deserialize(displayName);
player.displayName(component);
player.playerListName(component);
return;
}
}
// Строим display name с кастомным префиксом или без префикса
String displayName = GradientUtil.buildDisplayName(
prefix,
player.getName(),
data.isColorEnabled() ? data.getColors() : null,
cfg.isGradientOnPrefix(),
cfg.isContinuousGradient(),
prefixFormat,
cfg.isUseLegacyRgbFormat()
);
// Для display name нужен §x формат, конвертируем MiniMessage в legacy
// Используем новый PrefixService для получения полного имени
String displayName = module.getFormattedName(player);
// Конвертируем в legacy формат для display name
displayName = convertMiniMessageToLegacy(displayName);
var component = SERIALIZER.deserialize(displayName);
player.displayName(component);
@ -95,28 +58,6 @@ public final class DisplayNameUtil {
return sb.toString();
}
private static String buildWithLuckPermsPrefix(String lpPrefix, String nick,
List<String> colors,
boolean useLegacyFormat,
boolean continuousGradient) {
if (colors == null || colors.isEmpty()) {
return lpPrefix + nick;
}
if (continuousGradient) {
String cleanPrefix = stripColors(lpPrefix);
String fullText = cleanPrefix + nick;
return GradientUtil.applyGradient(fullText, colors, useLegacyFormat);
} else {
return lpPrefix + GradientUtil.applyGradient(nick, colors, useLegacyFormat);
}
}
private static String stripColors(String text) {
if (text == null) return null;
return text.replaceAll("(?i)(§x(§[0-9a-f]){6}|§[0-9a-fk-or]|&[0-9a-fk-or]|&#[0-9a-f]{6})", "");
}
public static String buildColoredPrefix(GradientModule module, GradientPlayerData data) {
if (!data.hasPrefix() || !data.isPrefixEnabled()) return null;

View file

@ -1,87 +1,36 @@
package com.loki.lochat.integrations;
import com.loki.lochat.util.FoliaUtil;
import org.bukkit.configuration.file.YamlConfiguration;
import com.loki.lochat.integrations.discord.DiscordConfig;
import com.loki.lochat.integrations.discord.DiscordMessageService;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.util.regex.Pattern;
/**
* Интеграция с Discord через вебхуки
* Интеграция с Discord - упрощенная версия
*/
public class DiscordIntegration {
private final JavaPlugin plugin;
private YamlConfiguration config;
private DiscordWebhook webhook;
private DiscordConfig config;
private DiscordMessageService messageService;
private boolean enabled = false;
// Паттерны для очистки сообщений
private static final Pattern MENTION_PATTERN = Pattern.compile("@(everyone|here)", Pattern.CASE_INSENSITIVE);
private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("§[0-9a-fk-or]", Pattern.CASE_INSENSITIVE);
public DiscordIntegration(JavaPlugin plugin) {
this.plugin = plugin;
loadConfig();
initWebhook();
init();
}
/**
* Загрузить конфигурацию
*/
private void loadConfig() {
// Создаём папку config если её нет
File configFolder = new File(plugin.getDataFolder(), "config");
if (!configFolder.exists()) {
configFolder.mkdirs();
}
File configFile = new File(configFolder, "discord.yml");
if (!configFile.exists()) {
try {
// Копируем из resources/config/discord.yml
java.io.InputStream in = plugin.getResource("config/discord.yml");
if (in != null) {
java.nio.file.Files.copy(in, configFile.toPath());
in.close();
}
} catch (Exception e) {
plugin.getLogger().warning("Failed to copy config/discord.yml: " + e.getMessage());
}
}
config = YamlConfiguration.loadConfiguration(configFile);
enabled = config.getBoolean("enabled", false);
private void init() {
config = new DiscordConfig(plugin);
enabled = config.isEnabled();
if (!enabled) {
plugin.getLogger().info("Discord integration disabled in config");
return;
}
}
/**
* Инициализировать вебхук
*/
private void initWebhook() {
if (!enabled) return;
// Получаем конфигурацию внешнего вида для дефолтных значений
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
messageService = new DiscordMessageService(plugin, config);
String webhookUrl = config.getString("webhook.url", "");
String username = config.getString("webhook.username", "Minecraft Server");
String avatarUrl = config.getString("webhook.avatar_url", appearanceConfig.getDefaultAvatarUrl());
int timeout = config.getInt("performance.timeout", 10);
int retryAttempts = config.getInt("performance.retry_attempts", 3);
long retryDelay = config.getLong("performance.retry_delay", 1000);
// Логируем только факт наличия URL, но не его содержимое (security)
webhook = new DiscordWebhook(plugin, webhookUrl, username, avatarUrl, timeout, retryAttempts, retryDelay);
if (!webhook.isValid()) {
if (!messageService.isValid()) {
plugin.getLogger().severe("Invalid Discord webhook URL! Please check config/discord.yml");
enabled = false;
return;
@ -90,205 +39,47 @@ public class DiscordIntegration {
plugin.getLogger().info("Discord integration initialized successfully");
}
/**
* Отправить сообщение чата в Discord
*/
public void sendChatMessage(Player player, String message, boolean isGlobal) {
if (!enabled || !config.getBoolean("chat.enabled", true)) return;
if (!enabled || !config.isChatEnabled()) return;
// Проверяем фильтр глобального чата
if (config.getBoolean("chat.global_only", true) && !isGlobal) {
if (config.isGlobalOnly() && !isGlobal) {
return;
}
// Фильтрация сообщения
String cleanMessage = sanitizeMessage(message);
if (cleanMessage.length() < config.getInt("filter.min_message_length", 1)) {
return;
}
// Обрезаем если слишком длинное
int maxLength = config.getInt("filter.max_message_length", 2000);
if (cleanMessage.length() > maxLength) {
cleanMessage = cleanMessage.substring(0, maxLength - 3) + "...";
}
String playerName = player.getName();
String format = config.getString("chat.format", "**{player}**: {message}");
String formattedMessage = format
.replace("{player}", playerName)
.replace("{message}", cleanMessage);
// Отправляем асинхронно
if (config.getBoolean("performance.async", true)) {
FoliaUtil.runAsync(plugin, () -> sendChatMessageInternal(formattedMessage, playerName, isGlobal));
} else {
sendChatMessageInternal(formattedMessage, playerName, isGlobal);
}
messageService.sendChatMessage(player, message, isGlobal);
}
/**
* Внутренний метод отправки сообщения чата
*/
private void sendChatMessageInternal(String message, String playerName, boolean isGlobal) {
if (config.getBoolean("chat.use_embed", true)) {
// Получаем заголовки из конфигурации внешнего вида
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String title = isGlobal ?
appearanceConfig.getDiscordEventTitle("global_chat") :
appearanceConfig.getDiscordEventTitle("local_chat");
// Если заголовок не найден в новой конфигурации, используем старый способ
if (title.isEmpty()) {
title = isGlobal ? "🌍 Глобальный чат" : "📍 Локальный чат";
}
String color = config.getString("chat.embed_color", appearanceConfig.getDefaultEmbedColor());
String thumbnail = appearanceConfig.getPlayerAvatarUrl().replace("{player}", playerName);
webhook.sendEmbed(title, message, color, thumbnail);
} else {
webhook.sendMessage(message);
}
}
/**
* Отправить событие входа игрока
*/
public void sendPlayerJoin(Player player) {
if (!enabled || !config.getBoolean("events.enabled", true) ||
!config.getBoolean("events.join.enabled", true)) return;
// Получаем конфигурацию внешнего вида
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String playerName = player.getName();
String title = config.getString("events.join.title", appearanceConfig.getDiscordEventTitle("join"));
String description = config.getString("events.join.description", "**{player}** присоединился к серверу")
.replace("{player}", playerName);
String color = config.getString("events.join.color", "00FF00");
String thumbnail = config.getString("events.join.thumbnail", appearanceConfig.getPlayerAvatarUrl())
.replace("{player}", playerName);
if (config.getBoolean("performance.async", true)) {
FoliaUtil.runAsync(plugin, () -> webhook.sendEmbed(title, description, color, thumbnail));
} else {
webhook.sendEmbed(title, description, color, thumbnail);
}
if (!enabled || !config.isEventEnabled("join")) return;
messageService.sendPlayerJoin(player);
}
/**
* Отправить событие выхода игрока
*/
public void sendPlayerQuit(Player player) {
if (!enabled || !config.getBoolean("events.enabled", true) ||
!config.getBoolean("events.quit.enabled", true)) return;
// Получаем конфигурацию внешнего вида
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String playerName = player.getName();
String title = config.getString("events.quit.title", appearanceConfig.getDiscordEventTitle("quit"));
String description = config.getString("events.quit.description", "**{player}** покинул сервер")
.replace("{player}", playerName);
String color = config.getString("events.quit.color", "FF0000");
String thumbnail = config.getString("events.quit.thumbnail", appearanceConfig.getPlayerAvatarUrl())
.replace("{player}", playerName);
if (config.getBoolean("performance.async", true)) {
FoliaUtil.runAsync(plugin, () -> webhook.sendEmbed(title, description, color, thumbnail));
} else {
webhook.sendEmbed(title, description, color, thumbnail);
}
if (!enabled || !config.isEventEnabled("quit")) return;
messageService.sendPlayerQuit(player);
}
/**
* Отправить событие смерти игрока
*/
public void sendPlayerDeath(Player player, String deathMessage) {
if (!enabled || !config.getBoolean("events.enabled", true) ||
!config.getBoolean("events.death.enabled", true)) return;
// Получаем конфигурацию внешнего вида
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String playerName = player.getName();
String title = config.getString("events.death.title", appearanceConfig.getDiscordEventTitle("death"));
String description = config.getString("events.death.description", "**{player}** {death_message}")
.replace("{player}", playerName)
.replace("{death_message}", sanitizeMessage(deathMessage));
String color = config.getString("events.death.color", "800080");
String thumbnail = config.getString("events.death.thumbnail", appearanceConfig.getPlayerAvatarUrl())
.replace("{player}", playerName);
if (config.getBoolean("performance.async", true)) {
FoliaUtil.runAsync(plugin, () -> webhook.sendEmbed(title, description, color, thumbnail));
} else {
webhook.sendEmbed(title, description, color, thumbnail);
}
if (!enabled || !config.isEventEnabled("death")) return;
messageService.sendPlayerDeath(player, deathMessage);
}
/**
* Очистить сообщение от нежелательных элементов
*/
private String sanitizeMessage(String message) {
if (message == null) return "";
// Удаляем цветовые коды Minecraft
message = COLOR_CODE_PATTERN.matcher(message).replaceAll("");
// Заменяем опасные упоминания
if (config.getBoolean("filter.sanitize_mentions", true)) {
message = MENTION_PATTERN.matcher(message).replaceAll("@\u200B$1"); // Добавляем невидимый символ
}
// Экранируем Discord markdown
message = message.replace("*", "\\*")
.replace("_", "\\_")
.replace("`", "\\`")
.replace("~", "\\~");
return message.trim();
public void sendTestMessage(String message, String senderName) {
if (!enabled) return;
messageService.sendTestMessage(message, senderName);
}
/**
* Перезагрузить конфигурацию
*/
public void reload() {
loadConfig();
initWebhook();
init();
plugin.getLogger().info("Discord integration reloaded");
}
/**
* Отправить тестовое сообщение
*/
public void sendTestMessage(String message, String senderName) {
if (!enabled) return;
String title = "🧪 Тестовое сообщение";
String description = "**Отправитель:** " + senderName + "\n**Сообщение:** " + message;
String color = "FFD700"; // золотой цвет
String thumbnail = "https://mc-heads.net/avatar/" + senderName + "/64";
webhook.sendEmbed(title, description, color, thumbnail);
}
/**
* Остановить executor при выключении плагина
*/
public void shutdown() {
if (webhook != null) webhook.shutdown();
if (messageService != null) {
messageService.shutdown();
}
}
/**
* Проверить включена ли интеграция
*/
public boolean isEnabled() {
return enabled;
}

View file

@ -2,9 +2,7 @@ package com.loki.lochat.integrations;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.ChatService;
import com.loki.lochat.api.service.IgnoreService;
import com.loki.lochat.api.service.PMService;
import com.loki.lochat.api.service.SpyService;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.gradient.GradientModule;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.Bukkit;
@ -19,17 +17,13 @@ import java.util.UUID;
public class PlaceholderAPIHook extends PlaceholderExpansion {
private final LoChat plugin;
private final IgnoreService ignoreService;
private final MessagingService messagingService;
private final ChatService chatService;
private final PMService pmService;
private final SpyService spyService;
public PlaceholderAPIHook(LoChat plugin) {
this.plugin = plugin;
this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class);
this.messagingService = plugin.getServiceRegistry().get(MessagingService.class);
this.chatService = plugin.getServiceRegistry().get(ChatService.class);
this.pmService = plugin.getServiceRegistry().get(PMService.class);
this.spyService = plugin.getServiceRegistry().get(SpyService.class);
}
@Override
@ -60,15 +54,15 @@ public class PlaceholderAPIHook extends PlaceholderExpansion {
GradientModule gradient = plugin.getGradientModule();
return switch (params.toLowerCase()) {
case "ignored_count" -> String.valueOf(ignoreService.getIgnoredCount(uuid));
case "ignored_count" -> String.valueOf(messagingService.getIgnoredCount(uuid));
case "global_enabled" -> String.valueOf(!chatService.isGlobalChatDisabled(uuid));
case "last_pm" -> {
Optional<UUID> lastOpt = pmService.getLastConversation(uuid);
Optional<UUID> lastOpt = messagingService.getLastConversation(uuid);
if (lastOpt.isEmpty()) yield "";
var lastPlayer = Bukkit.getOfflinePlayer(lastOpt.get());
yield lastPlayer.getName() != null ? lastPlayer.getName() : "";
}
case "spy_enabled" -> String.valueOf(spyService.isSpying(uuid));
case "spy_enabled" -> String.valueOf(messagingService.isSpying(uuid));
// Gradient placeholders (совместимость с LoPreff)
// Используем TAB формат §x§R§R§G§G§B§B для совместимости с TAB плагином

View file

@ -0,0 +1,126 @@
package com.loki.lochat.integrations.discord;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
/**
* Конфигурация Discord интеграции
*/
public class DiscordConfig {
private final JavaPlugin plugin;
private YamlConfiguration config;
public DiscordConfig(JavaPlugin plugin) {
this.plugin = plugin;
load();
}
private void load() {
File configFolder = new File(plugin.getDataFolder(), "config");
if (!configFolder.exists()) {
configFolder.mkdirs();
}
File configFile = new File(configFolder, "discord.yml");
if (!configFile.exists()) {
try {
java.io.InputStream in = plugin.getResource("config/discord.yml");
if (in != null) {
java.nio.file.Files.copy(in, configFile.toPath());
in.close();
}
} catch (Exception e) {
plugin.getLogger().warning("Failed to copy config/discord.yml: " + e.getMessage());
}
}
config = YamlConfiguration.loadConfiguration(configFile);
}
public boolean isEnabled() {
return config.getBoolean("enabled", false);
}
public String getWebhookUrl() {
return config.getString("webhook.url", "");
}
public String getUsername() {
return config.getString("webhook.username", "Minecraft Server");
}
public String getAvatarUrl() {
return config.getString("webhook.avatar_url", "");
}
public int getTimeout() {
return config.getInt("performance.timeout", 10);
}
public int getRetryAttempts() {
return config.getInt("performance.retry_attempts", 3);
}
public long getRetryDelay() {
return config.getLong("performance.retry_delay", 1000);
}
public boolean isAsync() {
return config.getBoolean("performance.async", true);
}
public boolean isChatEnabled() {
return config.getBoolean("chat.enabled", true);
}
public boolean isGlobalOnly() {
return config.getBoolean("chat.global_only", true);
}
public boolean useEmbed() {
return config.getBoolean("chat.use_embed", true);
}
public String getChatFormat() {
return config.getString("chat.format", "**{player}**: {message}");
}
public String getEmbedColor() {
return config.getString("chat.embed_color", "5865F2");
}
public boolean isEventEnabled(String event) {
return config.getBoolean("events.enabled", true) &&
config.getBoolean("events." + event + ".enabled", true);
}
public String getEventTitle(String event) {
return config.getString("events." + event + ".title", "");
}
public String getEventDescription(String event) {
return config.getString("events." + event + ".description", "");
}
public String getEventColor(String event) {
return config.getString("events." + event + ".color", "5865F2");
}
public String getEventThumbnail(String event) {
return config.getString("events." + event + ".thumbnail", "");
}
public int getMinMessageLength() {
return config.getInt("filter.min_message_length", 1);
}
public int getMaxMessageLength() {
return config.getInt("filter.max_message_length", 2000);
}
public boolean sanitizeMentions() {
return config.getBoolean("filter.sanitize_mentions", true);
}
}

View file

@ -0,0 +1,166 @@
package com.loki.lochat.integrations.discord;
import com.loki.lochat.integrations.DiscordWebhook;
import com.loki.lochat.utils.FoliaUtil;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.regex.Pattern;
/**
* Сервис для отправки сообщений в Discord
*/
public class DiscordMessageService {
private static final Pattern MENTION_PATTERN = Pattern.compile("@(everyone|here)", Pattern.CASE_INSENSITIVE);
private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("§[0-9a-fk-or]", Pattern.CASE_INSENSITIVE);
private final JavaPlugin plugin;
private final DiscordConfig config;
private final DiscordWebhook webhook;
public DiscordMessageService(JavaPlugin plugin, DiscordConfig config) {
this.plugin = plugin;
this.config = config;
this.webhook = new DiscordWebhook(
plugin,
config.getWebhookUrl(),
config.getUsername(),
getAvatarUrl(),
config.getTimeout(),
config.getRetryAttempts(),
config.getRetryDelay()
);
}
private String getAvatarUrl() {
String url = config.getAvatarUrl();
if (url.isEmpty()) {
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
return loChat.getConfigManager().getAppearanceConfig().getDefaultAvatarUrl();
}
return url;
}
public void sendChatMessage(Player player, String message, boolean isGlobal) {
String cleanMessage = sanitizeMessage(message);
if (cleanMessage.length() < config.getMinMessageLength()) {
return;
}
if (cleanMessage.length() > config.getMaxMessageLength()) {
cleanMessage = cleanMessage.substring(0, config.getMaxMessageLength() - 3) + "...";
}
String formatted = config.getChatFormat()
.replace("{player}", player.getName())
.replace("{message}", cleanMessage);
if (config.isAsync()) {
FoliaUtil.runAsync(plugin, () -> sendFormatted(formatted, player, isGlobal));
} else {
sendFormatted(formatted, player, isGlobal);
}
}
private void sendFormatted(String message, Player player, boolean isGlobal) {
if (config.useEmbed()) {
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String title = isGlobal ?
appearanceConfig.getDiscordEventTitle("global_chat") :
appearanceConfig.getDiscordEventTitle("local_chat");
if (title.isEmpty()) {
title = isGlobal ? "🌍 Глобальный чат" : "📍 Локальный чат";
}
String color = config.getEmbedColor();
String thumbnail = appearanceConfig.getPlayerAvatarUrl().replace("{player}", player.getName());
webhook.sendEmbed(title, message, color, thumbnail);
} else {
webhook.sendMessage(message);
}
}
public void sendPlayerJoin(Player player) {
sendEvent(player, "join", null);
}
public void sendPlayerQuit(Player player) {
sendEvent(player, "quit", null);
}
public void sendPlayerDeath(Player player, String deathMessage) {
sendEvent(player, "death", deathMessage);
}
private void sendEvent(Player player, String event, String extraMessage) {
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig();
String title = config.getEventTitle(event);
if (title.isEmpty()) {
title = appearanceConfig.getDiscordEventTitle(event);
}
String description = config.getEventDescription(event)
.replace("{player}", player.getName());
if (extraMessage != null) {
description = description.replace("{death_message}", sanitizeMessage(extraMessage));
}
String color = config.getEventColor(event);
String thumbnail = config.getEventThumbnail(event);
if (thumbnail.isEmpty()) {
thumbnail = appearanceConfig.getPlayerAvatarUrl();
}
final String finalThumbnail = thumbnail.replace("{player}", player.getName());
final String finalTitle = title;
final String finalDescription = description;
final String finalColor = color;
if (config.isAsync()) {
FoliaUtil.runAsync(plugin, () -> webhook.sendEmbed(finalTitle, finalDescription, finalColor, finalThumbnail));
} else {
webhook.sendEmbed(title, description, color, finalThumbnail);
}
}
public void sendTestMessage(String message, String senderName) {
String title = "🧪 Тестовое сообщение";
String description = "**Отправитель:** " + senderName + "\n**Сообщение:** " + message;
String color = "FFD700";
String thumbnail = "https://mc-heads.net/avatar/" + senderName + "/64";
webhook.sendEmbed(title, description, color, thumbnail);
}
private String sanitizeMessage(String message) {
if (message == null) return "";
message = COLOR_CODE_PATTERN.matcher(message).replaceAll("");
if (config.sanitizeMentions()) {
message = MENTION_PATTERN.matcher(message).replaceAll("@\u200B$1");
}
message = message.replace("*", "\\*")
.replace("_", "\\_")
.replace("`", "\\`")
.replace("~", "\\~");
return message.trim();
}
public boolean isValid() {
return webhook.isValid();
}
public void shutdown() {
webhook.shutdown();
}
}

View file

@ -3,6 +3,7 @@ package com.loki.lochat.listener;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MessageService;
import com.loki.lochat.core.filter.AdvancedMessageFilter;
import com.loki.lochat.core.filter.FilterResult;
import com.loki.lochat.core.registry.ServiceRegistry;
import com.loki.lochat.renderer.EnhancedChatRenderer;
import com.loki.lochat.utils.ChatFormatter;
@ -29,66 +30,81 @@ public class ChatEventListener implements Listener {
@EventHandler(priority = EventPriority.HIGHEST)
public void onChat(AsyncChatEvent event) {
Player sender = event.getPlayer();
// plain нужен только для фильтров — цвета игрока обрабатываем ниже отдельно
String rawMessage = PlainTextComponentSerializer.plainText().serialize(event.message());
// ✅ РЕЖИМ ЕДИНОГО ЧАТА: если локальный отключен, весь чат = глобальный
LoChat loChat = (LoChat) plugin;
boolean localEnabled = loChat.getConfigManager().isLocalEnabled();
boolean isGlobal;
String plainMessage;
if (localEnabled) {
// Старая логика: "!" = глобальный, без "!" = локальный
isGlobal = rawMessage.startsWith("!");
plainMessage = isGlobal ? rawMessage.substring(1).stripLeading() : rawMessage;
} else {
// Новая логика: весь чат глобальный, "!" не нужен
isGlobal = true;
plainMessage = rawMessage;
// Определяем режим чата (глобальный/локальный)
ChatMode mode = determineChatMode(loChat, rawMessage);
if (mode == null) {
event.setCancelled(true);
sender.sendMessage(ChatFormatter.parse("&#CF6679Этот режим чата отключен!"));
return;
}
// ✅ Проверка: включен ли глобальный/локальный чат
String plainMessage = mode.message;
boolean isGlobal = mode.isGlobal;
// Проверки: мут, кулдаун, фильтры
if (!processFilters(event, sender, plainMessage, isGlobal)) {
return;
}
// Обработка локального чата (радиус)
if (!isGlobal) {
handleLocalChat(event, sender, loChat);
}
// Рендеринг и интеграции
event.renderer(new EnhancedChatRenderer(plugin, isGlobal));
loChat.getDiscordIntegration().sendChatMessage(sender, plainMessage, isGlobal);
recordStatistics(loChat, sender, isGlobal);
}
private ChatMode determineChatMode(LoChat loChat, String rawMessage) {
boolean localEnabled = loChat.getConfigManager().isLocalEnabled();
boolean isGlobal;
String message;
if (localEnabled) {
isGlobal = rawMessage.startsWith("!");
message = isGlobal ? rawMessage.substring(1).stripLeading() : rawMessage;
} else {
isGlobal = true;
message = rawMessage;
}
// Проверка: включен ли режим
if (isGlobal && !loChat.getConfigManager().isGlobalEnabled()) {
event.setCancelled(true);
sender.sendMessage(ChatFormatter.parse("&#CF6679Глобальный чат отключен!"));
return;
return null;
}
if (!isGlobal && !localEnabled) {
event.setCancelled(true);
sender.sendMessage(ChatFormatter.parse("&#CF6679Локальный чат отключен!"));
return;
return null;
}
return new ChatMode(isGlobal, message);
}
private boolean processFilters(AsyncChatEvent event, Player sender, String plainMessage, boolean isGlobal) {
LoChat loChat = (LoChat) plugin;
// Мут, кулдаун
if (!messageService.processMessage(sender, plainMessage)) {
event.setCancelled(true);
return;
return false;
}
// Капс, реклама, повторы
AdvancedMessageFilter.FilterResult filterResult = advancedFilter.filterMessage(sender, plainMessage);
FilterResult filterResult = advancedFilter.filterMessage(sender, plainMessage);
if (!filterResult.allowed()) {
event.setCancelled(true);
sender.sendMessage(ChatFormatter.parse(filterResult.blockReason()));
return;
return false;
}
String filteredMessage = filterResult.filteredMessage();
// ── Цвет текста сообщения ({message}) ─────────────────────────────────
// Если стиль не задан явно, Paper может унаследовать цвет последнего символа градиента
// префикса в строке чата (например у LOCAL последняя буква давала синеватый #4169E1) —
// из‑за этого весь текст сообщения «подтягивался» под этот тон. Намеренный цвет текста
// задаётся в appearance.yml: prefixes.global.message-color / prefixes.local.message-color (#E8E0F0 по умолчанию).
// Решение: всегда выставлять стиль тела сообщения:
// • lochat.chat.colors → MiniMessage / &# / & из текста
// • иначе → parseWithDefaultMessageColor(..., message-color) + экранирование < (см. ChatFormatter)
// Цвет текста сообщения
Component messageComponent;
if (sender.hasPermission("lochat.chat.colors")) {
messageComponent = ChatFormatter.parse(filteredMessage);
} else {
@ -99,46 +115,41 @@ public class ChatEventListener implements Listener {
}
event.message(messageComponent);
return true;
}
// ✅ FIX: Радиус для локального чата - кэшируем локации в async потоке
// Paper позволяет читать Location в async, но не модифицировать World
if (!isGlobal) {
int radius = loChat.getConfigManager().getAppearanceConfig().getLocalRadius();
// Безопасно: AsyncChatEvent.viewers() thread-safe, Location.distance() тоже
event.viewers().removeIf(v -> {
if (!(v instanceof Player p)) return false;
// Проверяем мир и расстояние (чтение Location безопасно в async)
try {
return !com.loki.lochat.util.DistanceUtil.isInRange(sender, p, radius);
} catch (Exception e) {
// На всякий случай ловим исключения
return false;
}
});
private void handleLocalChat(AsyncChatEvent event, Player sender, LoChat loChat) {
int radius = loChat.getConfigManager().getAppearanceConfig().getLocalRadius();
long recipients = event.viewers().stream()
.filter(v -> v instanceof Player p && !p.equals(sender))
.count();
if (recipients == 0) {
String msg = loChat.getConfigManager().getMessagesConfig().getNobodyHeard();
sender.getScheduler().run(plugin, t ->
sender.sendMessage(ChatFormatter.parse(msg)), null
);
event.viewers().removeIf(v -> {
if (!(v instanceof Player p)) return false;
try {
return !com.loki.lochat.utils.DistanceUtil.isInRange(sender, p, radius);
} catch (Exception e) {
return false;
}
});
long recipients = event.viewers().stream()
.filter(v -> v instanceof Player p && !p.equals(sender))
.count();
if (recipients == 0) {
String msg = loChat.getConfigManager().getMessagesConfig().getNobodyHeard();
sender.getScheduler().run(plugin, t ->
sender.sendMessage(ChatFormatter.parse(msg)), null
);
}
}
event.renderer(new EnhancedChatRenderer(plugin, isGlobal));
loChat.getDiscordIntegration().sendChatMessage(sender, filteredMessage, isGlobal);
// Статистика
com.loki.lochat.api.service.PlayerDataService pds =
loChat.getServiceRegistry().get(com.loki.lochat.api.service.PlayerDataService.class);
if (pds instanceof com.loki.lochat.core.service.PlayerDataServiceImpl impl) {
private void recordStatistics(LoChat loChat, Player sender, boolean isGlobal) {
com.loki.lochat.api.service.PlayerService playerService =
loChat.getServiceRegistry().get(com.loki.lochat.api.service.PlayerService.class);
if (playerService instanceof com.loki.lochat.core.service.PlayerServiceImpl impl) {
impl.recordMessage(sender.getUniqueId(), isGlobal ? "global" : "local");
}
}
private record ChatMode(boolean isGlobal, String message) {
}
}

View file

@ -1,9 +1,8 @@
package com.loki.lochat.listener;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.api.service.NickService;
import com.loki.lochat.api.service.PlayerDataService;
import com.loki.lochat.api.service.PMService;
import com.loki.lochat.api.service.SpyService;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.core.filter.AdvancedMessageFilter;
import com.loki.lochat.core.registry.ServiceRegistry;
import org.bukkit.entity.Player;
@ -16,17 +15,15 @@ import org.bukkit.event.player.PlayerQuitEvent;
* Слушатель событий игрока
*/
public class PlayerEventListener implements Listener {
private final PlayerDataService playerDataService;
private final PlayerService playerService;
private final MessagingService messagingService;
private final NickService nickService;
private final PMService pmService;
private final SpyService spyService;
private final AdvancedMessageFilter advancedFilter;
public PlayerEventListener(ServiceRegistry registry, AdvancedMessageFilter advancedFilter) {
this.playerDataService = registry.get(PlayerDataService.class);
this.nickService = registry.get(NickService.class);
this.pmService = registry.get(PMService.class);
this.spyService = registry.get(SpyService.class);
this.playerService = registry.get(PlayerService.class);
this.messagingService = registry.get(MessagingService.class);
this.nickService = registry.get(NickService.class);
this.advancedFilter = advancedFilter;
}
@ -43,9 +40,9 @@ public class PlayerEventListener implements Listener {
Player player = event.getPlayer();
java.util.UUID uuid = player.getUniqueId();
playerDataService.clearPlayerData(uuid);
pmService.removeConversation(uuid);
spyService.removeSpy(uuid);
playerService.clearPlayerData(uuid);
messagingService.removeConversation(uuid);
messagingService.removeSpy(uuid);
// Очищаем flood / spam трекеры — иначе данные висят в памяти вечно
advancedFilter.clearPlayer(uuid);

View file

@ -1,7 +1,7 @@
package com.loki.lochat.managers;
import com.loki.lochat.LoChat;
import com.loki.lochat.util.FoliaUtil;
import com.loki.lochat.utils.FoliaUtil;
import com.loki.lochat.utils.ChatFormatter;
import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
import net.kyori.adventure.text.Component;

View file

@ -2,9 +2,9 @@ package com.loki.lochat.managers;
import com.loki.lochat.LoChat;
import com.loki.lochat.commands.CustomCommand;
import com.loki.lochat.managers.commands.MessageSender;
import com.loki.lochat.managers.commands.PlaceholderProcessor;
import com.loki.lochat.utils.ChatFormatter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.title.Title;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandMap;
import org.bukkit.configuration.ConfigurationSection;
@ -13,7 +13,6 @@ import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import java.io.File;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -41,11 +40,8 @@ public class CustomCommandManager {
}
FileConfiguration commandsConfig = YamlConfiguration.loadConfiguration(commandsFile);
// Очищаем старые команды
commands.clear();
// Загружаем команды из конфига
for (String commandName : commandsConfig.getKeys(false)) {
ConfigurationSection section = commandsConfig.getConfigurationSection(commandName);
if (section == null) continue;
@ -61,8 +57,6 @@ public class CustomCommandManager {
);
commands.put(commandName, data);
// Регистрируем команду
registerCommand(data);
}
@ -71,14 +65,11 @@ public class CustomCommandManager {
private void registerCommand(CustomCommandData data) {
try {
// Получаем CommandMap через Paper API (безопаснее чем рефлексия)
CommandMap commandMap = Bukkit.getCommandMap();
// Создаем и регистрируем команду
CustomCommand command = new CustomCommand(data.name(), this);
commandMap.register("lochat", command);
// Регистрируем алиасы
for (String alias : data.aliases()) {
CustomCommand aliasCommand = new CustomCommand(alias, this);
commandMap.register("lochat", aliasCommand);
@ -97,7 +88,6 @@ public class CustomCommandManager {
public boolean executeCustomCommand(String commandName, Player player, String[] args) {
CustomCommandData data = commands.get(commandName);
if (data == null) {
// Проверяем алиасы
for (CustomCommandData cmd : commands.values()) {
if (cmd.aliases().contains(commandName)) {
data = cmd;
@ -108,173 +98,32 @@ public class CustomCommandManager {
if (data == null) return false;
// Проверяем включена ли команда
if (!data.enabled()) {
// Не показываем ошибку, просто игнорируем команду
return true;
}
// Проверяем права
if (data.permission() != null && !player.hasPermission(data.permission())) {
player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getNoPermission()));
return true;
}
// Обрабатываем плейсхолдеры
String message = processPlaceholders(data.message(), player, args);
String message = PlaceholderProcessor.process(data.message(), player, args);
// Выполняем команду в зависимости от типа
switch (data.type().toLowerCase()) {
case "broadcast" -> sendBroadcast(data.target(), message);
case "title" -> sendTitle(data.target(), player, message);
case "actionbar" -> sendActionBar(data.target(), player, message);
case "title" -> MessageSender.sendTitle(data.target(), player, message);
case "actionbar" -> MessageSender.sendActionBar(data.target(), player, message);
case "bungee" -> sendBungeeCommand(player, data);
default -> sendChatMessage(data.target(), player, message);
default -> MessageSender.sendChat(data.target(), player, message);
}
return true;
}
private String processPlaceholders(String message, Player player, String[] args) {
String result = message;
// Базовые плейсхолдеры
result = result.replace("{player}", player.getName());
result = result.replace("{x}", String.valueOf(player.getLocation().getBlockX()));
result = result.replace("{y}", String.valueOf(player.getLocation().getBlockY()));
result = result.replace("{z}", String.valueOf(player.getLocation().getBlockZ()));
result = result.replace("{world}", player.getWorld().getName());
result = result.replace("{ping}", String.valueOf(player.getPing()));
// Аргументы команды
for (int i = 0; i < args.length; i++) {
result = result.replace("{arg" + i + "}", args[i]);
}
result = result.replace("{args}", String.join(" ", args));
// PlaceholderAPI если доступен
if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
try {
// Используем безопасный способ вызова PlaceholderAPI
Class<?> papiClass = Class.forName("me.clip.placeholderapi.PlaceholderAPI");
java.lang.reflect.Method setPlaceholdersMethod = papiClass.getMethod("setPlaceholders", org.bukkit.entity.Player.class, String.class);
result = (String) setPlaceholdersMethod.invoke(null, player, result);
} catch (Exception e) {
// Если PlaceholderAPI недоступен или произошла ошибка, просто пропускаем
plugin.getLogger().warning("Ошибка при обработке PlaceholderAPI плейсхолдеров: " + e.getMessage());
}
}
return result;
}
private void sendChatMessage(String target, Player sender, String message) {
Component component = ChatFormatter.parse(message);
switch (target.toLowerCase()) {
case "sender" -> sender.sendMessage(component);
case "all" -> {
for (Player player : Bukkit.getOnlinePlayers()) {
player.sendMessage(component);
}
}
default -> {
if (target.startsWith("permission:")) {
String permission = target.substring(11);
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.hasPermission(permission)) {
player.sendMessage(component);
}
}
} else {
sender.sendMessage(component);
}
}
}
}
private void sendBroadcast(String target, String message) {
Component component = ChatFormatter.parse(message);
if (target.equalsIgnoreCase("all")) {
for (Player player : Bukkit.getOnlinePlayers()) {
player.sendMessage(component);
}
} else {
if (target.startsWith("permission:")) {
String permission = target.substring(11);
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.hasPermission(permission)) {
player.sendMessage(component);
}
}
}
}
}
private void sendTitle(String target, Player sender, String message) {
String[] parts = message.split("\\n", 2);
Component title = ChatFormatter.parse(parts[0]);
Component subtitle = parts.length > 1 ? ChatFormatter.parse(parts[1]) : Component.empty();
Title titleObj = Title.title(title, subtitle,
Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(3), Duration.ofMillis(500)));
switch (target.toLowerCase()) {
case "sender" -> sender.showTitle(titleObj);
case "all" -> {
for (Player player : Bukkit.getOnlinePlayers()) {
player.showTitle(titleObj);
}
}
default -> {
if (target.startsWith("permission:")) {
String permission = target.substring(11);
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.hasPermission(permission)) {
player.showTitle(titleObj);
}
}
} else {
sender.showTitle(titleObj);
}
}
}
}
private void sendActionBar(String target, Player sender, String message) {
Component component = ChatFormatter.parse(message);
switch (target.toLowerCase()) {
case "sender" -> sender.sendActionBar(component);
case "all" -> {
for (Player player : Bukkit.getOnlinePlayers()) {
player.sendActionBar(component);
}
}
default -> {
if (target.startsWith("permission:")) {
String permission = target.substring(11);
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.hasPermission(permission)) {
player.sendActionBar(component);
}
}
} else {
sender.sendActionBar(component);
}
}
}
}
private void sendBungeeCommand(Player player, CustomCommandData data) {
// Отправляем сообщение игроку
if (!data.message().isEmpty()) {
Component component = ChatFormatter.parse(data.message());
player.sendMessage(component);
player.sendMessage(ChatFormatter.parse(data.message()));
}
// Подключаем к серверу через BungeeCord
String serverName = getServerFromConfig(data);
if (serverName != null && !serverName.isEmpty()) {
connectToServer(player, serverName);
@ -282,7 +131,6 @@ public class CustomCommandManager {
}
private String getServerFromConfig(CustomCommandData data) {
// Ищем параметр server в конфиге команды
try {
File commandsFile = new File(plugin.getDataFolder(), "custom-commands.yml");
FileConfiguration config = YamlConfiguration.loadConfiguration(commandsFile);

View file

@ -0,0 +1,73 @@
package com.loki.lochat.managers.commands;
import com.loki.lochat.utils.ChatFormatter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.title.Title;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.time.Duration;
public class MessageSender {
public static void sendChat(String target, Player sender, String message) {
Component formatted = ChatFormatter.parse(message);
switch (target.toLowerCase()) {
case "player" -> sender.sendMessage(formatted);
case "broadcast" -> Bukkit.broadcast(formatted);
case "server" -> Bukkit.getOnlinePlayers().forEach(p -> p.sendMessage(formatted));
default -> {
Player targetPlayer = Bukkit.getPlayer(target);
if (targetPlayer != null && targetPlayer.isOnline()) {
targetPlayer.sendMessage(formatted);
}
}
}
}
public static void sendTitle(String target, Player sender, String message) {
String[] parts = message.split("\\|", 2);
String titleText = parts.length > 0 ? parts[0] : "";
String subtitleText = parts.length > 1 ? parts[1] : "";
Component title = ChatFormatter.parse(titleText);
Component subtitle = ChatFormatter.parse(subtitleText);
Title titleObj = Title.title(
title,
subtitle,
Title.Times.times(
Duration.ofMillis(500),
Duration.ofMillis(3000),
Duration.ofMillis(500)
)
);
switch (target.toLowerCase()) {
case "player" -> sender.showTitle(titleObj);
case "broadcast", "server" -> Bukkit.getOnlinePlayers().forEach(p -> p.showTitle(titleObj));
default -> {
Player targetPlayer = Bukkit.getPlayer(target);
if (targetPlayer != null && targetPlayer.isOnline()) {
targetPlayer.showTitle(titleObj);
}
}
}
}
public static void sendActionBar(String target, Player sender, String message) {
Component formatted = ChatFormatter.parse(message);
switch (target.toLowerCase()) {
case "player" -> sender.sendActionBar(formatted);
case "broadcast", "server" -> Bukkit.getOnlinePlayers().forEach(p -> p.sendActionBar(formatted));
default -> {
Player targetPlayer = Bukkit.getPlayer(target);
if (targetPlayer != null && targetPlayer.isOnline()) {
targetPlayer.sendActionBar(formatted);
}
}
}
}
}

View file

@ -0,0 +1,36 @@
package com.loki.lochat.managers.commands;
import org.bukkit.entity.Player;
public class PlaceholderProcessor {
public static String process(String message, Player player, String[] args) {
String result = message;
// Базовые плейсхолдеры
result = result.replace("{player}", player.getName());
result = result.replace("{displayname}", player.getDisplayName());
result = result.replace("{world}", player.getWorld().getName());
result = result.replace("{x}", String.valueOf(player.getLocation().getBlockX()));
result = result.replace("{y}", String.valueOf(player.getLocation().getBlockY()));
result = result.replace("{z}", String.valueOf(player.getLocation().getBlockZ()));
result = result.replace("{health}", String.valueOf(Math.round(player.getHealth())));
result = result.replace("{food}", String.valueOf(player.getFoodLevel()));
result = result.replace("{level}", String.valueOf(player.getLevel()));
result = result.replace("{exp}", String.valueOf(player.getExp()));
// Аргументы команды
for (int i = 0; i < args.length; i++) {
result = result.replace("{arg" + i + "}", args[i]);
}
// Все аргументы вместе
if (args.length > 0) {
result = result.replace("{args}", String.join(" ", args));
} else {
result = result.replace("{args}", "");
}
return result;
}
}

View file

@ -1,5 +1,6 @@
package com.loki.lochat.renderer;
import com.loki.lochat.renderer.components.*;
import com.loki.lochat.utils.MentionHandler;
import com.loki.lochat.utils.TextFormatter;
import io.papermc.paper.chat.ChatRenderer;
@ -9,16 +10,13 @@ import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.List;
/**
* Рендерер чата — глобальный и локальный режимы.
* Цвета PREFIX и текста сообщения полностью настраиваются через chat-appearance.yml.
* Рендерер чата — глобальный и локальный режимы
*/
public class EnhancedChatRenderer implements ChatRenderer {
@ -43,14 +41,14 @@ public class EnhancedChatRenderer implements ChatRenderer {
// Обрабатываем сообщение
Component processed = message;
processed = TextFormatter.formatMarkdown(processed);
processed = processUrls(processed);
processed = processEmojis(processed);
processed = UrlProcessor.process(processed);
processed = EmojiComponent.processEmojis(processed);
if (viewer instanceof Player vp) {
processed = mentionHandler.processMentions(processed, source, vp);
}
// ✅ Получаем кастомный формат из конфига
// Получаем формат из конфига
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
String format = isGlobal ? appearanceCfg.getGlobalChatFormat() : appearanceCfg.getLocalChatFormat();
@ -61,13 +59,9 @@ public class EnhancedChatRenderer implements ChatRenderer {
Component playerPrefix = buildPlayerPrefix(source);
Component player = buildPlayerComponent(source);
// Парсим формат и заменяем плейсхолдеры
return buildFormattedMessage(format, emoji, prefix, separator, playerPrefix, player, processed);
}
/**
* Строит финальное сообщение по формату с плейсхолдерами
*/
private Component buildFormattedMessage(String format, Component emoji, Component prefix,
Component separator, Component playerPrefix,
Component player, Component message) {
@ -78,12 +72,10 @@ public class EnhancedChatRenderer implements ChatRenderer {
int lastEnd = 0;
while (matcher.find()) {
// Добавляем текст до плейсхолдера
if (matcher.start() > lastEnd) {
result = result.append(Component.text(format.substring(lastEnd, matcher.start())));
}
// Заменяем плейсхолдер
String placeholder = matcher.group(1);
switch (placeholder) {
case "emoji" -> result = result.append(emoji);
@ -98,7 +90,6 @@ public class EnhancedChatRenderer implements ChatRenderer {
lastEnd = matcher.end();
}
// Добавляем оставшийся текст
if (lastEnd < format.length()) {
result = result.append(Component.text(format.substring(lastEnd)));
}
@ -106,115 +97,35 @@ public class EnhancedChatRenderer implements ChatRenderer {
return result;
}
/**
* Строит компонент эмодзи
*/
private Component buildEmojiComponent() {
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
String emoji = isGlobal ? appearanceCfg.getGlobalEmoji() : appearanceCfg.getLocalEmoji();
return emoji.isEmpty() ? Component.empty() : Component.text(emoji);
}
/**
* Строит префикс игрока из LuckPerms/градиента
*/
private Component buildPlayerPrefix(Player player) {
// Получаем градиентный модуль
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
com.loki.lochat.gradient.GradientModule gradientModule = loChat.getGradientModule();
if (gradientModule != null) {
com.loki.lochat.gradient.data.GradientPlayerData data =
gradientModule.getDataManager().getPlayerData(player.getUniqueId());
if (data != null && data.isPrefixEnabled() && data.hasPrefix()) {
String prefixText = data.getPrefix();
if (data.hasColors() && data.isColorEnabled()) {
// Применяем градиент к префиксу
return buildGradientText(prefixText, data.getColors());
}
return Component.text(prefixText);
if (gradientModule != null && gradientModule.isEnabled()) {
String prefix = gradientModule.getPrefix(player);
if (!prefix.isEmpty()) {
return com.loki.lochat.utils.ChatFormatter.parse(prefix);
}
}
return Component.empty();
}
// ──────────────────────────────────────────────────────────────────────────
// Префикс
// ──────────────────────────────────────────────────────────────────────────
private Component buildPrefix() {
// ✅ FIX: Получаем AppearanceConfig через ConfigManager
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
String text = isGlobal ? appearanceCfg.getGlobalText() : appearanceCfg.getLocalText();
List<String> colors = isGlobal ? appearanceCfg.getGlobalColors() : appearanceCfg.getLocalColors();
return buildGradientText(text, colors);
return GradientBuilder.build(text, colors);
}
/**
* Строит компонент с плавным градиентом.
* colors — список hex-цветов (минимум 1).
* Если цветов меньше чем букв — интерполируем плавно между соседними цветами.
* Если цветов больше чем букв — берём по одному на букву.
*/
private Component buildGradientText(String text, List<String> colors) {
if (text.isEmpty()) return Component.empty();
if (colors.isEmpty()) {
return Component.text(text, FALLBACK_COLOR);
}
// Один цвет — просто красим весь текст
if (colors.size() == 1) {
return Component.text(text, parseColor(colors.get(0)));
}
char[] chars = text.toCharArray();
int len = chars.length;
Component result = Component.empty();
for (int i = 0; i < len; i++) {
// Нормализованная позиция 0..1
float t = len == 1 ? 0f : (float) i / (len - 1);
// Какой сегмент градиента
float scaled = t * (colors.size() - 1);
int seg = (int) scaled;
float frac = scaled - seg;
// Зажимаем в пределах списка
if (seg >= colors.size() - 1) {
seg = colors.size() - 2;
frac = 1f;
}
TextColor from = parseColor(colors.get(seg));
TextColor to = parseColor(colors.get(seg + 1));
TextColor blended = blend(from, to, frac);
result = result.append(Component.text(String.valueOf(chars[i]), blended));
}
return result;
}
/** Линейная интерполяция между двумя TextColor */
private static TextColor blend(TextColor from, TextColor to, float t) {
int r = Math.round(from.red() + t * (to.red() - from.red()));
int g = Math.round(from.green() + t * (to.green() - from.green()));
int b = Math.round(from.blue() + t * (to.blue() - from.blue()));
return TextColor.color(clamp(r), clamp(g), clamp(b));
}
private static int clamp(int v) { return Math.max(0, Math.min(255, v)); }
// ──────────────────────────────────────────────────────────────────────────
// Разделитель
// ──────────────────────────────────────────────────────────────────────────
private Component buildSeparator() {
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
String text = isGlobal ? appearanceCfg.getGlobalSeparatorText() : appearanceCfg.getLocalSeparatorText();
@ -222,14 +133,9 @@ public class EnhancedChatRenderer implements ChatRenderer {
return Component.text(text, parseColor(color));
}
// ──────────────────────────────────────────────────────────────────────────
// Ник игрока с hover
// ──────────────────────────────────────────────────────────────────────────
private Component buildPlayerComponent(Player player) {
Component displayName = player.displayName();
// ✅ FIX: Получаем AppearanceConfig для проверки hover
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
if (!appearanceCfg.isHoverEnabled()) {
@ -271,42 +177,6 @@ public class EnhancedChatRenderer implements ChatRenderer {
.clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " "));
}
// ──────────────────────────────────────────────────────────────────────────
// URL и эмодзи
// ──────────────────────────────────────────────────────────────────────────
private Component processUrls(Component message) {
String plain = PlainTextComponentSerializer.plainText().serialize(message);
java.util.regex.Pattern pattern =
java.util.regex.Pattern.compile("(https?://[\\w\\-.~:/?#\\[\\]@!$&'()*+,;=%]+)");
java.util.regex.Matcher matcher = pattern.matcher(plain);
Component result = message;
while (matcher.find()) {
String url = matcher.group(1);
Component urlComp = Component.text(url, NamedTextColor.AQUA, TextDecoration.UNDERLINED)
.hoverEvent(HoverEvent.showText(Component.text("Открыть: " + url, NamedTextColor.GRAY)))
.clickEvent(ClickEvent.openUrl(url));
result = result.replaceText(b -> b.matchLiteral(url).replacement(urlComp));
}
return result;
}
private Component processEmojis(Component message) {
// ✅ FIX: Получаем AppearanceConfig для эмодзи
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
Component result = message;
for (java.util.Map.Entry<String, String> e : appearanceCfg.getEmojis().entrySet()) {
result = result.replaceText(b -> b.matchLiteral(e.getKey()).replacement(e.getValue()));
}
return result;
}
// ──────────────────────────────────────────────────────────────────────────
// Утилиты
// ──────────────────────────────────────────────────────────────────────────
private TextColor parseColor(String hex) {
if (hex == null || hex.isBlank()) return FALLBACK_COLOR;
String n = hex.startsWith("#") ? hex : "#" + hex;

View file

@ -0,0 +1,36 @@
package com.loki.lochat.renderer.components;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.configuration.ConfigurationSection;
public class EmojiComponent {
public static Component build(ConfigurationSection emojiSection) {
if (emojiSection == null || !emojiSection.getBoolean("enabled", false)) {
return Component.empty();
}
String text = emojiSection.getString("text", "");
String color = emojiSection.getString("color", "#FFFFFF");
return Component.text(text)
.color(TextColor.fromHexString(color));
}
public static Component processEmojis(Component message) {
if (!(message instanceof TextComponent textComp)) {
return message;
}
String content = textComp.content();
content = content.replace(":heart:", "❤")
.replace(":star:", "⭐")
.replace(":fire:", "🔥")
.replace(":check:", "✓")
.replace(":cross:", "✗");
return textComp.content(content);
}
}

View file

@ -0,0 +1,48 @@
package com.loki.lochat.renderer.components;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.TextColor;
import java.util.List;
public class GradientBuilder {
public static Component build(String text, List<String> colors) {
if (colors == null || colors.size() < 2) {
return Component.text(text);
}
TextColor[] gradientColors = colors.stream()
.map(TextColor::fromHexString)
.toArray(TextColor[]::new);
TextComponent.Builder builder = Component.text();
int len = text.length();
for (int i = 0; i < len; i++) {
float t = (float) i / (len - 1);
int segmentIndex = (int) (t * (gradientColors.length - 1));
float segmentT = (t * (gradientColors.length - 1)) - segmentIndex;
TextColor from = gradientColors[segmentIndex];
TextColor to = gradientColors[Math.min(segmentIndex + 1, gradientColors.length - 1)];
TextColor blended = blend(from, to, segmentT);
builder.append(Component.text(text.charAt(i)).color(blended));
}
return builder.build();
}
private static TextColor blend(TextColor from, TextColor to, float t) {
int r = (int) (from.red() * (1 - t) + to.red() * t);
int g = (int) (from.green() * (1 - t) + to.green() * t);
int b = (int) (from.blue() * (1 - t) + to.blue() * t);
return TextColor.color(clamp(r), clamp(g), clamp(b));
}
private static int clamp(int v) {
return Math.max(0, Math.min(255, v));
}
}

View file

@ -0,0 +1,53 @@
package com.loki.lochat.renderer.components;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.TextColor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class UrlProcessor {
private static final Pattern URL_PATTERN = Pattern.compile(
"https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+"
);
public static Component process(Component message) {
if (!(message instanceof TextComponent textComp)) {
return message;
}
String content = textComp.content();
Matcher matcher = URL_PATTERN.matcher(content);
if (!matcher.find()) {
return message;
}
TextComponent.Builder builder = Component.text();
int lastEnd = 0;
matcher.reset();
while (matcher.find()) {
if (matcher.start() > lastEnd) {
builder.append(Component.text(content.substring(lastEnd, matcher.start())));
}
String url = matcher.group();
builder.append(Component.text(url)
.color(TextColor.fromHexString("#5DADE2"))
.clickEvent(ClickEvent.openUrl(url))
.hoverEvent(HoverEvent.showText(Component.text("Открыть: " + url))));
lastEnd = matcher.end();
}
if (lastEnd < content.length()) {
builder.append(Component.text(content.substring(lastEnd)));
}
return builder.build();
}
}

View file

@ -1,57 +0,0 @@
package com.loki.lochat.util;
/**
* Утилита для парсинга времени (DRY - вынесена общая логика)
*/
public class TimeParser {
/**
* Парсит строку времени в миллисекунды
*
* @param timeStr строка вида "1d", "2h", "30m", "60s"
* @return миллисекунды или -1 при ошибке
*/
public static long parse(String timeStr) {
if (timeStr == null || timeStr.isEmpty()) return 0;
timeStr = timeStr.toLowerCase();
try {
if (timeStr.endsWith("d")) {
return Long.parseLong(timeStr.replace("d", "")) * 24 * 60 * 60 * 1000L;
} else if (timeStr.endsWith("h")) {
return Long.parseLong(timeStr.replace("h", "")) * 60 * 60 * 1000L;
} else if (timeStr.endsWith("m")) {
return Long.parseLong(timeStr.replace("m", "")) * 60 * 1000L;
} else if (timeStr.endsWith("s")) {
return Long.parseLong(timeStr.replace("s", "")) * 1000L;
} else {
return Long.parseLong(timeStr) * 60 * 1000L; // По умолчанию минуты
}
} catch (NumberFormatException e) {
return -1;
}
}
/**
* Форматирует миллисекунды в читаемую строку
*/
public static String format(long millis) {
if (millis <= 0) return "0с";
long seconds = millis / 1000;
long minutes = seconds / 60;
long hours = minutes / 60;
long days = hours / 24;
if (days > 0) {
return days + "д " + (hours % 24) + "ч";
} else if (hours > 0) {
return hours + "ч " + (minutes % 60) + "м";
} else if (minutes > 0) {
return minutes + "м " + (seconds % 60) + "с";
} else {
return seconds + "с";
}
}
}

View file

@ -1,146 +1,49 @@
package com.loki.lochat.utils;
import com.loki.lochat.utils.color.ColorConverter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.entity.Player;
/**
* Утилита для форматирования сообщений - упрощенная версия
*/
public final class ChatFormatter {
private static final MiniMessage MM = MiniMessage.miniMessage();
private static final PlainTextComponentSerializer PLAIN =
PlainTextComponentSerializer.plainText();
private static final PlainTextComponentSerializer PLAIN = PlainTextComponentSerializer.plainText();
private ChatFormatter() {
}
/* ===================== BASIC ===================== */
// ========== Базовые методы ==========
public static Component parse(String message) {
if (message == null) return Component.empty();
// Конвертируем legacy форматы в MiniMessage перед парсингом
String converted = convertLegacyFormats(message);
String converted = ColorConverter.convertLegacyFormats(message);
return MM.deserialize(converted);
}
/**
* Default message color for players without {@code lochat.chat.colors}.
* Escapes {@code \} and {@code <} in the raw text so a literal {@code </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);
String hex = (hexCode == null || hexCode.isBlank()) ? "#FFFFFF" :
(hexCode.startsWith("#") ? hexCode : "#" + hexCode);
String escaped = ColorConverter.escapeForMiniMessage(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;
for (int i = 0; i + 1 < replacements.length; i += 2) {
result = result.replace(replacements[i], replacements[i + 1]);
}
// Конвертируем legacy форматы в MiniMessage перед парсингом
String converted = convertLegacyFormats(result);
String converted = ColorConverter.convertLegacyFormats(result);
return MM.deserialize(converted);
}
/**
* Конвертирует legacy форматы цветов в MiniMessage формат
*/
private static String convertLegacyFormats(String message) {
if (message == null) return "";
// Конвертируем &#RRGGBB в <color:#RRGGBB>
java.util.regex.Pattern pattern1 = java.util.regex.Pattern.compile("&#([0-9a-fA-F]{6})");
java.util.regex.Matcher matcher1 = pattern1.matcher(message);
StringBuffer sb1 = new StringBuffer();
while (matcher1.find()) {
String hex = matcher1.group(1).toLowerCase();
matcher1.appendReplacement(sb1, "<color:#" + hex + ">");
}
matcher1.appendTail(sb1);
message = sb1.toString();
// Конвертируем #RRGGBB в <color:#RRGGBB> (только если не внутри тегов)
java.util.regex.Pattern pattern2 = java.util.regex.Pattern.compile("(?<!<)#([0-9a-fA-F]{6})(?![^<]*>)");
java.util.regex.Matcher matcher2 = pattern2.matcher(message);
StringBuffer sb2 = new StringBuffer();
while (matcher2.find()) {
String hex = matcher2.group(1).toLowerCase();
matcher2.appendReplacement(sb2, "<color:#" + hex + ">");
}
matcher2.appendTail(sb2);
message = sb2.toString();
// Конвертируем §[0-9a-fk-or] в соответствующие MiniMessage теги
message = message.replaceAll("§0", "<black>");
message = message.replaceAll("§1", "<dark_blue>");
message = message.replaceAll("§2", "<dark_green>");
message = message.replaceAll("§3", "<dark_aqua>");
message = message.replaceAll("§4", "<dark_red>");
message = message.replaceAll("§5", "<dark_purple>");
message = message.replaceAll("§6", "<gold>");
message = message.replaceAll("§7", "<gray>");
message = message.replaceAll("§8", "<dark_gray>");
message = message.replaceAll("§9", "<blue>");
message = message.replaceAll("§a", "<green>");
message = message.replaceAll("§b", "<aqua>");
message = message.replaceAll("§c", "<red>");
message = message.replaceAll("§d", "<light_purple>");
message = message.replaceAll("§e", "<yellow>");
message = message.replaceAll("§f", "<white>");
message = message.replaceAll("§k", "<obfuscated>");
message = message.replaceAll("§l", "<bold>");
message = message.replaceAll("§m", "<strikethrough>");
message = message.replaceAll("§n", "<underlined>");
message = message.replaceAll("§o", "<italic>");
message = message.replaceAll("§r", "<reset>");
// Конвертируем &[0-9a-fk-or] в соответствующие MiniMessage теги
message = message.replaceAll("&0", "<black>");
message = message.replaceAll("&1", "<dark_blue>");
message = message.replaceAll("&2", "<dark_green>");
message = message.replaceAll("&3", "<dark_aqua>");
message = message.replaceAll("&4", "<dark_red>");
message = message.replaceAll("&5", "<dark_purple>");
message = message.replaceAll("&6", "<gold>");
message = message.replaceAll("&7", "<gray>");
message = message.replaceAll("&8", "<dark_gray>");
message = message.replaceAll("&9", "<blue>");
message = message.replaceAll("&a", "<green>");
message = message.replaceAll("&b", "<aqua>");
message = message.replaceAll("&c", "<red>");
message = message.replaceAll("&d", "<light_purple>");
message = message.replaceAll("&e", "<yellow>");
message = message.replaceAll("&f", "<white>");
// Конвертируем форматирование
message = message.replaceAll("&k", "<obfuscated>");
message = message.replaceAll("&l", "<bold>");
message = message.replaceAll("&m", "<strikethrough>");
message = message.replaceAll("&n", "<underlined>");
message = message.replaceAll("&o", "<italic>");
message = message.replaceAll("&r", "<reset>");
return message;
}
// ========== Утилиты ==========
public static String stripTags(String message) {
if (message == null) return "";
@ -159,63 +62,24 @@ public final class ChatFormatter {
return obj.toString();
}
/**
* Отправляет сообщение игроку
*/
public static void sendMessage(Player player, Component component) {
if (player != null && component != null) {
player.sendMessage(component);
}
}
/* ===================== COLORS ===================== */
/**
* Конвертирует различные форматы цветов в MiniMessage формат
*/
public static String convertAllColors(String message) {
if (message == null) return "";
return convertLegacyFormats(message);
return ColorConverter.convertLegacyFormats(message);
}
/* ===================== EMOJIS (REMOVED) ===================== */
public static String replaceEmojis(String message) {
return message; // Эмодзи удалены
}
public static String replaceEmojis(String message, Player player) {
return message; // Эмодзи удалены
}
/* ===================== CHAT ===================== */
/**
* Legacy API для ChatManager
*/
public static String processPlayerMessage(
String message,
Player player,
boolean hasColorPermission
) {
return message;
}
/* ===================== ANNOUNCE ===================== */
// ========== Форматирование сообщений ==========
public static Component formatAnnouncement(String format, String message) {
return parse(format.replace("<message>", message));
}
/* ===================== PM ===================== */
public static Component formatPmSentNew(
String format,
Player sender,
Player receiver,
String message
) {
// Получаем отображаемые имена игроков
public static Component formatPmSentNew(String format, Player sender, Player receiver, String message) {
String senderDisplay = getPlayerDisplayName(sender);
String receiverDisplay = getPlayerDisplayName(receiver);
@ -227,13 +91,7 @@ public final class ChatFormatter {
.replace("{message}", message));
}
public static Component formatPmReceivedNew(
String format,
Player sender,
Player receiver,
String message
) {
// Получаем отображаемые имена игроков
public static Component formatPmReceivedNew(String format, Player sender, Player receiver, String message) {
String senderDisplay = getPlayerDisplayName(sender);
String receiverDisplay = getPlayerDisplayName(receiver);
@ -245,40 +103,45 @@ public final class ChatFormatter {
.replace("{message}", message));
}
/**
* Получает отображаемое имя игрока с градиентом
*/
private static String getPlayerDisplayName(Player player) {
try {
// Получаем экземпляр плагина
com.loki.lochat.LoChat plugin = com.loki.lochat.LoChat.getInstance();
// Если градиентный модуль включен, используем его
if (plugin.getGradientModule() != null && plugin.getGradientModule().isEnabled()) {
return plugin.getGradientModule().getFormattedName(player);
}
} catch (Exception e) {
// Если произошла ошибка, логируем и возвращаем обычное имя
com.loki.lochat.LoChat.getInstance().getLogger().warning(
"Ошибка при получении градиентного имени для " + player.getName() + ": " + e.getMessage()
);
}
// Иначе просто имя игрока
return player.getName();
}
/* ===================== FORMAT (Component-based) ===================== */
public static Component format(
String format,
Component name,
Component prefix,
Component message
) {
public static Component format(String format, Component name, Component prefix, Component message) {
return MM.deserialize(format)
.replaceText(b -> b.matchLiteral("{name}").replacement(name))
.replaceText(b -> b.matchLiteral("{prefix}").replacement(prefix))
.replaceText(b -> b.matchLiteral("{message}").replacement(message));
}
// ========== Приватные методы ==========
private static String getPlayerDisplayName(Player player) {
try {
com.loki.lochat.LoChat plugin = com.loki.lochat.LoChat.getInstance();
if (plugin.getGradientModule() != null && plugin.getGradientModule().isEnabled()) {
return plugin.getGradientModule().getFormattedName(player);
}
} catch (Exception e) {
com.loki.lochat.LoChat.getInstance().getLogger().warning(
"Ошибка при получении градиентного имени для " + player.getName() + ": " + e.getMessage()
);
}
return player.getName();
}
// ========== Deprecated методы для обратной совместимости ==========
@Deprecated
public static String replaceEmojis(String message) {
return message;
}
@Deprecated
public static String replaceEmojis(String message, Player player) {
return message;
}
@Deprecated
public static String processPlayerMessage(String message, Player player, boolean hasColorPermission) {
return message;
}
}

View file

@ -1,4 +1,4 @@
package com.loki.lochat.util;
package com.loki.lochat.utils;
import org.bukkit.entity.Player;

View file

@ -1,4 +1,4 @@
package com.loki.lochat.util;
package com.loki.lochat.utils;
import org.bukkit.Bukkit;
import org.bukkit.entity.Entity;

View file

@ -1,6 +1,6 @@
package com.loki.lochat.utils;
import com.loki.lochat.util.FoliaUtil;
import com.loki.lochat.utils.FoliaUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;

View file

@ -0,0 +1,144 @@
package com.loki.lochat.utils.color;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Конвертер различных форматов цветов в MiniMessage
*/
public class ColorConverter {
private static final Pattern HEX_PATTERN_1 = Pattern.compile("&#([0-9a-fA-F]{6})");
private static final Pattern HEX_PATTERN_2 = Pattern.compile("(?<!<)#([0-9a-fA-F]{6})(?![^<]*>)");
// Мапа для быстрой конвертации цветов (вместо 32 вызовов replaceAll)
private static final Map<String, String> COLOR_MAP = Map.ofEntries(
// § коды
Map.entry("§0", "<black>"),
Map.entry("§1", "<dark_blue>"),
Map.entry("§2", "<dark_green>"),
Map.entry("§3", "<dark_aqua>"),
Map.entry("§4", "<dark_red>"),
Map.entry("§5", "<dark_purple>"),
Map.entry("§6", "<gold>"),
Map.entry("§7", "<gray>"),
Map.entry("§8", "<dark_gray>"),
Map.entry("§9", "<blue>"),
Map.entry("§a", "<green>"),
Map.entry("§b", "<aqua>"),
Map.entry("§c", "<red>"),
Map.entry("§d", "<light_purple>"),
Map.entry("§e", "<yellow>"),
Map.entry("§f", "<white>"),
Map.entry("§k", "<obfuscated>"),
Map.entry("§l", "<bold>"),
Map.entry("§m", "<strikethrough>"),
Map.entry("§n", "<underlined>"),
Map.entry("§o", "<italic>"),
Map.entry("§r", "<reset>"),
// & коды
Map.entry("&0", "<black>"),
Map.entry("&1", "<dark_blue>"),
Map.entry("&2", "<dark_green>"),
Map.entry("&3", "<dark_aqua>"),
Map.entry("&4", "<dark_red>"),
Map.entry("&5", "<dark_purple>"),
Map.entry("&6", "<gold>"),
Map.entry("&7", "<gray>"),
Map.entry("&8", "<dark_gray>"),
Map.entry("&9", "<blue>"),
Map.entry("&a", "<green>"),
Map.entry("&b", "<aqua>"),
Map.entry("&c", "<red>"),
Map.entry("&d", "<light_purple>"),
Map.entry("&e", "<yellow>"),
Map.entry("&f", "<white>"),
Map.entry("&k", "<obfuscated>"),
Map.entry("&l", "<bold>"),
Map.entry("&m", "<strikethrough>"),
Map.entry("&n", "<underlined>"),
Map.entry("&o", "<italic>"),
Map.entry("&r", "<reset>")
);
/**
* Конвертирует legacy форматы цветов в MiniMessage формат
*/
public static String convertLegacyFormats(String message) {
if (message == null) return "";
// Конвертируем &#RRGGBB в <color:#RRGGBB>
message = convertHexFormat1(message);
// Конвертируем #RRGGBB в <color:#RRGGBB>
message = convertHexFormat2(message);
// Конвертируем § и & коды через Map (быстрее чем 32 replaceAll)
message = convertLegacyCodes(message);
return message;
}
private static String convertHexFormat1(String message) {
Matcher matcher = HEX_PATTERN_1.matcher(message);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String hex = matcher.group(1).toLowerCase();
matcher.appendReplacement(sb, "<color:#" + hex + ">");
}
matcher.appendTail(sb);
return sb.toString();
}
private static String convertHexFormat2(String message) {
Matcher matcher = HEX_PATTERN_2.matcher(message);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String hex = matcher.group(1).toLowerCase();
matcher.appendReplacement(sb, "<color:#" + hex + ">");
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* Конвертирует legacy коды (§ и &) через Map - быстрее чем 32 replaceAll
*/
private static String convertLegacyCodes(String message) {
StringBuilder result = new StringBuilder(message.length());
int i = 0;
while (i < message.length()) {
if (i + 1 < message.length()) {
char current = message.charAt(i);
char next = message.charAt(i + 1);
// Проверяем § или &
if (current == '§' || current == '&') {
String code = String.valueOf(current) + next;
String replacement = COLOR_MAP.get(code);
if (replacement != null) {
result.append(replacement);
i += 2;
continue;
}
}
}
result.append(message.charAt(i));
i++;
}
return result.toString();
}
/**
* Экранирует текст для безопасного использования внутри MiniMessage тегов
*/
public static String escapeForMiniMessage(String text) {
if (text == null) return "";
return text.replace("\\", "\\\\").replace("<", "\\<");
}
}