feat: chat channel system (/join /leave /channel)
- config-driven channels in config/channels.yml (global, local, staff, trade) with emoji, colors, prefix, radius, cooldowns, permissions and aliases - Channel record + ChannelConfig loader wired into ConfigManager (reload supported) - ChannelService (api) + ConcurrentHashMap-backed ChannelServiceImpl, joined in ServiceRegistry; default channel join treated as 'not in channel' - JoinChannelCommand / LeaveChannelCommand / ChannelCommand (+ tab completion), registered in CommandManager; plugin.yml commands + permissions - ChatEventListener routes AsyncChatEvent to the active channel: per-channel cooldown, local-radius viewer filtering, channel-aware renderer - EnhancedChatRenderer supports Channel overload (emojiFor/prefixText/channelColor) - ChannelServiceImplTest covering join/leave/permissions/aliases
This commit is contained in:
parent
689ab1a9ae
commit
a822c91637
17 changed files with 821 additions and 12 deletions
11
TODO.md
11
TODO.md
|
|
@ -66,11 +66,12 @@
|
|||
|
||||
## 🟡 NEXT: NEW FEATURES
|
||||
|
||||
### 1. Channels (like VentureChat / Carbon)
|
||||
- `/join`, `/leave`, `/channel` — full channel system: global, local, staff, trade, party.
|
||||
- Per-channel colors, aliases, permissions, cooldowns, spy mode.
|
||||
- Party chat: private groups `/party`.
|
||||
- Network channels (Bungee/Velocity) sync.
|
||||
### 1. Channels (like VentureChat / Carbon) ✅
|
||||
- `/join`, `/leave`, `/channel` — full channel system: global, local, staff, trade.
|
||||
- Config-driven channels via `config/channels.yml` (emoji, colors, prefix, radius, cooldowns, permissions, aliases).
|
||||
- Chat routing: `ChatEventListener` sends to the joined channel; local radius filtering; per-channel cooldown; channel-aware `EnhancedChatRenderer`.
|
||||
- Party chat: private groups `/party` (future).
|
||||
- Network channels (Bungee/Velocity) sync (future).
|
||||
|
||||
### 2. Anti-swear hardening + filter statistics ✅
|
||||
- Obfuscation bypass: `SwearNormalizer` (q→р / leet-подстановки, повторы букв «сууука», разделители внутри слова «с.у.к.а» / «с у к а») + tolerant-маскирование в replace-режиме. Single source of truth для Aho-Corasick.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
package com.loki.lochat.api.service;
|
||||
|
||||
import com.loki.lochat.channel.Channel;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Сервис каналов чата: состояние игроков (в каком канале они состоят).
|
||||
*/
|
||||
public interface ChannelService {
|
||||
|
||||
boolean isEnabled();
|
||||
|
||||
Channel getDefault();
|
||||
|
||||
Collection<Channel> getChannels();
|
||||
|
||||
Channel find(String nameOrAlias);
|
||||
|
||||
/**
|
||||
* Активный (не-дефолтный) канал игрока, если он его присоединил через /join.
|
||||
*/
|
||||
Optional<Channel> getActive(Player player);
|
||||
|
||||
boolean isInChannel(Player player);
|
||||
|
||||
boolean canUse(Player player, Channel channel);
|
||||
|
||||
/**
|
||||
* Переводит игрока в канал. Дефолтный канал трактуется как «не присоединён».
|
||||
*
|
||||
* @return true если присоединение выполнено, false если нет канала или нет прав
|
||||
*/
|
||||
boolean join(Player player, Channel channel);
|
||||
|
||||
void leave(Player player);
|
||||
}
|
||||
33
src/main/java/com/loki/lochat/channel/Channel.java
Normal file
33
src/main/java/com/loki/lochat/channel/Channel.java
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package com.loki.lochat.channel;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Канал чата (global, local, staff, trade...).
|
||||
* Иммутабельное описание канала из config/channels.yml.
|
||||
*/
|
||||
public record Channel(
|
||||
String id,
|
||||
String display,
|
||||
String prefix,
|
||||
String color,
|
||||
int radius,
|
||||
int cooldown,
|
||||
String permission,
|
||||
List<String> aliases) {
|
||||
|
||||
/** radius < 1 — глобальный канал (видны всем игрокам) */
|
||||
public boolean isGlobalScope() {
|
||||
return radius < 1;
|
||||
}
|
||||
|
||||
/** radius > 0 — локальный канал с радиусом действия */
|
||||
public boolean isLocalScope() {
|
||||
return radius > 0;
|
||||
}
|
||||
|
||||
/** Есть ли требование по праву для входа/использования */
|
||||
public boolean hasPermission() {
|
||||
return permission != null && !permission.isBlank();
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,10 @@ import com.loki.lochat.commands.admin.chat.ClearChatCommand;
|
|||
import com.loki.lochat.commands.admin.chat.ClearChatConfigCommand;
|
||||
import com.loki.lochat.commands.admin.system.LoChatCommand;
|
||||
import com.loki.lochat.commands.admin.system.ReloadConfigCommand;
|
||||
import com.loki.lochat.commands.chat.ChannelCommand;
|
||||
import com.loki.lochat.commands.chat.GlobalChatCommand;
|
||||
import com.loki.lochat.commands.chat.JoinChannelCommand;
|
||||
import com.loki.lochat.commands.chat.LeaveChannelCommand;
|
||||
import com.loki.lochat.commands.chat.LocalChatCommand;
|
||||
import com.loki.lochat.commands.messaging.IgnoreCommand;
|
||||
import com.loki.lochat.commands.messaging.IgnoreListCommand;
|
||||
|
|
@ -54,6 +57,11 @@ public class CommandManager {
|
|||
reg("g", new GlobalChatCommand(plugin));
|
||||
reg("l", new LocalChatCommand(plugin));
|
||||
|
||||
// ── Каналы ─────────────────────────────────────────────────────────────
|
||||
regTab("join", new JoinChannelCommand(plugin));
|
||||
reg("leave", new LeaveChannelCommand(plugin));
|
||||
regTab("channel", new ChannelCommand(plugin));
|
||||
|
||||
// ── Личные сообщения / игнор ──────────────────────────────────────────
|
||||
reg("msg", new MsgCommand(plugin));
|
||||
reg("reply", new ReplyCommand(plugin));
|
||||
|
|
|
|||
120
src/main/java/com/loki/lochat/commands/chat/ChannelCommand.java
Normal file
120
src/main/java/com/loki/lochat/commands/chat/ChannelCommand.java
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package com.loki.lochat.commands.chat;
|
||||
|
||||
import com.loki.lochat.LoChat;
|
||||
import com.loki.lochat.api.service.ChannelService;
|
||||
import com.loki.lochat.channel.Channel;
|
||||
import com.loki.lochat.commands.base.PlayerCommand;
|
||||
import com.loki.lochat.utils.format.ChatFormatter;
|
||||
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* /channel — инфо о текущем канале со списком доступных.
|
||||
* /channel <канал> — переключиться. /channel off — выйти.
|
||||
* /channel list — список каналов.
|
||||
*/
|
||||
public class ChannelCommand extends PlayerCommand {
|
||||
|
||||
public ChannelCommand(LoChat plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean executePlayerCommand(@NotNull Player player, @NotNull Command command,
|
||||
@NotNull String label, @NotNull String[] args) {
|
||||
ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class);
|
||||
if (!channels.isEnabled()) {
|
||||
player.sendMessage(ChatFormatter.parse("&#CF6679Система каналов отключена"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (args.length == 0) {
|
||||
showCurrent(player, channels);
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (args[0].toLowerCase(Locale.ROOT)) {
|
||||
case "off", "leave" -> {
|
||||
if (channels.isInChannel(player)) {
|
||||
channels.leave(player);
|
||||
player.sendMessage(ChatFormatter.parse("⚖C9Вы вышли из канала"));
|
||||
} else {
|
||||
player.sendMessage(ChatFormatter.parse("&#B798A8Вы не находитесь в канале"));
|
||||
}
|
||||
}
|
||||
case "list" -> listChannels(player, channels);
|
||||
default -> joinChannel(player, channels, args[0]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showCurrent(Player player, ChannelService channels) {
|
||||
Channel active = channels.getActive(player).orElse(null);
|
||||
if (active == null) {
|
||||
Channel def = channels.getDefault();
|
||||
player.sendMessage(ChatFormatter.parse("&#B798A8Вы в дефолтном канале [ẲE9"
|
||||
+ (def == null ? "?" : def.display() + "&8 " + def.prefix()) + "&#B798A8]"));
|
||||
} else {
|
||||
player.sendMessage(ChatFormatter.parse("⚖C9Вы в канале [ẲE9"
|
||||
+ active.display() + "&8 " + active.prefix() + "⚖C9]"));
|
||||
}
|
||||
player.sendMessage(ChatFormatter.parse("&#B798A8/join <канал> ⚖C9— войти, "
|
||||
+ "&#B798A8/channel list ⚖C9— все каналы"));
|
||||
}
|
||||
|
||||
private void listChannels(Player player, ChannelService channels) {
|
||||
player.sendMessage(ChatFormatter.parse("&#B798A8Доступные каналы:"));
|
||||
for (Channel c : channels.getChannels()) {
|
||||
boolean canUse = channels.canUse(player, c);
|
||||
boolean active = c.equals(channels.getActive(player).orElse(null));
|
||||
player.sendMessage(ChatFormatter.parse((canUse ? "⚖C9" : "&#CF6679")
|
||||
+ (active ? "\u25B8 " : "\u25AA ") + c.display() + "&8 " + c.prefix()
|
||||
+ (canUse ? "" : " &#CF6679(нет прав)")
|
||||
+ " &#B798A8— " + (c.isGlobalScope() ? "глобальный" : "локальный, " + c.radius() + "б")));
|
||||
}
|
||||
}
|
||||
|
||||
private void joinChannel(Player player, ChannelService channels, String name) {
|
||||
Channel channel = channels.find(name);
|
||||
if (channel == null) {
|
||||
player.sendMessage(ChatFormatter.parse("&#CF6679Канал ẲE9" + name + " &#CF6679не найден"));
|
||||
return;
|
||||
}
|
||||
if (!channels.canUse(player, channel)) {
|
||||
player.sendMessage(ChatFormatter.parse(
|
||||
plugin.getConfigManager().getMessagesConfig().getNoPermission()));
|
||||
return;
|
||||
}
|
||||
Channel active = channels.getActive(player).orElse(null);
|
||||
channels.join(player, channel);
|
||||
if (channel.equals(active)) {
|
||||
player.sendMessage(ChatFormatter.parse("&#B798A8Вы уже в канале [ẲE9"
|
||||
+ channel.display() + "&8 " + channel.prefix() + "&#B798A8]"));
|
||||
} else {
|
||||
player.sendMessage(ChatFormatter.parse("⚖C9Вы вошли в канал [ẲE9"
|
||||
+ channel.display() + "&8 " + channel.prefix() + "⚖C9]"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable List<String> getTabCompletions(@NotNull CommandSender sender, @NotNull Command command,
|
||||
@NotNull String alias, @NotNull String[] args) {
|
||||
if (args.length != 1) {
|
||||
return null;
|
||||
}
|
||||
ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class);
|
||||
String input = args[0].toLowerCase(Locale.ROOT);
|
||||
return java.util.stream.Stream.concat(
|
||||
java.util.stream.Stream.of("list", "off"),
|
||||
channels.getChannels().stream().map(Channel::id))
|
||||
.filter(s -> s.startsWith(input))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package com.loki.lochat.commands.chat;
|
||||
|
||||
import com.loki.lochat.LoChat;
|
||||
import com.loki.lochat.api.service.ChannelService;
|
||||
import com.loki.lochat.channel.Channel;
|
||||
import com.loki.lochat.commands.base.PlayerCommand;
|
||||
import com.loki.lochat.utils.format.ChatFormatter;
|
||||
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* /join <канал|алиас> — войти в канал чата.
|
||||
*/
|
||||
public class JoinChannelCommand extends PlayerCommand {
|
||||
|
||||
public JoinChannelCommand(LoChat plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean executePlayerCommand(@NotNull Player player, @NotNull Command command,
|
||||
@NotNull String label, @NotNull String[] args) {
|
||||
if (!plugin.getConfigManager().getChannelConfig().isEnabled()) {
|
||||
player.sendMessage(ChatFormatter.parse("&#CF6679Система каналов отключена"));
|
||||
return true;
|
||||
}
|
||||
if (args.length == 0) {
|
||||
player.sendMessage(ChatFormatter.parse("&#CF6679Использование: ẲE9/join <канал>"));
|
||||
return true;
|
||||
}
|
||||
|
||||
ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class);
|
||||
Channel channel = channels.find(args[0]);
|
||||
if (channel == null) {
|
||||
player.sendMessage(ChatFormatter.parse("&#CF6679Канал ẲE9" + args[0] + " &#CF6679не найден"));
|
||||
return true;
|
||||
}
|
||||
if (!channels.canUse(player, channel)) {
|
||||
player.sendMessage(ChatFormatter.parse(
|
||||
plugin.getConfigManager().getMessagesConfig().getNoPermission()));
|
||||
return true;
|
||||
}
|
||||
|
||||
Channel active = channels.getActive(player).orElse(null);
|
||||
channels.join(player, channel);
|
||||
if (channel.equals(channels.getDefault())) {
|
||||
player.sendMessage(ChatFormatter.parse("⚖C9Вы в канале [ẲE9"
|
||||
+ channel.display() + "&8 " + channel.prefix() + "⚖C9] (дефолтный)"));
|
||||
} else if (channel.equals(active)) {
|
||||
player.sendMessage(ChatFormatter.parse("&#B798A8Вы уже в канале [ẲE9"
|
||||
+ channel.display() + "&8 " + channel.prefix() + "&#B798A8]"));
|
||||
} else {
|
||||
player.sendMessage(ChatFormatter.parse("⚖C9Вы вошли в канал [ẲE9"
|
||||
+ channel.display() + "&8 " + channel.prefix() + "⚖C9]"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable List<String> getTabCompletions(@NotNull CommandSender sender, @NotNull Command command,
|
||||
@NotNull String alias, @NotNull String[] args) {
|
||||
if (args.length != 1) {
|
||||
return null;
|
||||
}
|
||||
ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class);
|
||||
String input = args[0].toLowerCase(Locale.ROOT);
|
||||
return channels.getChannels().stream()
|
||||
.filter(c -> !(sender instanceof Player p) || channels.canUse(p, c))
|
||||
.map(Channel::id)
|
||||
.filter(id -> id.toLowerCase(Locale.ROOT).startsWith(input))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.loki.lochat.commands.chat;
|
||||
|
||||
import com.loki.lochat.LoChat;
|
||||
import com.loki.lochat.api.service.ChannelService;
|
||||
import com.loki.lochat.commands.base.PlayerCommand;
|
||||
import com.loki.lochat.utils.format.ChatFormatter;
|
||||
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* /leave — выйти из текущего канала (вернуться к дефолтному чату).
|
||||
*/
|
||||
public class LeaveChannelCommand extends PlayerCommand {
|
||||
|
||||
public LeaveChannelCommand(LoChat plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean executePlayerCommand(@NotNull Player player, @NotNull Command command,
|
||||
@NotNull String label, @NotNull String[] args) {
|
||||
ChannelService channels = plugin.getServiceRegistry().get(ChannelService.class);
|
||||
if (!channels.isEnabled()) {
|
||||
player.sendMessage(ChatFormatter.parse("&#CF6679Система каналов отключена"));
|
||||
return true;
|
||||
}
|
||||
if (!channels.isInChannel(player)) {
|
||||
player.sendMessage(ChatFormatter.parse("&#B798A8Вы не находитесь в канале"));
|
||||
return true;
|
||||
}
|
||||
channels.leave(player);
|
||||
player.sendMessage(ChatFormatter.parse("⚖C9Вы вышли из канала"));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
99
src/main/java/com/loki/lochat/config/ChannelConfig.java
Normal file
99
src/main/java/com/loki/lochat/config/ChannelConfig.java
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
package com.loki.lochat.config;
|
||||
|
||||
import com.loki.lochat.channel.Channel;
|
||||
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Конфигурация каналов чата (config/channels.yml).
|
||||
*/
|
||||
public class ChannelConfig extends BaseConfig {
|
||||
|
||||
private volatile Map<String, Channel> channels = new LinkedHashMap<>();
|
||||
private volatile Map<String, Channel> byAlias = new LinkedHashMap<>();
|
||||
private volatile Channel defaultChannel;
|
||||
|
||||
public ChannelConfig(JavaPlugin plugin) {
|
||||
super(plugin, "channels.yml", true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLoad() {
|
||||
loadChannels();
|
||||
buildAliasIndex();
|
||||
}
|
||||
|
||||
private void loadChannels() {
|
||||
Map<String, Channel> result = new LinkedHashMap<>();
|
||||
ConfigurationSection section = getConfig().getConfigurationSection("channels.list");
|
||||
if (section != null) {
|
||||
for (String id : section.getKeys(false)) {
|
||||
String path = "channels.list." + id;
|
||||
result.put(id, new Channel(
|
||||
id,
|
||||
getConfig().getString(path + ".display", ""),
|
||||
getConfig().getString(path + ".prefix", id.toUpperCase(Locale.ROOT)),
|
||||
getConfig().getString(path + ".color", "&7"),
|
||||
getConfig().getInt(path + ".radius", -1),
|
||||
getConfig().getInt(path + ".cooldown", 0),
|
||||
getConfig().getString(path + ".permission", ""),
|
||||
getConfig().getStringList(path + ".aliases")));
|
||||
}
|
||||
}
|
||||
this.channels = result;
|
||||
}
|
||||
|
||||
private void buildAliasIndex() {
|
||||
Map<String, Channel> index = new LinkedHashMap<>();
|
||||
for (Channel channel : channels.values()) {
|
||||
index.put(channel.id().toLowerCase(Locale.ROOT), channel);
|
||||
for (String alias : channel.aliases()) {
|
||||
if (alias == null || alias.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
index.putIfAbsent(alias.toLowerCase(Locale.ROOT), channel);
|
||||
}
|
||||
}
|
||||
this.byAlias = index;
|
||||
|
||||
String defaultId = getConfig().getString("channels.default", "global");
|
||||
this.defaultChannel = channels.get(defaultId);
|
||||
if (defaultChannel == null) {
|
||||
this.defaultChannel = channels.isEmpty() ? null : channels.values().iterator().next();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return getConfig().getBoolean("channels.enabled", true);
|
||||
}
|
||||
|
||||
public Channel getDefault() {
|
||||
return defaultChannel;
|
||||
}
|
||||
|
||||
public Channel get(String id) {
|
||||
return id == null ? null : channels.get(id.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск канала по имени или алиасу (без учёта регистра).
|
||||
*/
|
||||
public Channel find(String name) {
|
||||
return name == null ? null : byAlias.get(name.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public Collection<Channel> getChannels() {
|
||||
return channels.values();
|
||||
}
|
||||
|
||||
public List<Channel> getChannelList() {
|
||||
return List.copyOf(channels.values());
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ public class ConfigManager {
|
|||
private volatile MuteConfig muteConfig;
|
||||
private volatile FiltersConfig filtersConfig;
|
||||
private volatile SoundsConfig soundsConfig;
|
||||
private volatile ChannelConfig channelConfig;
|
||||
|
||||
public ConfigManager(LoChat plugin) {
|
||||
this.plugin = plugin;
|
||||
|
|
@ -33,6 +34,7 @@ public class ConfigManager {
|
|||
this.muteConfig = new MuteConfig(plugin);
|
||||
this.filtersConfig = new FiltersConfig(plugin);
|
||||
this.soundsConfig = new SoundsConfig(plugin);
|
||||
this.channelConfig = new ChannelConfig(plugin);
|
||||
|
||||
// Инициализируем все конфиги
|
||||
this.appearanceConfig.init();
|
||||
|
|
@ -40,6 +42,7 @@ public class ConfigManager {
|
|||
this.muteConfig.init();
|
||||
this.filtersConfig.init();
|
||||
this.soundsConfig.init();
|
||||
this.channelConfig.init();
|
||||
|
||||
loader.updateConfig();
|
||||
}
|
||||
|
|
@ -66,6 +69,10 @@ public class ConfigManager {
|
|||
return soundsConfig;
|
||||
}
|
||||
|
||||
public ChannelConfig getChannelConfig() {
|
||||
return channelConfig;
|
||||
}
|
||||
|
||||
// ========== Обновление и перезагрузка ==========
|
||||
|
||||
public synchronized void reload() {
|
||||
|
|
@ -77,6 +84,7 @@ public class ConfigManager {
|
|||
this.muteConfig.reload();
|
||||
this.filtersConfig.reload();
|
||||
this.soundsConfig.reload();
|
||||
this.channelConfig.reload();
|
||||
|
||||
plugin.getLogger().info("Все конфигурации успешно перезагружены");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,8 @@ public class PluginInitializer {
|
|||
plugin.setChatFilter(filter);
|
||||
|
||||
plugin.getServer().getPluginManager().registerEvents(
|
||||
new ChatEventListener(plugin, messageService, playerService, filter), plugin
|
||||
new ChatEventListener(plugin, messageService, playerService, filter,
|
||||
serviceRegistry.get(com.loki.lochat.api.service.ChannelService.class)), plugin
|
||||
);
|
||||
plugin.getServer().getPluginManager().registerEvents(
|
||||
new PlayerEventListener(plugin, playerService, messagingService, nickService, filter), plugin
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.loki.lochat.core.registry;
|
||||
|
||||
import com.loki.lochat.LoChat;
|
||||
import com.loki.lochat.api.service.ChannelService;
|
||||
import com.loki.lochat.api.service.ChatService;
|
||||
import com.loki.lochat.api.service.MentionService;
|
||||
import com.loki.lochat.api.service.MessageService;
|
||||
|
|
@ -14,6 +15,7 @@ import com.loki.lochat.config.ConfigManager;
|
|||
import com.loki.lochat.config.MessageConfig;
|
||||
import com.loki.lochat.core.factory.ServiceFactory;
|
||||
import com.loki.lochat.core.service.ChatServiceImpl;
|
||||
import com.loki.lochat.core.service.ChannelServiceImpl;
|
||||
import com.loki.lochat.core.service.MentionServiceImpl;
|
||||
import com.loki.lochat.core.service.MessageServiceImpl;
|
||||
import com.loki.lochat.core.service.NickServiceImpl;
|
||||
|
|
@ -47,6 +49,8 @@ public class ServiceRegistry {
|
|||
PlayerService playerService = new PlayerServiceImpl(plugin);
|
||||
register(PlayerService.class, playerService);
|
||||
|
||||
register(ChannelService.class, new ChannelServiceImpl(configManager.getChannelConfig()));
|
||||
|
||||
MessageService messageService = new MessageServiceImpl(plugin, muteService, playerService);
|
||||
register(MessageService.class, messageService);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
package com.loki.lochat.core.service;
|
||||
|
||||
import com.loki.lochat.api.service.ChannelService;
|
||||
import com.loki.lochat.channel.Channel;
|
||||
import com.loki.lochat.config.ChannelConfig;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Реализация сервиса каналов чата.
|
||||
* Хранит активный (присоединённый) канал каждого игрока в памяти.
|
||||
*/
|
||||
public class ChannelServiceImpl implements ChannelService {
|
||||
|
||||
private final ChannelConfig config;
|
||||
private final Map<UUID, String> activeChannels = new ConcurrentHashMap<>();
|
||||
|
||||
public ChannelServiceImpl(ChannelConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return config.isEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Channel getDefault() {
|
||||
return config.getDefault();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Channel> getChannels() {
|
||||
return config.getChannels();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Channel find(String nameOrAlias) {
|
||||
return config.find(nameOrAlias);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Channel> getActive(Player player) {
|
||||
String id = activeChannels.get(player.getUniqueId());
|
||||
if (id == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(config.get(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInChannel(Player player) {
|
||||
return activeChannels.containsKey(player.getUniqueId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse(Player player, Channel channel) {
|
||||
return !channel.hasPermission() || player.hasPermission(channel.permission());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean join(Player player, Channel channel) {
|
||||
if (channel == null || !canUse(player, channel)) {
|
||||
return false;
|
||||
}
|
||||
Channel defaultChannel = config.getDefault();
|
||||
if (defaultChannel != null && channel.id().equals(defaultChannel.id())) {
|
||||
activeChannels.remove(player.getUniqueId());
|
||||
return true;
|
||||
}
|
||||
activeChannels.put(player.getUniqueId(), channel.id());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void leave(Player player) {
|
||||
activeChannels.remove(player.getUniqueId());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
package com.loki.lochat.listener;
|
||||
|
||||
import com.loki.lochat.LoChat;
|
||||
import com.loki.lochat.api.service.ChannelService;
|
||||
import com.loki.lochat.api.service.MessageService;
|
||||
import com.loki.lochat.api.service.PlayerService;
|
||||
import com.loki.lochat.channel.Channel;
|
||||
import com.loki.lochat.core.filter.AdvancedMessageFilter;
|
||||
import com.loki.lochat.core.filter.FilterResult;
|
||||
import com.loki.lochat.renderer.EnhancedChatRenderer;
|
||||
|
|
@ -24,13 +26,15 @@ public class ChatEventListener implements Listener {
|
|||
private final MessageService messageService;
|
||||
private final PlayerService playerService;
|
||||
private final AdvancedMessageFilter advancedFilter;
|
||||
private final ChannelService channelService;
|
||||
|
||||
public ChatEventListener(JavaPlugin plugin, MessageService messageService, PlayerService playerService,
|
||||
AdvancedMessageFilter filter) {
|
||||
AdvancedMessageFilter filter, ChannelService channelService) {
|
||||
this.plugin = plugin;
|
||||
this.messageService = messageService;
|
||||
this.playerService = playerService;
|
||||
this.advancedFilter = filter;
|
||||
this.channelService = channelService;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
|
|
@ -39,6 +43,13 @@ public class ChatEventListener implements Listener {
|
|||
String rawMessage = PlainTextComponentSerializer.plainText().serialize(event.message());
|
||||
LoChat loChat = (LoChat) plugin;
|
||||
|
||||
// Система каналов: если игрок в присоединённом канале — маршрутизируем туда
|
||||
Channel active = channelService.isEnabled() ? channelService.getActive(sender).orElse(null) : null;
|
||||
if (active != null) {
|
||||
handleChannelChat(event, sender, active, rawMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Определяем режим чата (глобальный/локальный)
|
||||
ChatMode mode = determineChatMode(loChat, rawMessage);
|
||||
if (mode == null) {
|
||||
|
|
@ -66,6 +77,52 @@ public class ChatEventListener implements Listener {
|
|||
recordStatistics(loChat, sender, isGlobal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Маршрутизация сообщения в присоединённый (не-дефолтный) канал.
|
||||
*/
|
||||
private void handleChannelChat(AsyncChatEvent event, Player sender, Channel channel, String rawMessage) {
|
||||
String plainMessage = rawMessage.stripLeading();
|
||||
|
||||
// Кулдаун канала
|
||||
int cooldown = channel.cooldown();
|
||||
if (cooldown > 0 && !sender.hasPermission("chat.bypass.cooldown")) {
|
||||
String cooldownType = "channel_" + channel.id();
|
||||
if (playerService.isOnCooldown(sender.getUniqueId(), cooldownType, cooldown)) {
|
||||
int remaining = playerService.getRemainingCooldown(sender.getUniqueId(), cooldownType, cooldown);
|
||||
String msg = ((LoChat) plugin).getConfigManager().getMessagesConfig().getCooldownMessage()
|
||||
.replace("{remaining}", String.valueOf(remaining));
|
||||
event.setCancelled(true);
|
||||
sender.sendMessage(ChatFormatter.parse(msg));
|
||||
return;
|
||||
}
|
||||
playerService.setCooldown(sender.getUniqueId(), cooldownType);
|
||||
}
|
||||
|
||||
if (!processFilters(event, sender, plainMessage, channel.isGlobalScope())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Радиус локального канала
|
||||
if (channel.isLocalScope()) {
|
||||
int radius = channel.radius();
|
||||
event.viewers().removeIf(v -> {
|
||||
if (!(v instanceof Player p)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return !com.loki.lochat.utils.player.DistanceUtil.isInRange(sender, p, radius);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
event.renderer(new EnhancedChatRenderer(plugin, channel));
|
||||
LoChat loChat = (LoChat) plugin;
|
||||
loChat.getDiscordIntegration().sendChatMessage(sender, plainMessage, channel.isGlobalScope());
|
||||
playerService.recordMessage(sender.getUniqueId(), "channel_" + channel.id());
|
||||
}
|
||||
|
||||
private ChatMode determineChatMode(LoChat loChat, String rawMessage) {
|
||||
boolean localEnabled = loChat.getConfigManager().isLocalEnabled();
|
||||
boolean isGlobal;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package com.loki.lochat.renderer;
|
||||
|
||||
import com.loki.lochat.channel.Channel;
|
||||
import com.loki.lochat.renderer.components.EmojiComponent;
|
||||
import com.loki.lochat.renderer.components.GradientBuilder;
|
||||
import com.loki.lochat.renderer.components.UrlProcessor;
|
||||
import com.loki.lochat.translate.TranslationService;
|
||||
import com.loki.lochat.utils.format.ChatFormatter;
|
||||
import com.loki.lochat.utils.format.TextFormatter;
|
||||
import com.loki.lochat.utils.player.MentionHandler;
|
||||
|
||||
|
|
@ -33,12 +35,22 @@ public class EnhancedChatRenderer implements ChatRenderer {
|
|||
|
||||
private final JavaPlugin plugin;
|
||||
private final boolean isGlobal;
|
||||
private final Channel channel;
|
||||
private final MentionHandler mentionHandler;
|
||||
private final com.loki.lochat.config.ConfigManager cfg;
|
||||
|
||||
public EnhancedChatRenderer(JavaPlugin plugin, boolean isGlobal) {
|
||||
this(plugin, isGlobal, null);
|
||||
}
|
||||
|
||||
public EnhancedChatRenderer(JavaPlugin plugin, Channel channel) {
|
||||
this(plugin, channel.isGlobalScope(), channel);
|
||||
}
|
||||
|
||||
private EnhancedChatRenderer(JavaPlugin plugin, boolean isGlobal, Channel channel) {
|
||||
this.plugin = plugin;
|
||||
this.isGlobal = isGlobal;
|
||||
this.channel = channel;
|
||||
this.mentionHandler = new MentionHandler(plugin);
|
||||
|
||||
com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin;
|
||||
|
|
@ -86,6 +98,26 @@ public class EnhancedChatRenderer implements ChatRenderer {
|
|||
));
|
||||
}
|
||||
|
||||
private String emojiFor() {
|
||||
if (channel != null) {
|
||||
return channel.display() == null ? "" : channel.display();
|
||||
}
|
||||
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
|
||||
return isGlobal ? appearanceCfg.getGlobalEmoji() : appearanceCfg.getLocalEmoji();
|
||||
}
|
||||
|
||||
private String prefixText() {
|
||||
if (channel != null) {
|
||||
return channel.prefix() == null ? channel.id() : channel.prefix();
|
||||
}
|
||||
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
|
||||
return isGlobal ? appearanceCfg.getGlobalText() : appearanceCfg.getLocalText();
|
||||
}
|
||||
|
||||
private String channelColor() {
|
||||
return channel != null && channel.color() != null ? channel.color() : "&7";
|
||||
}
|
||||
|
||||
private Component buildFormattedMessage(String format, Map<String, Component> parts) {
|
||||
Component result = Component.empty();
|
||||
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\{([^}]+)\\}");
|
||||
|
|
@ -112,8 +144,7 @@ public class EnhancedChatRenderer implements ChatRenderer {
|
|||
}
|
||||
|
||||
private Component buildEmojiComponent() {
|
||||
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
|
||||
String emoji = isGlobal ? appearanceCfg.getGlobalEmoji() : appearanceCfg.getLocalEmoji();
|
||||
String emoji = emojiFor();
|
||||
return emoji.isEmpty() ? Component.empty() : Component.text(emoji);
|
||||
}
|
||||
|
||||
|
|
@ -146,9 +177,11 @@ public class EnhancedChatRenderer implements ChatRenderer {
|
|||
}
|
||||
|
||||
private Component buildPrefix() {
|
||||
String text = prefixText();
|
||||
if (channel != null) {
|
||||
return ChatFormatter.parse(channelColor() + text);
|
||||
}
|
||||
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
|
||||
|
||||
String text = isGlobal ? appearanceCfg.getGlobalText() : appearanceCfg.getLocalText();
|
||||
List<String> colors = isGlobal ? appearanceCfg.getGlobalColors() : appearanceCfg.getLocalColors();
|
||||
|
||||
return GradientBuilder.build(text, colors);
|
||||
|
|
@ -157,7 +190,9 @@ public class EnhancedChatRenderer implements ChatRenderer {
|
|||
private Component buildSeparator() {
|
||||
com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig();
|
||||
String text = isGlobal ? appearanceCfg.getGlobalSeparatorText() : appearanceCfg.getLocalSeparatorText();
|
||||
String color = isGlobal ? appearanceCfg.getGlobalSeparatorColor() : appearanceCfg.getLocalSeparatorColor();
|
||||
String color = channel != null
|
||||
? channelColor()
|
||||
: (isGlobal ? appearanceCfg.getGlobalSeparatorColor() : appearanceCfg.getLocalSeparatorColor());
|
||||
return Component.text(text, parseColor(color));
|
||||
}
|
||||
|
||||
|
|
|
|||
57
src/main/resources/config/channels.yml
Normal file
57
src/main/resources/config/channels.yml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# LoChat channels
|
||||
# Channels let players choose where their chat messages go.
|
||||
# /join <channel> — join a channel
|
||||
# /leave — leave the current channel (back to default chat)
|
||||
# /channel [name|list|off] — view info, switch, or list channels
|
||||
#
|
||||
# Per channel:
|
||||
# display — icon shown before the channel name in messages
|
||||
# prefix — channel label
|
||||
# color — color of the prefix (legacy & codes or &#RRGGBB)
|
||||
# radius -1 = global (all players), >0 = local radius in blocks
|
||||
# cooldown seconds between messages in this channel (0 = off)
|
||||
# permission required to join/use the channel (empty = everyone)
|
||||
# aliases alternative names to reference the channel
|
||||
|
||||
channels:
|
||||
# Master switch for the channel system
|
||||
enabled: true
|
||||
# Default channel id: when a player is not in a joined channel, they fall back to it
|
||||
default: "global"
|
||||
|
||||
list:
|
||||
global:
|
||||
display: "\u2726"
|
||||
prefix: "GLOBAL"
|
||||
color: "&b"
|
||||
radius: -1
|
||||
cooldown: 3
|
||||
permission: ""
|
||||
aliases: [ g ]
|
||||
|
||||
local:
|
||||
display: "\u25C6"
|
||||
prefix: "LOCAL"
|
||||
color: "&d"
|
||||
radius: 100
|
||||
cooldown: 1
|
||||
permission: ""
|
||||
aliases: [ l ]
|
||||
|
||||
staff:
|
||||
display: "\u2694"
|
||||
prefix: "STAFF"
|
||||
color: "&6"
|
||||
radius: -1
|
||||
cooldown: 2
|
||||
permission: "lochat.channel.staff"
|
||||
aliases: [ s, staffchat ]
|
||||
|
||||
trade:
|
||||
display: "\u27A4"
|
||||
prefix: "TRADE"
|
||||
color: "&e"
|
||||
radius: -1
|
||||
cooldown: 5
|
||||
permission: "lochat.channel.trade"
|
||||
aliases: [ t, tradechat ]
|
||||
|
|
@ -20,6 +20,24 @@ commands:
|
|||
permission: chat.global.use
|
||||
aliases: [ п ]
|
||||
|
||||
join:
|
||||
description: Join a chat channel
|
||||
usage: /join <channel>
|
||||
permission: chat.channel.join
|
||||
aliases: [ войти ]
|
||||
|
||||
leave:
|
||||
description: Leave the current chat channel
|
||||
usage: /leave
|
||||
permission: chat.channel.leave
|
||||
aliases: [ выйти ]
|
||||
|
||||
channel:
|
||||
description: View or switch chat channel
|
||||
usage: /channel [channel|list|off]
|
||||
permission: chat.channel.use
|
||||
aliases: [ ch, канал, channels, каналы ]
|
||||
|
||||
globalchat:
|
||||
description: Toggle global chat
|
||||
usage: /globalchat toggle
|
||||
|
|
@ -225,6 +243,21 @@ commands:
|
|||
permissions:
|
||||
chat.global.use:
|
||||
default: true
|
||||
chat.channel.use:
|
||||
description: Can use /channel
|
||||
default: true
|
||||
chat.channel.join:
|
||||
description: Can use /join
|
||||
default: true
|
||||
chat.channel.leave:
|
||||
description: Can use /leave
|
||||
default: true
|
||||
lochat.channel.staff:
|
||||
description: Access to the staff channel
|
||||
default: op
|
||||
lochat.channel.trade:
|
||||
description: Access to the trade channel
|
||||
default: op
|
||||
chat.global.toggle:
|
||||
default: true
|
||||
chat.global.color:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
package com.loki.lochat.core.service;
|
||||
|
||||
import com.loki.lochat.api.service.ChannelService;
|
||||
import com.loki.lochat.channel.Channel;
|
||||
import com.loki.lochat.config.ChannelConfig;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class ChannelServiceImplTest {
|
||||
|
||||
private final Channel global = new Channel("global", "\u2726", "GLOBAL", "&b", -1, 3, "", List.of("g"));
|
||||
private final Channel staff = new Channel("staff", "\u2694", "STAFF", "&6", -1, 2,
|
||||
"lochat.channel.staff", List.of("s"));
|
||||
|
||||
private Player player() {
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
return player;
|
||||
}
|
||||
|
||||
private ChannelService createService() {
|
||||
ChannelConfig config = mock(ChannelConfig.class);
|
||||
when(config.isEnabled()).thenReturn(true);
|
||||
when(config.getDefault()).thenReturn(global);
|
||||
when(config.get("global")).thenReturn(global);
|
||||
when(config.get("staff")).thenReturn(staff);
|
||||
when(config.find("global")).thenReturn(global);
|
||||
when(config.find("staff")).thenReturn(staff);
|
||||
when(config.find("s")).thenReturn(staff);
|
||||
when(config.getChannels()).thenReturn(List.of(global, staff));
|
||||
return new ChannelServiceImpl(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getActive_emptyByDefault() {
|
||||
Player player = player();
|
||||
ChannelService service = createService();
|
||||
assertTrue(service.getActive(player).isEmpty());
|
||||
assertFalse(service.isInChannel(player));
|
||||
}
|
||||
|
||||
@Test
|
||||
void join_nonDefaultChannel() {
|
||||
Player player = player();
|
||||
when(player.hasPermission("lochat.channel.staff")).thenReturn(true);
|
||||
ChannelService service = createService();
|
||||
assertTrue(service.join(player, staff));
|
||||
Optional<Channel> active = service.getActive(player);
|
||||
assertTrue(active.isPresent());
|
||||
assertEquals("staff", active.get().id());
|
||||
assertTrue(service.isInChannel(player));
|
||||
}
|
||||
|
||||
@Test
|
||||
void join_defaultChannelMeansNotJoined() {
|
||||
Player player = player();
|
||||
ChannelService service = createService();
|
||||
assertTrue(service.join(player, global));
|
||||
assertTrue(service.getActive(player).isEmpty());
|
||||
assertFalse(service.isInChannel(player));
|
||||
}
|
||||
|
||||
@Test
|
||||
void join_withoutPermissionFails() {
|
||||
Player player = player();
|
||||
when(player.hasPermission("lochat.channel.staff")).thenReturn(false);
|
||||
ChannelService service = createService();
|
||||
assertFalse(service.join(player, staff));
|
||||
assertFalse(service.isInChannel(player));
|
||||
}
|
||||
|
||||
@Test
|
||||
void join_withPermission() {
|
||||
Player player = player();
|
||||
when(player.hasPermission("lochat.channel.staff")).thenReturn(true);
|
||||
ChannelService service = createService();
|
||||
assertTrue(service.join(player, staff));
|
||||
assertEquals("staff", service.getActive(player).orElseThrow().id());
|
||||
}
|
||||
|
||||
@Test
|
||||
void leave_clearsActiveChannel() {
|
||||
Player player = player();
|
||||
when(player.hasPermission("lochat.channel.staff")).thenReturn(true);
|
||||
ChannelService service = createService();
|
||||
service.join(player, staff);
|
||||
service.leave(player);
|
||||
assertFalse(service.isInChannel(player));
|
||||
assertTrue(service.getActive(player).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void find_byAlias() {
|
||||
ChannelService service = createService();
|
||||
Channel byId = service.find("staff");
|
||||
Channel byAlias = service.find("s");
|
||||
assertEquals("staff", byId.id());
|
||||
assertEquals("staff", byAlias.id());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue