feat: LoHub Action system + Join system implementation

This commit is contained in:
loki5512344 2026-02-23 17:27:34 +01:00
parent 69cae99476
commit 6a4c8a7886
55 changed files with 3180 additions and 1022 deletions

View file

@ -16,6 +16,10 @@ public class CustomCommand extends Command {
public CustomCommand(String name, CustomCommandManager manager) {
super(name);
this.manager = manager;
init(name);
}
private void init(String name) {
this.setDescription("Кастомная команда LoChat");
this.setUsage("/" + name);
}

View file

@ -14,9 +14,6 @@ import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
/**
* Команда /nick для установки кастомного ника
*/
public class NickCommand implements CommandExecutor, TabCompleter {
private final LoChat plugin;
private final NickService nickService;
@ -38,17 +35,14 @@ public class NickCommand implements CommandExecutor, TabCompleter {
return true;
}
// /nick - показать текущий ник
if (args.length == 0) {
return showCurrentNick(player);
}
// /nick reset - сбросить ник
if (args[0].equalsIgnoreCase("reset")) {
return resetNick(player);
}
// /nick <ник> - установить ник
String nickname = String.join(" ", args);
return setNick(player, nickname);
}
@ -87,27 +81,23 @@ public class NickCommand implements CommandExecutor, TabCompleter {
}
private boolean setNick(Player player, String nickname) {
// Валидация
if (!nickService.isValidNickname(nickname)) {
player.sendMessage(Component.text("Невалидный ник!", NamedTextColor.RED));
player.sendMessage(Component.text("Требования:", NamedTextColor.GRAY));
player.sendMessage(Component.text("• Длина: 3-16 символов", NamedTextColor.GRAY));
player.sendMessage(Component.text("• Разрешены: буквы, цифры, _, русские буквы", NamedTextColor.GRAY));
player.sendMessage(Component.text("• Поддержка цветов: <red>текст</red>, <gradient:#FF0000:#00FF00>текст</gradient>", NamedTextColor.GRAY));
player.sendMessage(Component.text("• Поддержка цветов: &#FF0000текст, <gradient:&#FF0000:&#00FF00>текст</gradient>", NamedTextColor.GRAY));
return true;
}
// Проверка уникальности
if (nickService.isNicknameTaken(nickname)) {
var currentNick = nickService.getNickname(player.getUniqueId());
// Если это не свой текущий ник
if (currentNick.isEmpty() || !currentNick.get().equals(nickname)) {
player.sendMessage(Component.text("Этот ник уже занят!", NamedTextColor.RED));
return true;
}
}
// Устанавливаем ник
boolean success = nickService.setNickname(player.getUniqueId(), nickname);
if (success) {
@ -126,8 +116,22 @@ public class NickCommand implements CommandExecutor, TabCompleter {
List<String> completions = new ArrayList<>();
if (args.length == 1) {
completions.add("reset");
completions.add("<ник>");
String input = args[0].toLowerCase();
if ("reset".startsWith(input)) {
completions.add("reset");
}
if (sender instanceof Player player) {
var currentNick = nickService.getNickname(player.getUniqueId());
if (currentNick.isPresent()) {
completions.add(currentNick.get());
}
}
completions.add("&#FF0000Красный");
completions.add("&#00FF00Зелёный");
completions.add("Русский_Ник");
}
return completions;

View file

@ -0,0 +1,164 @@
package com.loki.lochat.core.filter;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AdvancedMessageFilter {
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 final FileConfiguration config;
private final List<String> whitelistedDomains;
private final List<String> blacklistedDomains;
public AdvancedMessageFilter(FileConfiguration config) {
this.config = config;
this.whitelistedDomains = config.getStringList("filters.url.whitelist");
this.blacklistedDomains = config.getStringList("filters.url.blacklist");
}
public FilterResult filterMessage(Player player, String message) {
if (config.getBoolean("filters.url.enabled", true)) {
FilterResult urlResult = filterUrls(player, message);
if (!urlResult.isAllowed()) {
return urlResult;
}
message = urlResult.getFilteredMessage();
}
if (config.getBoolean("filters.ip.enabled", true)) {
FilterResult ipResult = filterIPs(player, message);
if (!ipResult.isAllowed()) {
return ipResult;
}
message = ipResult.getFilteredMessage();
}
if (config.getBoolean("filters.repeat.enabled", true)) {
message = filterRepeatingChars(message);
}
return new FilterResult(true, message, null);
}
private FilterResult filterUrls(Player player, String message) {
if (player.hasPermission("lochat.bypass.urlfilter")) {
return new FilterResult(true, message, null);
}
Matcher matcher = URL_PATTERN.matcher(message);
while (matcher.find()) {
String url = matcher.group();
String domain = extractDomain(url);
if (!whitelistedDomains.isEmpty()) {
boolean isWhitelisted = whitelistedDomains.stream()
.anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase()));
if (!isWhitelisted) {
return new FilterResult(false, message,
config.getString("filters.url.blocked-message", "&#FF6B6BСсылки запрещены!"));
}
}
if (!blacklistedDomains.isEmpty()) {
boolean isBlacklisted = blacklistedDomains.stream()
.anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase()));
if (isBlacklisted) {
return new FilterResult(false, message,
config.getString("filters.url.blocked-message", "&#FF6B6BЭта ссылка запрещена!"));
}
}
}
return new FilterResult(true, message, null);
}
private FilterResult filterIPs(Player player, String message) {
if (player.hasPermission("lochat.bypass.ipfilter")) {
return new FilterResult(true, message, null);
}
Matcher matcher = IP_PATTERN.matcher(message);
if (matcher.find()) {
String replacement = config.getString("filters.ip.replacement", "[IP скрыт]");
String filtered = matcher.replaceAll(replacement);
if (config.getBoolean("filters.ip.block", false)) {
return new FilterResult(false, message,
config.getString("filters.ip.blocked-message", "&#FF6B6BIP адреса запрещены!"));
}
return new FilterResult(true, filtered, null);
}
return new FilterResult(true, message, null);
}
private String filterRepeatingChars(String message) {
int maxRepeats = config.getInt("filters.repeat.max-repeats", 3);
Matcher matcher = REPEAT_PATTERN.matcher(message);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String repeated = matcher.group(1);
String replacement = repeated.repeat(Math.min(maxRepeats, matcher.group().length()));
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(result);
return result.toString();
}
private String extractDomain(String url) {
url = url.replaceFirst("^https?://", "");
int slashIndex = url.indexOf('/');
if (slashIndex != -1) {
url = url.substring(0, slashIndex);
}
return url;
}
public static class FilterResult {
private final boolean allowed;
private final String filteredMessage;
private final String blockReason;
public FilterResult(boolean allowed, String filteredMessage, String blockReason) {
this.allowed = allowed;
this.filteredMessage = filteredMessage;
this.blockReason = blockReason;
}
public boolean isAllowed() {
return allowed;
}
public String getFilteredMessage() {
return filteredMessage;
}
public String getBlockReason() {
return blockReason;
}
}
}

View file

@ -9,25 +9,29 @@ import org.bukkit.plugin.java.JavaPlugin;
import java.util.HashMap;
import java.util.Map;
/**
* Реестр сервисов - Dependency Injection контейнер (SOLID DIP)
*/
public class ServiceRegistry {
private final Map<Class<?>, Object> services = new HashMap<>();
private final JavaPlugin plugin;
public ServiceRegistry(JavaPlugin plugin) {
this.plugin = plugin;
registerServices();
init();
}
public ServiceRegistry(JavaPlugin plugin, ConfigManager configManager, MessageConfig messageConfig) {
this.plugin = plugin;
initWithDeps(configManager, messageConfig);
}
private void init() {
registerServices();
}
private void initWithDeps(ConfigManager configManager, MessageConfig messageConfig) {
registerServicesWithDeps(configManager, messageConfig);
}
private void registerServices() {
// Регистрируем все сервисы (базовая версия)
register(ChatService.class, new ChatServiceImpl(plugin, this));
register(MuteService.class, new MuteServiceImpl(plugin));
register(CooldownService.class, new CooldownServiceImpl());
@ -36,14 +40,12 @@ public class ServiceRegistry {
}
private void registerServicesWithDeps(ConfigManager configManager, MessageConfig messageConfig) {
// Регистрируем все сервисы с зависимостями
register(ChatService.class, new ChatServiceImpl(plugin, this));
register(MuteService.class, new MuteServiceImpl(plugin));
register(CooldownService.class, new CooldownServiceImpl());
register(MessageService.class, new MessageServiceImpl(plugin, this));
register(PlayerDataService.class, new PlayerDataServiceImpl(plugin));
// Новые сервисы
register(PMService.class, new PMServiceImpl());
register(IgnoreService.class, new IgnoreServiceImpl(plugin));
register(SpyService.class, new SpyServiceImpl(plugin, messageConfig));

View file

@ -1,8 +1,10 @@
package com.loki.lochat.listener;
import com.loki.lochat.api.service.MessageService;
import com.loki.lochat.core.filter.AdvancedMessageFilter;
import com.loki.lochat.core.registry.ServiceRegistry;
import com.loki.lochat.util.FoliaUtil;
import com.loki.lochat.utils.MentionHandler;
import com.loki.lochat.utils.TextFormatter;
import io.papermc.paper.event.player.AsyncChatEvent;
import io.papermc.paper.chat.ChatRenderer;
import net.kyori.adventure.audience.Audience;
@ -12,24 +14,23 @@ import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin;
/**
* Слушатель событий чата с Paper Chat API
* Поддерживает hover tooltips, click actions, mentions
*/
public class ChatEventListener implements Listener {
private final JavaPlugin plugin;
private final MessageService messageService;
private final AdvancedMessageFilter advancedFilter;
private final MentionHandler mentionHandler;
public ChatEventListener(JavaPlugin plugin, ServiceRegistry registry) {
this.plugin = plugin;
this.messageService = registry.get(MessageService.class);
this.advancedFilter = new AdvancedMessageFilter(plugin.getConfig());
this.mentionHandler = new MentionHandler(plugin);
}
@EventHandler(priority = EventPriority.HIGHEST)
@ -37,7 +38,6 @@ public class ChatEventListener implements Listener {
Player sender = event.getPlayer();
String message = PlainTextComponentSerializer.plainText().serialize(event.message());
// Проверяем фильтры (мут, кулдаун)
boolean allowed = messageService.processMessage(sender, message);
if (!allowed) {
@ -45,13 +45,20 @@ public class ChatEventListener implements Listener {
return;
}
// Устанавливаем кастомный рендерер
AdvancedMessageFilter.FilterResult filterResult = advancedFilter.filterMessage(sender, message);
if (!filterResult.isAllowed()) {
event.setCancelled(true);
sender.sendMessage(com.loki.lochat.utils.ChatFormatter.parse(filterResult.getBlockReason()));
return;
}
String filteredMessage = filterResult.getFilteredMessage();
event.message(Component.text(filteredMessage));
event.renderer(new EnhancedChatRenderer(sender, event.message()));
}
/**
* Продвинутый рендерер чата с hover/click/mentions
*/
private class EnhancedChatRenderer implements ChatRenderer {
private final Player sender;
private final Component originalMessage;
@ -63,47 +70,31 @@ public class ChatEventListener implements Listener {
@Override
public Component render(Player source, Component sourceDisplayName, Component message, Audience viewer) {
// Получаем конфиг формата
String format = plugin.getConfig().getString("chat.global.format", "{prefix} {player}: {message}");
String prefix = plugin.getConfig().getString("chat.global.prefix", "[G]");
// Создаём компонент с hover и click
Component playerComponent = createPlayerComponent(source, sourceDisplayName);
// Обрабатываем сообщение
Component processedMessage = message;
// 1. Обрабатываем URL (делаем кликабельными)
processedMessage = TextFormatter.formatMarkdown(processedMessage);
processedMessage = processUrls(processedMessage);
// 2. Обрабатываем эмодзи шорткоды (:smile: -> 😊)
processedMessage = processEmojis(processedMessage);
// 3. Обрабатываем упоминания если viewer это Player
if (viewer instanceof Player viewerPlayer) {
processedMessage = processMentions(processedMessage, viewerPlayer);
processedMessage = mentionHandler.processMentions(processedMessage, source, viewerPlayer);
}
// Собираем финальное сообщение
return Component.text(prefix + " ")
.append(playerComponent)
.append(Component.text(": "))
.append(processedMessage);
}
/**
* Создаёт компонент имени игрока с hover и click
*/
private Component createPlayerComponent(Player player, Component displayName) {
// Проверяем включен ли hover
if (!plugin.getConfig().getBoolean("chat.hover.enabled", true)) {
return displayName.clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " "));
}
// Получаем формат из конфига
java.util.List<String> hoverLines = plugin.getConfig().getStringList("chat.hover.format");
if (hoverLines.isEmpty()) {
// Дефолтный формат если конфиг пустой
hoverLines = java.util.Arrays.asList(
"<gray>Игрок: <white>{player}",
"<gray>Мир: <green>{world}",
@ -112,12 +103,10 @@ public class ChatEventListener implements Listener {
);
}
// Строим hover text
net.kyori.adventure.text.TextComponent.Builder hoverBuilder = Component.text();
for (int i = 0; i < hoverLines.size(); i++) {
String line = hoverLines.get(i);
// Заменяем плейсхолдеры
line = line.replace("{player}", player.getName())
.replace("{world}", player.getWorld().getName())
.replace("{ping}", String.valueOf(player.getPing()))
@ -125,62 +114,28 @@ public class ChatEventListener implements Listener {
.replace("{health}", String.valueOf(Math.round(player.getHealth())))
.replace("{food}", String.valueOf(player.getFoodLevel()));
// Парсим MiniMessage
Component lineComponent = com.loki.lochat.utils.ChatFormatter.parse(line);
hoverBuilder.append(lineComponent);
// Добавляем перенос строки если не последняя
if (i < hoverLines.size() - 1) {
hoverBuilder.append(Component.newline());
}
}
// Click: открываем /msg
return displayName
.hoverEvent(HoverEvent.showText(hoverBuilder.build()))
.clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " "));
}
/**
* Обрабатывает упоминания (@player) в сообщении
*/
private Component processMentions(Component message, Player viewer) {
String plainText = PlainTextComponentSerializer.plainText().serialize(message);
// Проверяем упоминание текущего игрока
if (plainText.toLowerCase().contains("@" + viewer.getName().toLowerCase())) {
// Подсвечиваем упоминание
Component highlighted = message.replaceText(builder ->
builder.matchLiteral("@" + viewer.getName())
.replacement(Component.text("@" + viewer.getName(), NamedTextColor.YELLOW, TextDecoration.BOLD))
);
// Воспроизводим звук уведомления
FoliaUtil.runEntityTask(plugin, viewer, () -> {
viewer.playSound(viewer.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.5f);
});
return highlighted;
}
return message;
}
/**
* Обрабатывает URL в сообщении (делает кликабельными)
*/
private Component processUrls(Component message) {
String plainText = PlainTextComponentSerializer.plainText().serialize(message);
// Простой regex для URL
String urlPattern = "(https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)";
String urlPattern = "(https?://[\\w\\-._~:/?#\\[\\]@!35538117-c5a8-4bfe-b9f2-b0dcf9374222'()*+,;=%]+)";
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(urlPattern);
java.util.regex.Matcher matcher = pattern.matcher(plainText);
Component result = message;
while (matcher.find()) {
String url = matcher.group(1);
// Делаем URL кликабельным с hover
Component urlComponent = Component.text(url, NamedTextColor.AQUA, TextDecoration.UNDERLINED)
.hoverEvent(HoverEvent.showText(Component.text("Открыть: " + url, NamedTextColor.GRAY)))
.clickEvent(ClickEvent.openUrl(url));
@ -193,35 +148,28 @@ public class ChatEventListener implements Listener {
return result;
}
/**
* Обрабатывает эмодзи шорткоды (:smile: -> ☺)
* Использует только MC-совместимые Unicode символы
*/
private Component processEmojis(Component message) {
Component result = message;
// MC-совместимые Unicode эмодзи
result = result.replaceText(builder -> builder.matchLiteral(":smile:").replacement("☺"));
result = result.replaceText(builder -> builder.matchLiteral(":heart:").replacement("♥"));
result = result.replaceText(builder -> builder.matchLiteral(":fire:").replacement("※"));
result = result.replaceText(builder -> builder.matchLiteral(":heart:").replacement("❤"));
result = result.replaceText(builder -> builder.matchLiteral(":star:").replacement("★"));
result = result.replaceText(builder -> builder.matchLiteral(":check:").replacement("✓"));
result = result.replaceText(builder -> builder.matchLiteral(":cross:").replacement("✗"));
result = result.replaceText(builder -> builder.matchLiteral(":skull:").replacement("☠"));
result = result.replaceText(builder -> builder.matchLiteral(":check:").replacement("✔"));
result = result.replaceText(builder -> builder.matchLiteral(":cross:").replacement("✖"));
result = result.replaceText(builder -> builder.matchLiteral(":arrow:").replacement("→"));
result = result.replaceText(builder -> builder.matchLiteral(":skull:").replacement("☠"));
result = result.replaceText(builder -> builder.matchLiteral(":note:").replacement("♪"));
result = result.replaceText(builder -> builder.matchLiteral(":sun:").replacement("☀"));
result = result.replaceText(builder -> builder.matchLiteral(":moon:").replacement("☾"));
result = result.replaceText(builder -> builder.matchLiteral(":peace:").replacement("☮"));
result = result.replaceText(builder -> builder.matchLiteral(":yin:").replacement("☯"));
result = result.replaceText(builder -> builder.matchLiteral(":flower:").replacement("✿"));
result = result.replaceText(builder -> builder.matchLiteral(":moon:").replacement("☽"));
result = result.replaceText(builder -> builder.matchLiteral(":snowflake:").replacement("❄"));
result = result.replaceText(builder -> builder.matchLiteral(":lightning:").replacement("⚡"));
result = result.replaceText(builder -> builder.matchLiteral(":crown:").replacement("♔"));
result = result.replaceText(builder -> builder.matchLiteral(":sword:").replacement("⚔"));
result = result.replaceText(builder -> builder.matchLiteral(":shield:").replacement("⛨"));
result = result.replaceText(builder -> builder.matchLiteral(":fire:").replacement("🔥"));
result = result.replaceText(builder -> builder.matchLiteral(":diamond:").replacement("◆"));
result = result.replaceText(builder -> builder.matchLiteral(":sword:").replacement("⚔"));
result = result.replaceText(builder -> builder.matchLiteral(":pickaxe:").replacement("⛏"));
result = result.replaceText(builder -> builder.matchLiteral(":bow:").replacement("🏹"));
result = result.replaceText(builder -> builder.matchLiteral(":shield:").replacement("🛡"));
result = result.replaceText(builder -> builder.matchLiteral(":potion:").replacement("🧪"));
result = result.replaceText(builder -> builder.matchLiteral(":book:").replacement("📖"));
result = result.replaceText(builder -> builder.matchLiteral(":crown:").replacement("👑"));
result = result.replaceText(builder -> builder.matchLiteral(":gem:").replacement("💎"));
return result;
}
}

View file

@ -27,6 +27,10 @@ public class CustomCommandManager {
public CustomCommandManager(LoChat plugin) {
this.plugin = plugin;
this.commands = new HashMap<>();
init();
}
private void init() {
loadCommands();
}

View file

@ -0,0 +1,97 @@
package com.loki.lochat.managers;
import com.loki.lochat.LoChat;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
public class EmojiManager {
private final LoChat plugin;
private final Map<String, String> emojis = new HashMap<>();
private boolean enabled;
private boolean requirePermission;
public EmojiManager(LoChat plugin) {
this.plugin = plugin;
load();
}
public void load() {
File file = new File(plugin.getDataFolder(), "emojis.yml");
if (!file.exists()) {
plugin.saveResource("emojis.yml", false);
}
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
emojis.clear();
// Настройки
enabled = config.getBoolean("settings.enabled", true);
requirePermission = config.getBoolean("settings.require-permission", false);
// Загружаем все категории
for (String category : config.getKeys(false)) {
if (category.equals("settings")) continue;
ConfigurationSection section = config.getConfigurationSection(category);
if (section != null) {
for (String key : section.getKeys(false)) {
String value = section.getString(key);
if (value != null) {
emojis.put(key, value);
}
}
}
}
plugin.getLogger().info("Загружено " + emojis.size() + " смайликов");
}
public void reload() {
load();
}
/**
* Заменяет смайлики в сообщении
*/
public String process(String message, Player player) {
if (!enabled) return message;
// Проверка права
if (requirePermission && player != null && !player.hasPermission("chat.emoji.use")) {
return message;
}
String result = message;
for (Map.Entry<String, String> entry : emojis.entrySet()) {
result = result.replace(entry.getKey(), entry.getValue());
}
return result;
}
public String process(String message) {
if (!enabled) return message;
String result = message;
for (Map.Entry<String, String> entry : emojis.entrySet()) {
result = result.replace(entry.getKey(), entry.getValue());
}
return result;
}
public Map<String, String> getEmojis() {
return emojis;
}
public boolean isEnabled() {
return enabled;
}
}

View file

@ -0,0 +1,106 @@
package com.loki.lochat.utils;
import com.loki.lochat.util.FoliaUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MentionHandler {
private static final Pattern EVERYONE_PATTERN = Pattern.compile("@everyone\\b");
private static final Pattern HERE_PATTERN = Pattern.compile("@here\\b");
private static final Pattern ROLE_PATTERN = Pattern.compile("@role:([\\w.]+)");
private static final Pattern PLAYER_PATTERN = Pattern.compile("@(\\w+)");
private final JavaPlugin plugin;
public MentionHandler(JavaPlugin plugin) {
this.plugin = plugin;
}
public Component processMentions(Component message, Player sender, Player viewer) {
String plainText = PlainTextComponentSerializer.plainText().serialize(message);
List<Player> mentionedPlayers = new ArrayList<>();
if (sender.hasPermission("lochat.mention.everyone")) {
Matcher everyoneMatcher = EVERYONE_PATTERN.matcher(plainText);
if (everyoneMatcher.find()) {
mentionedPlayers.addAll(Bukkit.getOnlinePlayers());
message = highlightMention(message, "@everyone");
}
}
if (sender.hasPermission("lochat.mention.here")) {
Matcher hereMatcher = HERE_PATTERN.matcher(plainText);
if (hereMatcher.find()) {
mentionedPlayers.addAll(Bukkit.getOnlinePlayers());
message = highlightMention(message, "@here");
}
}
if (sender.hasPermission("lochat.mention.role")) {
Matcher roleMatcher = ROLE_PATTERN.matcher(plainText);
while (roleMatcher.find()) {
String permission = roleMatcher.group(1);
String fullMention = "@role:" + permission;
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.hasPermission(permission)) {
mentionedPlayers.add(player);
}
}
message = highlightMention(message, fullMention);
}
}
Matcher playerMatcher = PLAYER_PATTERN.matcher(plainText);
while (playerMatcher.find()) {
String playerName = playerMatcher.group(1);
Player mentioned = Bukkit.getPlayerExact(playerName);
if (mentioned != null && mentioned.isOnline()) {
mentionedPlayers.add(mentioned);
message = highlightMention(message, "@" + playerName);
}
}
if (mentionedPlayers.contains(viewer)) {
playMentionSound(viewer);
message = highlightForViewer(message, viewer);
}
return message;
}
private Component highlightMention(Component message, String mention) {
return message.replaceText(builder ->
builder.matchLiteral(mention)
.replacement(Component.text(mention, NamedTextColor.YELLOW, TextDecoration.BOLD))
);
}
private Component highlightForViewer(Component message, Player viewer) {
String viewerMention = "@" + viewer.getName();
return message.replaceText(builder ->
builder.matchLiteral(viewerMention)
.replacement(Component.text(viewerMention, NamedTextColor.GOLD, TextDecoration.BOLD))
);
}
private void playMentionSound(Player player) {
FoliaUtil.runEntityTask(plugin, player, () -> {
player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.5f);
});
}
}

View file

@ -0,0 +1,95 @@
package com.loki.lochat.utils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TextFormatter {
private static final Pattern BOLD_PATTERN = Pattern.compile("\\*\\*(.+?)\\*\\*");
private static final Pattern ITALIC_PATTERN = Pattern.compile("\\*(.+?)\\*");
private static final Pattern UNDERLINE_PATTERN = Pattern.compile("__(.+?)__");
private static final Pattern STRIKETHROUGH_PATTERN = Pattern.compile("~~(.+?)~~");
private static final Pattern CODE_PATTERN = Pattern.compile("`(.+?)`");
public static Component formatMarkdown(Component message) {
String text = PlainTextComponentSerializer.plainText().serialize(message);
text = formatBold(text);
text = formatItalic(text);
text = formatUnderline(text);
text = formatStrikethrough(text);
text = formatCode(text);
return ChatFormatter.parse(text);
}
private static String formatBold(String text) {
Matcher matcher = BOLD_PATTERN.matcher(text);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String content = matcher.group(1);
matcher.appendReplacement(result, "<bold>" + content + "</bold>");
}
matcher.appendTail(result);
return result.toString();
}
private static String formatItalic(String text) {
Matcher matcher = ITALIC_PATTERN.matcher(text);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String content = matcher.group(1);
if (!content.startsWith("*") && !content.endsWith("*")) {
matcher.appendReplacement(result, "<italic>" + content + "</italic>");
}
}
matcher.appendTail(result);
return result.toString();
}
private static String formatUnderline(String text) {
Matcher matcher = UNDERLINE_PATTERN.matcher(text);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String content = matcher.group(1);
matcher.appendReplacement(result, "<underlined>" + content + "</underlined>");
}
matcher.appendTail(result);
return result.toString();
}
private static String formatStrikethrough(String text) {
Matcher matcher = STRIKETHROUGH_PATTERN.matcher(text);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String content = matcher.group(1);
matcher.appendReplacement(result, "<strikethrough>" + content + "</strikethrough>");
}
matcher.appendTail(result);
return result.toString();
}
private static String formatCode(String text) {
Matcher matcher = CODE_PATTERN.matcher(text);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String content = matcher.group(1);
matcher.appendReplacement(result, "&#888888`" + content + "`");
}
matcher.appendTail(result);
return result.toString();
}
}

View file

@ -10,7 +10,7 @@
settings:
# Префикс автосообщений
prefix: "<gradient:#00D4FF:#0099CC>[INFO]</gradient>"
prefix: "&#00D4FF[&#00D4FFI&#00CAF7N&#00C0EEF&#00B7E6O&#0099CC]"
# Режим отправки: sequential (по порядку), random (случайно), custom (свой порядок)
mode: "sequential"

View file

@ -12,7 +12,7 @@ config-version: 3
# └─────────────────────────────────────────────────────────────────────────────┘
# Префикс плагина в сообщениях
prefix: "<gradient:#00D4FF:#0099CC>[LoChat]</gradient>"
prefix: "&#00D4FF[&#00D4FFL&#00CAF7o&#00C0EEC&#00B7E6h&#00ADDDa&#00A3D5t&#0099CC]"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ НАСТРОЙКИ ЧАТА │
@ -30,9 +30,8 @@ chat:
# Кулдаун между сообщениями (секунды)
cooldown: 3
# Префикс глобального чата
prefix: "<gradient:#FFD700:#FFA500>[G]</gradient>"
# Формат сообщения: {prefix} {player} {message}
format: "{prefix} <gradient:#00FF88:#00CC66>{player}</gradient><dark_gray>:</dark_gray> {message}"
prefix: "&#FFD700[G]"
format: "{prefix} &#00FF88{player}&#555555: {message}"
# ═══════════════════════════════════════════════════════════════════════════
# ЛОКАЛЬНЫЙ ЧАТ
@ -45,9 +44,8 @@ chat:
# Кулдаун между сообщениями (секунды)
cooldown: 1
# Префикс локального чата
prefix: "<gradient:#87CEEB:#4682B4>[L]</gradient>"
# Формат сообщения
format: "{prefix} <gradient:#00FF88:#00CC66>{player}</gradient><dark_gray>:</dark_gray> {message}"
prefix: "&#87CEEB[L]"
format: "{prefix} &#00FF88{player}&#555555: {message}"
# ═══════════════════════════════════════════════════════════════════════════
# ЛИЧНЫЕ СООБЩЕНИЯ
@ -111,9 +109,8 @@ mentions:
# Тип звука
sound-type: "BLOCK_NOTE_BLOCK_PLING"
# Подсветка упоминания в чате
highlight: "<gradient:#FFFF00:#FFD700>@{player}</gradient>"
# Подсветка собственного ника
self-highlight: "<gradient:#FFD700:#FFA500>{player}</gradient>"
highlight: "&#FFFF00@{player}"
self-highlight: "&#FFD700{player}"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ АВТОСООБЩЕНИЯ │
# └─────────────────────────────────────────────────────────────────────────────┘
@ -134,7 +131,7 @@ announcements:
# Показывать title при объявлении
show-title: true
# Заголовок title
title-header: "<gradient:#FF6B6B:#FF4757>ОБЪЯВЛЕНИЕ</gradient>"
title-header: "&#FF6B6BОБЪЯВЛЕНИЕ"
# Показывать actionbar при объявлении
show-actionbar: true
# Время показа title (в секундах)
@ -142,7 +139,7 @@ announcements:
# Формат объявления
formats:
announcement: "<gradient:#FF6B6B:#FF4757>[ОБЪЯВЛЕНИЕ]</gradient> <message>"
announcement: "&#FF6B6B[ОБЪЯВЛЕНИЕ] <message>"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОЧИСТКА ЧАТА │
@ -153,7 +150,7 @@ clearchat:
# Показывать сообщение после очистки чата
enabled: true
# Текст сообщения ({player} - имя администратора)
text: "<gradient:#FF6B6B:#FF4757>Чат очищен администратором {player}</gradient>"
text: "&#FF6B6BЧат очищен администратором {player}"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ГОЛОВЫ ИГРОКОВ │
@ -179,20 +176,14 @@ mute:
# %operator% - модератор
messages:
# Сообщение при муте (broadcast)
muted: "<gradient:#FF6B6B:#FF4757>%player%</gradient> <gray>был замучен на</gray> <gradient:#FF6B6B:#FF4757>%duration%</gradient> <gray>модератором</gray> <gradient:#FF6B6B:#FF4757>%operator%</gradient><gray>. Причина:</gray> <gradient:#FF6B6B:#FF4757>%reason%</gradient>"
# Сообщение при размуте (broadcast)
unmuted: "<gradient:#6BCB77:#4ECDC4>%player%</gradient> <gray>был размучен модератором</gray> <gradient:#6BCB77:#4ECDC4>%operator%</gradient>"
# Сообщение замученному игроку
you-muted: "<gradient:#FF6B6B:#FF4757>Вы замучены на %duration%! Причина: %reason%</gradient>"
# Сообщение при перманентном муте
you-muted-permanent: "<gradient:#FF6B6B:#FF4757>Вы замучены навсегда! Причина: %reason%</gradient>"
# Сообщение при попытке написать в чат
cannot-chat: "<gradient:#FF6B6B:#FF4757>Вы замучены! Осталось: %duration%</gradient>"
cannot-chat-permanent: "<gradient:#FF6B6B:#FF4757>Вы замучены навсегда!</gradient>"
# Тихий мут (только для тех кто видит)
silent-muted: "<gray>[Тихо]</gray> <gradient:#FF6B6B:#FF4757>%player%</gradient> <gray>замучен на</gray> <gradient:#FF6B6B:#FF4757>%duration%</gradient> <gray>(%operator%). Причина: %reason%</gray>"
silent-unmuted: "<gray>[Тихо]</gray> <gradient:#6BCB77:#4ECDC4>%player%</gradient> <gray>размучен (%operator%)</gray>"
muted: "&#FF6B6B%player% &#888888был замучен на &#FF6B6B%duration% &#888888модератором &#FF6B6B%operator%&#888888. Причина: &#FF6B6B%reason%"
unmuted: "&#6BCB77%player% &#888888был размучен модератором &#6BCB77%operator%"
you-muted: "&#FF6B6BВы замучены на %duration%! Причина: %reason%"
you-muted-permanent: "&#FF6B6BВы замучены навсегда! Причина: %reason%"
cannot-chat: "&#FF6B6BВы замучены! Осталось: %duration%"
cannot-chat-permanent: "&#FF6B6BВы замучены навсегда!"
silent-muted: "&#888888[Тихо] &#FF6B6B%player% &#888888замучен на &#FF6B6B%duration% &#888888(%operator%). Причина: %reason%"
silent-unmuted: "&#888888[Тихо] &#6BCB77%player% &#888888размучен (%operator%)"
# Причина по умолчанию если не указана
default-reason: "Нарушение правил"
@ -212,4 +203,46 @@ mute:
- "7d"
- "14d"
- "30d"
- "perm"
- "perm"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ФИЛЬТРЫ СООБЩЕНИЙ │
# └─────────────────────────────────────────────────────────────────────────────┘
filters:
url:
enabled: true
whitelist:
- "youtube.com"
- "youtu.be"
- "discord.gg"
- "twitch.tv"
blacklist:
- "scam-site.com"
blocked-message: "&#FF6B6BС&#FF5F5Fс&#FF5353ы&#FF4747л&#FF3B3Bк&#FF2F2Fи &#FF2323з&#FF1717а&#FF0B0Bп&#FF0000р&#FF0000е&#FF0000щ&#FF0000е&#FF0000н&#FF0000ы!"
ip:
enabled: true
block: false
replacement: "[IP скрыт]"
blocked-message: "&#FF6B6BI&#FF5F5FP &#FF5353а&#FF4747д&#FF3B3Bр&#FF2F2Fе&#FF2323с&#FF1717а &#FF0B0Bз&#FF0000а&#FF0000п&#FF0000р&#FF0000е&#FF0000щ&#FF0000е&#FF0000н&#FF0000ы!"
repeat:
enabled: true
max-repeats: 3
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ УПОМИНАНИЯ │
# └─────────────────────────────────────────────────────────────────────────────┘
advanced-mentions:
enabled: true
everyone:
enabled: true
permission: "lochat.mention.everyone"
here:
enabled: true
permission: "lochat.mention.here"
role:
enabled: true
permission: "lochat.mention.role"

View file

@ -10,7 +10,7 @@
# └─────────────────────────────────────────────────────────────────────────────┘
# Префикс градиентного модуля
prefix: "<gradient:#9370DB:#8A2BE2>[Gradient]</gradient>"
prefix: "&#9370DB[&#9370DBG&#8E6CD9r&#896AD7a&#8467D5d&#7F65D3i&#7A62D1e&#7560CFn&#705DCDt&#6B5BCB]"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ КОМАНДА /color │

View file

@ -9,23 +9,23 @@
# └─────────────────────────────────────────────────────────────────────────────┘
# Префикс плагина
prefix: "<gradient:#00D4FF:#0099CC>[LoChat]</gradient>"
prefix: "&#00D4FF[&#00D4FFL&#00CAF7o&#00C0EEC&#00B7E6h&#00ADDDa&#00A3D5t&#0099CC]"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ГЛОБАЛЬНЫЙ ЧАТ │
# └─────────────────────────────────────────────────────────────────────────────┘
global:
disabled: "<gradient:#FF6B6B:#FF4757>Глобальный чат отключен</gradient>"
toggled-on: "<gradient:#6BCB77:#4ECDC4>Глобальный чат включен</gradient>"
toggled-off: "<gradient:#FF6B6B:#FF4757>Глобальный чат отключен</gradient>"
disabled: "&#FF6B6BГ&#FF5F5Fл&#FF5353о&#FF4747б&#FF3B3Bа&#FF2F2Fл&#FF2323ь&#FF1717н&#FF0B0Bы&#FF0000й &#FF0000ч&#FF0000а&#FF0000т &#FF0000о&#FF0000т&#FF0000к&#FF0000л&#FF0000ю&#FF0000ч&#FF0000е&#FF0000н"
toggled-on: "&#6BCB77Г&#62C973л&#5AC76Fо&#51C56Bб&#49C367а&#40C163л&#38BF5Fь&#2FBD5Bн&#27BB57ы&#1EB953й &#16B74F ч&#0DB54Bа&#05B347т &#05B347в&#05B347к&#05B347л&#05B347ю&#05B347ч&#05B347е&#05B347н"
toggled-off: "&#FF6B6BГ&#FF5F5Fл&#FF5353о&#FF4747б&#FF3B3Bа&#FF2F2Fл&#FF2323ь&#FF1717н&#FF0B0Bы&#FF0000й &#FF0000ч&#FF0000а&#FF0000т &#FF0000о&#FF0000т&#FF0000к&#FF0000л&#FF0000ю&#FF0000ч&#FF0000е&#FF0000н"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ЛОКАЛЬНЫЙ ЧАТ │
# └─────────────────────────────────────────────────────────────────────────────┘
local:
no-players: "<gradient:#FFA726:#FF9800>Рядом нет игроков</gradient>"
no-players: "&#FFA726Р&#FFA322я&#FF9F1Eд&#FF9B1Aо&#FF9716м &#FF9312н&#FF8F0Eе&#FF8B0Aт &#FF8706и&#FF8302г&#FF8000р&#FF8000о&#FF8000к&#FF8000о&#FF8000в"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ЛИЧНЫЕ СООБЩЕНИЯ │
@ -47,36 +47,36 @@ pm:
received: "<gray>[PM]</gray> <white>{player}</white> <gray>→</gray> <gradient:#87CEEB:#4682B4>Ты</gradient><gray>:</gray> {message}"
# Ошибки PM
no-reply: "<gradient:#FF6B6B:#FF4757>Некому ответить</gradient>"
ignored: "<gradient:#FF6B6B:#FF4757>Этот игрок вас игнорирует</gradient>"
player-offline: "<gradient:#FF6B6B:#FF4757>Игрок не в сети</gradient>"
self: "<gradient:#FF6B6B:#FF4757>Нельзя писать самому себе</gradient>"
no-reply: "&#FF6B6BН&#FF5F5Fе&#FF5353к&#FF4747о&#FF3B3Bм&#FF2F2Fу &#FF2323о&#FF1717т&#FF0B0Bв&#FF0000е&#FF0000т&#FF0000и&#FF0000т&#FF0000ь"
ignored: "&#FF6B6BЭ&#FF5F5Fт&#FF5353о&#FF4747т &#FF3B3Bи&#FF2F2Fг&#FF2323р&#FF1717о&#FF0B0Bк &#FF0000в&#FF0000а&#FF0000с &#FF0000и&#FF0000г&#FF0000н&#FF0000о&#FF0000р&#FF0000и&#FF0000р&#FF0000у&#FF0000е&#FF0000т"
player-offline: "&#FF6B6BИ&#FF5F5Fг&#FF5353р&#FF4747о&#FF3B3Bк &#FF2F2Fн&#FF2323е &#FF1717в &#FF0B0Bс&#FF0000е&#FF0000т&#FF0000и"
self: "&#FF6B6BН&#FF5F5Fе&#FF5353л&#FF4747ь&#FF3B3Bз&#FF2F2Fя &#FF2323п&#FF1717и&#FF0B0Bс&#FF0000а&#FF0000т&#FF0000ь &#FF0000с&#FF0000а&#FF0000м&#FF0000о&#FF0000м&#FF0000у &#FF0000с&#FF0000е&#FF0000б&#FF0000е"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ИГНОРИРОВАНИЕ │
# └─────────────────────────────────────────────────────────────────────────────┘
ignore:
added: "<gradient:#6BCB77:#4ECDC4>Игрок {player} добавлен в игнор</gradient>"
removed: "<gradient:#6BCB77:#4ECDC4>Игрок {player} убран из игнора</gradient>"
already-ignored: "<gradient:#FFA726:#FF9800>Игрок уже в игноре</gradient>"
not-ignored: "<gradient:#FFA726:#FF9800>Игрок не в игноре</gradient>"
self: "<gradient:#FF6B6B:#FF4757>Нельзя игнорировать себя</gradient>"
added: "&#6BCB77И&#62C973г&#5AC76Fр&#51C56Bо&#49C367к &#40C163{&#38BF5Fp&#2FBD5Bl&#27BB57a&#1EB953y&#16B74Fe&#0DB54Br&#05B347} &#05B347д&#05B347о&#05B347б&#05B347а&#05B347в&#05B347л&#05B347е&#05B347н &#05B347в &#05B347и&#05B347г&#05B347н&#05B347о&#05B347р"
removed: "&#6BCB77И&#62C973г&#5AC76Fр&#51C56Bо&#49C367к &#40C163{&#38BF5Fp&#2FBD5Bl&#27BB57a&#1EB953y&#16B74Fe&#0DB54Br&#05B347} &#05B347у&#05B347б&#05B347р&#05B347а&#05B347н &#05B347и&#05B347з &#05B347и&#05B347г&#05B347н&#05B347о&#05B347р&#05B347а"
already-ignored: "&#FFA726И&#FFA322г&#FF9F1Eр&#FF9B1Aо&#FF9716к &#FF9312у&#FF8F0Eж&#FF8B0Aе &#FF8706в &#FF8302и&#FF8000г&#FF8000н&#FF8000о&#FF8000р&#FF8000е"
not-ignored: "&#FFA726И&#FFA322г&#FF9F1Eр&#FF9B1Aо&#FF9716к &#FF9312н&#FF8F0Eе &#FF8B0Aв &#FF8706и&#FF8302г&#FF8000н&#FF8000о&#FF8000р&#FF8000е"
self: "&#FF6B6BН&#FF5F5Fе&#FF5353л&#FF4747ь&#FF3B3Bз&#FF2F2Fя &#FF2323и&#FF1717г&#FF0B0Bн&#FF0000о&#FF0000р&#FF0000и&#FF0000р&#FF0000о&#FF0000в&#FF0000а&#FF0000т&#FF0000ь &#FF0000с&#FF0000е&#FF0000б&#FF0000я"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОБЪЯВЛЕНИЯ │
# └─────────────────────────────────────────────────────────────────────────────┘
announce:
reloaded: "<gradient:#6BCB77:#4ECDC4>Конфигурация перезагружена</gradient>"
reloaded: "&#6BCB77К&#62C973о&#5AC76Fн&#51C56Bф&#49C367и&#40C163г&#38BF5Fу&#2FBD5Bр&#27BB57а&#1EB953ц&#16B74Fи&#0DB54Bя &#05B347п&#05B347е&#05B347р&#05B347е&#05B347з&#05B347а&#05B347г&#05B347р&#05B347у&#05B347ж&#05B347е&#05B347н&#05B347а"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ШПИОН РЕЖИМ │
# └─────────────────────────────────────────────────────────────────────────────┘
spy:
enabled: "<gradient:#6BCB77:#4ECDC4>Шпион режим включен</gradient>"
disabled: "<gradient:#FF6B6B:#FF4757>Шпион режим выключен</gradient>"
enabled: "&#6BCB77Ш&#62C973п&#5AC76Fи&#51C56Bо&#49C367н &#40C163р&#38BF5Fе&#2FBD5Bж&#27BB57и&#1EB953м &#16B74Fв&#0DB54Bк&#05B347л&#05B347ю&#05B347ч&#05B347е&#05B347н"
disabled: "&#FF6B6BШ&#FF5F5Fп&#FF5353и&#FF4747о&#FF3B3Bн &#FF2F2Fр&#FF2323е&#FF1717ж&#FF0B0Bи&#FF0000м &#FF0000в&#FF0000ы&#FF0000к&#FF0000л&#FF0000ю&#FF0000ч&#FF0000е&#FF0000н"
format: "<gray>[SPY]</gray> <gradient:#FFD700:#FFA500>{sender}</gradient> <gray>→</gray> <gradient:#87CEEB:#4682B4>{receiver}</gradient><gray>:</gray> <white>{message}</white>"
chat-format: "<gray>[SPY]</gray> <gradient:#FFA726:#FF9800>{type}</gradient> <gradient:#00FF88:#00CC66>{sender}</gradient><gray>:</gray> <white>{message}</white>"
@ -85,49 +85,49 @@ spy:
# └─────────────────────────────────────────────────────────────────────────────┘
filter:
blocked: "<gradient:#FF6B6B:#FF4757>Сообщение заблокировано (недопустимые слова)</gradient>"
warning: "<gradient:#FFA726:#FF9800>Предупреждение: не используйте недопустимые слова!</gradient>"
blocked: "&#FF6B6BС&#FF5F5Fо&#FF5353о&#FF4747б&#FF3B3Bщ&#FF2F2Fе&#FF2323н&#FF1717и&#FF0B0Bе &#FF0000з&#FF0000а&#FF0000б&#FF0000л&#FF0000о&#FF0000к&#FF0000и&#FF0000р&#FF0000о&#FF0000в&#FF0000а&#FF0000н&#FF0000о"
warning: "&#FFA726П&#FFA322р&#FF9F1Eе&#FF9B1Aд&#FF9716у&#FF9312п&#FF8F0Eр&#FF8B0Aе&#FF8706ж&#FF8302д&#FF8000е&#FF8000н&#FF8000и&#FF8000е"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ КУЛДАУНЫ │
# └─────────────────────────────────────────────────────────────────────────────┘
cooldown:
wait: "<gradient:#FFA726:#FF9800>Подождите {time} сек перед следующим сообщением</gradient>"
wait: "&#FFA726П&#FFA322о&#FF9F1Eд&#FF9B1Aо&#FF9716ж&#FF9312д&#FF8F0Eи&#FF8B0Aт&#FF8706е &#FF8302{&#FF8000t&#FF8000i&#FF8000m&#FF8000e&#FF8000}"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ АНТИ-СПАМ │
# └─────────────────────────────────────────────────────────────────────────────┘
antispam:
caps: "<gradient:#FF6B6B:#FF4757>Слишком много заглавных букв!</gradient>"
repeat: "<gradient:#FF6B6B:#FF4757>Не повторяйте символы!</gradient>"
similar: "<gradient:#FF6B6B:#FF4757>Не отправляйте одинаковые сообщения!</gradient>"
caps: "&#FF6B6BС&#FF5F5Fл&#FF5353и&#FF4747ш&#FF3B3Bк&#FF2F2Fо&#FF2323м &#FF1717м&#FF0B0Bн&#FF0000о&#FF0000г&#FF0000о &#FF0000з&#FF0000а&#FF0000г&#FF0000л&#FF0000а&#FF0000в&#FF0000н&#FF0000ы&#FF0000х"
repeat: "&#FF6B6BН&#FF5F5Fе &#FF5353п&#FF4747о&#FF3B3Bв&#FF2F2Fт&#FF2323о&#FF1717р&#FF0B0Bя&#FF0000й&#FF0000т&#FF0000е &#FF0000с&#FF0000и&#FF0000м&#FF0000в&#FF0000о&#FF0000л&#FF0000ы"
similar: "&#FF6B6BН&#FF5F5Fе &#FF5353о&#FF4747т&#FF3B3Bп&#FF2F2Fр&#FF2323а&#FF1717в&#FF0B0Bл&#FF0000я&#FF0000й&#FF0000т&#FF0000е &#FF0000о&#FF0000д&#FF0000и&#FF0000н&#FF0000а&#FF0000к&#FF0000о&#FF0000в&#FF0000ы&#FF0000е"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОЧИСТКА ЧАТА │
# └─────────────────────────────────────────────────────────────────────────────┘
clearchat:
cleared: "<gradient:#6BCB77:#4ECDC4>Чат очищен администратором {player}</gradient>"
cleared: "&#6BCB77Ч&#62C973а&#5AC76Fт &#51C56Bо&#49C367ч&#40C163и&#38BF5Fщ&#2FBD5Bе&#27BB57н &#1EB953{&#16B74Fp&#0DB54Bl&#05B347a&#05B347y&#05B347e&#05B347r&#05B347}"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ ОШИБКИ │
# └─────────────────────────────────────────────────────────────────────────────┘
errors:
no-permission: "<gradient:#FF6B6B:#FF4757>Нет прав!</gradient>"
player-not-found: "<gradient:#FF6B6B:#FF4757>Игрок не найден</gradient>"
invalid-usage: "<gradient:#FFA726:#FF9800>Использование: {usage}</gradient>"
no-permission: "&#FF6B6BН&#FF5F5Fе&#FF5353т &#FF4747п&#FF3B3Bр&#FF2F2Fа&#FF2323в&#FF1717!"
player-not-found: "&#FF6B6BИ&#FF5F5Fг&#FF5353р&#FF4747о&#FF3B3Bк &#FF2F2Fн&#FF2323е &#FF1717н&#FF0B0Bа&#FF0000й&#FF0000д&#FF0000е&#FF0000н"
invalid-usage: "&#FFA726И&#FFA322с&#FF9F1Eп&#FF9B1Aо&#FF9716л&#FF9312ь&#FF8F0Eз&#FF8B0Aо&#FF8706в&#FF8302а&#FF8000н&#FF8000и&#FF8000е&#FF8000: &#FF8000{&#FF8000u&#FF8000s&#FF8000a&#FF8000g&#FF8000e&#FF8000}"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ СИСТЕМА МУТОВ │
# └─────────────────────────────────────────────────────────────────────────────┘
mute:
muted: "<gradient:#6BCB77:#4ECDC4>Игрок {player} замучен на {time}</gradient>"
unmuted: "<gradient:#6BCB77:#4ECDC4>Игрок {player} размучен</gradient>"
already-muted: "<gradient:#FFA726:#FF9800>Игрок уже замучен</gradient>"
not-muted: "<gradient:#FFA726:#FF9800>Игрок не замучен</gradient>"
you-muted: "<gradient:#FF6B6B:#FF4757>Вы замучены! Осталось: {time}</gradient>"
permanent: "<gradient:#FF6B6B:#FF4757>Вы замучены навсегда!</gradient>"
muted: "&#6BCB77И&#62C973г&#5AC76Fр&#51C56Bо&#49C367к &#40C163{&#38BF5Fp&#2FBD5Bl&#27BB57a&#1EB953y&#16B74Fe&#0DB54Br&#05B347} &#05B347з&#05B347а&#05B347м&#05B347у&#05B347ч&#05B347е&#05B347н &#05B347н&#05B347а &#05B347{&#05B347t&#05B347i&#05B347m&#05B347e&#05B347}"
unmuted: "&#6BCB77И&#62C973г&#5AC76Fр&#51C56Bо&#49C367к &#40C163{&#38BF5Fp&#2FBD5Bl&#27BB57a&#1EB953y&#16B74Fe&#0DB54Br&#05B347} &#05B347р&#05B347а&#05B347з&#05B347м&#05B347у&#05B347ч&#05B347е&#05B347н"
already-muted: "&#FFA726И&#FFA322г&#FF9F1Eр&#FF9B1Aо&#FF9716к &#FF9312у&#FF8F0Eж&#FF8B0Aе &#FF8706з&#FF8302а&#FF8000м&#FF8000у&#FF8000ч&#FF8000е&#FF8000н"
not-muted: "&#FFA726И&#FFA322г&#FF9F1Eр&#FF9B1Aо&#FF9716к &#FF9312н&#FF8F0Eе &#FF8B0Aз&#FF8706а&#FF8302м&#FF8000у&#FF8000ч&#FF8000е&#FF8000н"
you-muted: "&#FF6B6BВ&#FF5F5Fы &#FF5353з&#FF4747а&#FF3B3Bм&#FF2F2Fу&#FF2323ч&#FF1717е&#FF0B0Bн&#FF0000ы&#FF0000! &#FF0000О&#FF0000с&#FF0000т&#FF0000а&#FF0000л&#FF0000о&#FF0000с&#FF0000ь&#FF0000: &#FF0000{&#FF0000t&#FF0000i&#FF0000m&#FF0000e&#FF0000}"
permanent: "&#FF6B6BВ&#FF5F5Fы &#FF5353з&#FF4747а&#FF3B3Bм&#FF2F2Fу&#FF2323ч&#FF1717е&#FF0B0Bн&#FF0000ы &#FF0000н&#FF0000а&#FF0000в&#FF0000с&#FF0000е&#FF0000г&#FF0000д&#FF0000а&#FF0000!"

View file

@ -241,3 +241,18 @@ permissions:
lochat.nick:
description: Can use custom nicknames
default: true
lochat.mention.everyone:
description: Can mention @everyone
default: op
lochat.mention.here:
description: Can mention @here
default: op
lochat.mention.role:
description: Can mention @role:<permission>
default: op
lochat.bypass.urlfilter:
description: Bypass URL filter
default: op
lochat.bypass.ipfilter:
description: Bypass IP filter
default: op