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