diff --git a/core/src/main/java/tc/oc/pgm/PGMPlugin.java b/core/src/main/java/tc/oc/pgm/PGMPlugin.java index 9335ade09e..13f4372054 100644 --- a/core/src/main/java/tc/oc/pgm/PGMPlugin.java +++ b/core/src/main/java/tc/oc/pgm/PGMPlugin.java @@ -40,6 +40,7 @@ import tc.oc.pgm.api.match.MatchManager; import tc.oc.pgm.api.module.Module; import tc.oc.pgm.api.module.exception.ModuleLoadException; +import tc.oc.pgm.channels.ChatManager; import tc.oc.pgm.command.util.PGMCommandGraph; import tc.oc.pgm.db.CacheDatastore; import tc.oc.pgm.db.SQLDatastore; @@ -98,6 +99,7 @@ public class PGMPlugin extends JavaPlugin implements PGM, Listener { private NameDecorationRegistry nameDecorationRegistry; private ScheduledExecutorService executorService; private ScheduledExecutorService asyncExecutorService; + private ChatManager chatManager; private InventoryManager inventoryManager; private AfkTracker afkTracker; @@ -241,6 +243,8 @@ public void onEnable() { asyncExecutorService.scheduleAtFixedRate(new ShouldRestartTask(), 0, 1, TimeUnit.MINUTES); } + chatManager = new ChatManager(); + registerListeners(); registerCommands(); } @@ -347,6 +351,11 @@ public AfkTracker getAfkTracker() { return afkTracker; } + @Override + public ChatManager getChatManager() { + return chatManager; + } + private void registerCommands() { try { new PGMCommandGraph(this); @@ -383,6 +392,7 @@ private void registerListeners() { registerEvents(new MotdListener()); registerEvents(new ServerPingDataListener(matchManager, mapOrder, getLogger())); registerEvents(new JoinLeaveAnnouncer(matchManager)); + registerEvents(chatManager); } private boolean loadInitialMaps() { diff --git a/core/src/main/java/tc/oc/pgm/api/PGM.java b/core/src/main/java/tc/oc/pgm/api/PGM.java index 6aa52778c2..8ec0eab35a 100644 --- a/core/src/main/java/tc/oc/pgm/api/PGM.java +++ b/core/src/main/java/tc/oc/pgm/api/PGM.java @@ -11,6 +11,7 @@ import tc.oc.pgm.api.map.MapLibrary; import tc.oc.pgm.api.map.MapOrder; import tc.oc.pgm.api.match.MatchManager; +import tc.oc.pgm.channels.ChatManager; import tc.oc.pgm.namedecorations.NameDecorationRegistry; import tc.oc.pgm.tablist.MatchTabManager; import tc.oc.pgm.util.listener.AfkTracker; @@ -43,6 +44,8 @@ public interface PGM extends Plugin { AfkTracker getAfkTracker(); + ChatManager getChatManager(); + AtomicReference GLOBAL = new AtomicReference<>(null); static PGM set(PGM pgm) { diff --git a/core/src/main/java/tc/oc/pgm/api/channels/Channel.java b/core/src/main/java/tc/oc/pgm/api/channels/Channel.java new file mode 100644 index 0000000000..7b273a3f9d --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/api/channels/Channel.java @@ -0,0 +1,223 @@ +package tc.oc.pgm.api.channels; + +import java.util.Collection; +import java.util.List; +import java.util.function.Predicate; +import net.kyori.adventure.text.Component; +import org.bukkit.command.CommandSender; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.key.CloudKey; +import org.incendo.cloud.meta.CommandMeta; +import org.incendo.cloud.parser.standard.StringParser; +import org.incendo.cloud.suggestion.SuggestionProvider; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.event.ChannelMessageEvent; +import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.api.setting.SettingValue; +import tc.oc.pgm.util.Audience; +import tc.oc.pgm.util.Players; + +/** + * Represents a communication channel for handling chat messages + * + * @param the type of the target associated with the channel + */ +public interface Channel { + + CloudKey MESSAGE_KEY = CloudKey.of("message", String.class); + + /** + * Gets the display name of the channel, defaulting to the first alias. + * + * @return the channel's display name + */ + default String getDisplayName() { + return getAliases().getFirst(); + } + + /** + * Retrieves the list of aliases for the channel. + * + * @return list of channel aliases + */ + List getAliases(); + + /** + * A character used as a shortcut prefix in messages to specify the target channel. For instance, + * a `!` at the start of a message may indicate global channel messaging, or {@code null} if none. + * + * @return the shortcut character + */ + @Nullable + default Character getShortcut() { + return null; + } + + /** + * Retrieves the channel's setting value, or {@code null} if none. + * + * @return the setting value + */ + default SettingValue getSetting() { + return null; + } + + /** + * Retrieves the format these channel messages should be logged using via the + * {@code AsyncPlayerChatEvent}. + * + * @param target the message target + * @return formatted messaged printed to console + */ + default String getLoggerFormat(T target) { + return "<%s>: %s"; + } + + /** + * If the channel supports message redirection, where messages may be forwarded to another channel + * or location e.g. team messages might be redirected to global chat post match end. + * + * @return {@code true} if redirection is supported, {@code false} otherwise + */ + default boolean supportsRedirect() { + return false; + } + + /** + * Checks if a player has permission to send a message in the channel. + * + * @param sender the player sending the message + * @return {@code true} if the player has permission + */ + default boolean canSendMessage(MatchPlayer sender) { + return true; + } + + /** + * Retrieves the channel target based on the sender and command context. + * + * @param sender the sender player + * @param arguments command arguments + * @return the target of the channel + */ + T getTarget(MatchPlayer sender, CommandContext arguments); + + /** + * Gets the collection of players viewing messages in this channel for the given target. + * + * @param target the message target + * @return collection of viewers + */ + Collection getViewers(T target); + + /** + * Gets the players viewing broadcast messages for the given target. + * + * @param target the target audience + * @return collection of broadcast viewers + */ + default Collection getBroadcastViewers(T target) { + return getViewers(target); + } + + /** + * Called when a message is sent; allows for additional actions (e.g., sound feedback). + * + * @param event the message event + */ + default void messageSent(final ChannelMessageEvent event) {} + + /** + * Formats a message for this channel. + * + * @param target message target + * @param sender the sender player (optional) + * @param message the content to format + * @return the formatted message + */ + Component formatMessage(T target, @Nullable MatchPlayer sender, Component message); + + /** + * Registers commands using all channel aliases by default unless overrode. + * + * @param manager command manager for command registration + */ + default void registerCommand(CommandManager manager) { + List aliases = getAliases(); + if (aliases.isEmpty()) return; + + manager.command(manager + .commandBuilder(aliases.getFirst(), aliases.subList(1, aliases.size()), CommandMeta.empty()) + .optional( + MESSAGE_KEY, + StringParser.greedyStringParser(), + SuggestionProvider.blockingStrings(Players::suggestPlayers)) + .handler(context -> { + MatchPlayer sender = + context.inject(MatchPlayer.class).orElseThrow(IllegalStateException::new); + + if (context.contains(MESSAGE_KEY)) { + PGM.get().getChatManager().process(this, sender, context); + } else { + PGM.get().getChatManager().setChannel(sender, this); + } + })); + } + + /** + * Processes a player's chat message in this channel from a {@code AsyncPlayerChatEvent}. Adding + * any context required to the command context store. + * + * @param sender the player + * @param message the message content + * @param context the command context + */ + default void processChatMessage( + MatchPlayer sender, String message, CommandContext context) { + context.store(MESSAGE_KEY, message); + } + + /** + * Processes a chat message from a {@code AsyncPlayerChatEvent} with a shortcut character. Adding + * any context required to the command context store. + * + * @param sender the player + * @param message the message starting with a shortcut + * @param context the command context + */ + default void processChatShortcut( + MatchPlayer sender, String message, CommandContext context) { + if (message.length() == 1) { + PGM.get().getChatManager().setChannel(sender, this); + return; + } + + context.store(Channel.MESSAGE_KEY, message.substring(1).trim()); + } + + /** + * Broadcasts a message to all viewers of the target. + * + * @param component the message to broadcast + * @param target the target audience + */ + default void broadcastMessage(Component component, T target) { + broadcastMessage(component, target, player -> true); + } + + /** + * Broadcasts a message to viewers, filtered by a predicate. + * + * @param component the message to broadcast + * @param target the target audience + * @param filter predicate to filter recipients + */ + default void broadcastMessage(Component component, T target, Predicate filter) { + Collection viewers = getBroadcastViewers(target); + Component finalMessage = formatMessage(target, null, component); + viewers.stream().filter(filter).forEach(player -> player.sendMessage(finalMessage)); + Audience.console().sendMessage(finalMessage); + } +} diff --git a/core/src/main/java/tc/oc/pgm/api/event/ChannelMessageEvent.java b/core/src/main/java/tc/oc/pgm/api/event/ChannelMessageEvent.java new file mode 100644 index 0000000000..dc44b0c7b3 --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/api/event/ChannelMessageEvent.java @@ -0,0 +1,81 @@ +package tc.oc.pgm.api.event; + +import static net.kyori.adventure.text.Component.text; + +import java.util.Collection; +import net.kyori.adventure.text.Component; +import org.bukkit.event.HandlerList; +import tc.oc.pgm.api.channels.Channel; +import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.util.event.PreemptiveEvent; + +public class ChannelMessageEvent extends PreemptiveEvent { + + private final Channel channel; + private final MatchPlayer sender; + private final T target; + private Collection viewers; + private String message; + private Component component; + + public ChannelMessageEvent( + Channel channel, + MatchPlayer sender, + T target, + Collection viewers, + String message) { + this.channel = channel; + this.sender = sender; + this.target = target; + this.viewers = viewers; + this.message = message; + } + + public Channel getChannel() { + return channel; + } + + public MatchPlayer getSender() { + return sender; + } + + public T getTarget() { + return target; + } + + public Collection getViewers() { + return viewers; + } + + public void setViewers(Collection viewers) { + this.viewers = viewers; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + this.component = null; + } + + public Component getComponent() { + return (component != null) ? component : text(message); + } + + public void setComponent(Component component) { + this.component = component; + } + + private static final HandlerList handlers = new HandlerList(); + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/core/src/main/java/tc/oc/pgm/api/integration/Integration.java b/core/src/main/java/tc/oc/pgm/api/integration/Integration.java index aa1115c6fa..d83a2367e9 100644 --- a/core/src/main/java/tc/oc/pgm/api/integration/Integration.java +++ b/core/src/main/java/tc/oc/pgm/api/integration/Integration.java @@ -3,10 +3,14 @@ import static tc.oc.pgm.util.Assert.assertNotNull; import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import org.bukkit.entity.Player; import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.api.channels.Channel; import tc.oc.pgm.api.player.MatchPlayer; public final class Integration { @@ -23,6 +27,7 @@ private Integration() {} new AtomicReference(new NoopVanishIntegration()); private static final AtomicReference SQUAD = new AtomicReference<>(new NoopSquadIntegration()); + private static Set> CHANNELS = new HashSet<>(); public static void setFriendIntegration(FriendIntegration integration) { FRIENDS.set(assertNotNull(integration)); @@ -44,6 +49,13 @@ public static void setSquadIntegration(SquadIntegration integration) { SQUAD.set(assertNotNull(integration)); } + public static void registerChannel(Channel channel) { + if (CHANNELS == null) + throw new IllegalStateException( + "New channels cannot be registered after ChatManager has been initialised!"); + CHANNELS.add(assertNotNull(channel)); + } + public static boolean isFriend(Player a, Player b) { return FRIENDS.get().isFriend(a, b); } @@ -83,6 +95,12 @@ public static boolean isDisguised(Player player) { return isVanished(player) || getNick(player) != null; } + public static Set> pollRegisteredChannels() { + Set> channels = CHANNELS; + CHANNELS = null; + return Collections.unmodifiableSet(channels); + } + // No-op Implementations private static class NoopFriendIntegration implements FriendIntegration { diff --git a/core/src/main/java/tc/oc/pgm/api/setting/SettingKey.java b/core/src/main/java/tc/oc/pgm/api/setting/SettingKey.java index 52dc3f1da1..4209d063c3 100644 --- a/core/src/main/java/tc/oc/pgm/api/setting/SettingKey.java +++ b/core/src/main/java/tc/oc/pgm/api/setting/SettingKey.java @@ -9,6 +9,7 @@ import java.util.List; import org.bukkit.Material; import org.jetbrains.annotations.NotNull; +import tc.oc.pgm.api.PGM; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.modules.PlayerTimeMatchModule; import tc.oc.pgm.util.Aliased; @@ -20,12 +21,12 @@ * @see SettingValue */ public enum SettingKey implements Aliased { - CHAT( - "chat", - Materials.SIGN, - CHAT_TEAM, - CHAT_GLOBAL, - CHAT_ADMIN), // Changes the default chat channel + CHAT("chat", Materials.SIGN, CHAT_TEAM, CHAT_GLOBAL, CHAT_ADMIN) { + @Override + public void update(MatchPlayer player) { + PGM.get().getChatManager().setChannel(player, player.getSettings().getValue(CHAT)); + } + }, // Changes the default chat channel DEATH( Arrays.asList("death", "dms"), Materials.SKULL, @@ -87,7 +88,6 @@ public void update(MatchPlayer player) { PlayerTimeMatchModule.updatePlayerTime(player); } }; // Changes player preference for time of day - ; private final List aliases; private final SettingValue[] values; diff --git a/core/src/main/java/tc/oc/pgm/channels/AdminChannel.java b/core/src/main/java/tc/oc/pgm/channels/AdminChannel.java new file mode 100644 index 0000000000..acd3dd440d --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/channels/AdminChannel.java @@ -0,0 +1,105 @@ +package tc.oc.pgm.channels; + +import static net.kyori.adventure.text.Component.empty; +import static net.kyori.adventure.text.Component.text; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; +import org.incendo.cloud.context.CommandContext; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.Permissions; +import tc.oc.pgm.api.channels.Channel; +import tc.oc.pgm.api.event.ChannelMessageEvent; +import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.api.setting.SettingKey; +import tc.oc.pgm.api.setting.SettingValue; +import tc.oc.pgm.util.bukkit.Sounds; +import tc.oc.pgm.util.named.NameStyle; + +public class AdminChannel implements Channel { + + private static final List ALIASES = List.of("a"); + + public static final TextComponent PREFIX = text() + .append(text("[", NamedTextColor.WHITE)) + .append(text("A", NamedTextColor.GOLD)) + .append(text("] ", NamedTextColor.WHITE)) + .build(); + + @Override + public String getDisplayName() { + return "admin"; + } + + @Override + public List getAliases() { + return ALIASES; + } + + @Override + public Character getShortcut() { + return '$'; + } + + @Override + public SettingValue getSetting() { + return SettingValue.CHAT_ADMIN; + } + + @Override + public String getLoggerFormat(Void target) { + return "[A] %s: %s"; + } + + @Override + public boolean canSendMessage(MatchPlayer sender) { + return sender.getBukkit().hasPermission(Permissions.ADMINCHAT); + } + + @Override + public Void getTarget(MatchPlayer sender, CommandContext arguments) { + return null; + } + + @Override + public Collection getViewers(Void unused) { + Set players = new HashSet<>(); + PGM.get().getMatchManager().getMatches().forEachRemaining(match -> { + for (MatchPlayer player : match.getPlayers()) + if (player.getBukkit().hasPermission(Permissions.ADMINCHAT)) players.add(player); + }); + return players; + } + + @Override + public void messageSent(ChannelMessageEvent event) { + for (MatchPlayer viewer : event.getViewers()) { + if (viewer.equals(event.getSender())) continue; + SettingValue value = viewer.getSettings().getValue(SettingKey.SOUNDS); + if (value.equals(SettingValue.SOUNDS_ALL) || value.equals(SettingValue.SOUNDS_CHAT)) + viewer.playSound(Sounds.ADMIN_CHAT); + } + } + + @Override + public Component formatMessage(Void target, @Nullable MatchPlayer sender, Component message) { + return text() + .append(PREFIX) + .append( + sender != null + ? text() + .append(sender.getName(NameStyle.VERBOSE)) + .append(text(": ", NamedTextColor.WHITE)) + .build() + : empty()) + .append(message) + .build(); + } +} diff --git a/core/src/main/java/tc/oc/pgm/channels/ChatManager.java b/core/src/main/java/tc/oc/pgm/channels/ChatManager.java new file mode 100644 index 0000000000..1e9251cee1 --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/channels/ChatManager.java @@ -0,0 +1,338 @@ +package tc.oc.pgm.channels; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.Component.translatable; +import static tc.oc.pgm.util.text.TextException.exception; +import static tc.oc.pgm.util.text.TextException.noPermission; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerChatTabCompleteEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.key.CloudKey; +import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.channels.Channel; +import tc.oc.pgm.api.event.ChannelMessageEvent; +import tc.oc.pgm.api.integration.Integration; +import tc.oc.pgm.api.party.Party; +import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.api.setting.SettingKey; +import tc.oc.pgm.api.setting.SettingValue; +import tc.oc.pgm.api.setting.Settings; +import tc.oc.pgm.ffa.Tribute; +import tc.oc.pgm.util.Audience; +import tc.oc.pgm.util.Players; +import tc.oc.pgm.util.bukkit.OnlinePlayerUUIDMapAdapter; +import tc.oc.pgm.util.text.TextException; + +public class ChatManager implements Listener { + + CloudKey ORIGINAL_EVENT_KEY = + CloudKey.of("event", AsyncPlayerChatEvent.class); + + private static final Cache CHAT_EVENT_CACHE = + CacheBuilder.newBuilder() + .weakKeys() + .expireAfterWrite(15, TimeUnit.SECONDS) + .build(); + + private final GlobalChannel globalChannel; + private final AdminChannel adminChannel; + private final TeamChannel teamChannel; + private final Set> channels; + private final Map> shortcuts; + private final OnlinePlayerUUIDMapAdapter> selectedChannel; + + private CommandManager manager; + + public ChatManager() { + this.channels = new HashSet<>(); + this.channels.add(globalChannel = new GlobalChannel()); + this.channels.add(adminChannel = new AdminChannel()); + this.channels.add(teamChannel = new TeamChannel()); + this.channels.add(new PrivateMessageChannel()); + this.channels.addAll(Integration.pollRegisteredChannels()); + + this.shortcuts = new HashMap<>(); + for (Channel channel : channels) { + if (channel.getShortcut() == null) continue; + + this.shortcuts.putIfAbsent(channel.getShortcut(), channel); + } + + this.selectedChannel = new OnlinePlayerUUIDMapAdapter<>(PGM.get()); + } + + public void registerCommands(CommandManager manager) { + this.manager = manager; + + for (Channel channel : PGM.get().getChatManager().getChannels()) { + channel.registerCommand(manager); + } + } + + public boolean processChat(MatchPlayer sender, AsyncPlayerChatEvent event) { + final String message = event.getMessage().trim(); + if (message.isEmpty()) return false; + + CommandContext context = new CommandContext<>(sender.getBukkit(), manager); + context.store(ORIGINAL_EVENT_KEY, event); + + Channel channel = shortcuts.get(message.charAt(0)); + + if (channel != null && channel.canSendMessage(sender)) { + channel.processChatShortcut(sender, message, context); + context.optional(Channel.MESSAGE_KEY).ifPresent(event::setMessage); + } + + if (channel == null) { + channel = getSelectedChannel(sender); + channel.processChatMessage(sender, message, context); + } + + if (context.contains(Channel.MESSAGE_KEY)) { + return process(channel, sender, context); + } + + return false; + } + + public boolean process( + Channel channel, MatchPlayer sender, CommandContext context) { + return processChannelMessage( + calculateChannelRedirect(channel, sender, context), sender, context); + } + + private boolean processChannelMessage( + Channel channel, MatchPlayer sender, CommandContext context) { + if (!channel.canSendMessage(sender)) throw noPermission(); + throwMuted(sender); + + T target = channel.getTarget(sender, context); + Collection viewers = channel.getViewers(target); + + final AsyncPlayerChatEvent asyncEvent = new AsyncPlayerChatEvent( + false, + sender.getBukkit(), + context.get(Channel.MESSAGE_KEY), + viewers.stream().map(MatchPlayer::getBukkit).collect(Collectors.toSet())); + + CHAT_EVENT_CACHE.put(asyncEvent, true); + sender.getMatch().callEvent(asyncEvent); + if (asyncEvent.isCancelled()) return false; + + final ChannelMessageEvent event = + new ChannelMessageEvent<>(channel, sender, target, viewers, asyncEvent.getMessage()); + + sender.getMatch().callEvent(event); + + if (event.isCancelled()) { + if (event.getSender() != null && event.getCancellationReason() != null) { + event.getSender().sendWarning(event.getCancellationReason()); + } + return false; + } + + Component finalMessage = event + .getChannel() + .formatMessage(event.getTarget(), event.getSender(), event.getComponent()); + event.getViewers().forEach(player -> player.sendMessage(finalMessage)); + + channel.messageSent(event); + + String logFormat = "[CHAT] " + channel.getLoggerFormat(target); + context.optional(ORIGINAL_EVENT_KEY).ifPresentOrElse(e -> e.setFormat(logFormat), () -> { + String message = String.format( + logFormat, sender.getBukkit().getDisplayName(), context.get(Channel.MESSAGE_KEY)); + Audience.console().sendMessage(text(message)); + }); + + return true; + } + + private Channel calculateChannelRedirect( + Channel channel, MatchPlayer sender, CommandContext context) { + if (Integration.isVanished(sender.getBukkit()) && !(channel instanceof AdminChannel)) { + // Allow private messaging with players who can see each other + if (channel instanceof PrivateMessageChannel pmc && pmc.canSendVanished(sender, context)) { + return channel; + } + + if (!channel.supportsRedirect()) throw exception("vanish.chat.deny"); + + return adminChannel; + } + + // Try to use global chat when a match has ended + if (channel.supportsRedirect() + && (sender.getMatch().isFinished() || sender.getParty() instanceof Tribute)) { + if (channel instanceof TeamChannel) return globalChannel; + } + + return channel; + } + + private void throwMuted(MatchPlayer player) { + if (!Integration.isMuted(player.getBukkit())) return; + Optional muteReason = + Optional.ofNullable(Integration.getMuteReason(player.getBukkit())); + Component reason = + muteReason.isPresent() ? text(muteReason.get()) : translatable("moderation.mute.noReason"); + + throw exception("moderation.mute.message", reason.color(NamedTextColor.AQUA)); + } + + @EventHandler + public void onPlayerTabComplete(PlayerChatTabCompleteEvent event) { + if (event.getChatMessage().trim().equals(event.getLastToken())) { + char first = event.getLastToken().charAt(0); + if (shortcuts.containsKey(first)) { + List suggestions = + Players.getPlayerNames(event.getPlayer(), event.getLastToken().substring(1)); + suggestions.replaceAll(s -> first + s); + + event.getTabCompletions().addAll(suggestions); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerJoin(final PlayerJoinEvent event) { + MatchPlayer player = PGM.get().getMatchManager().getPlayer(event.getPlayer()); + if (player == null) return; + selectedChannel.put( + player.getId(), findChannelBySetting(player.getSettings().getValue(SettingKey.CHAT))); + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) + public void onChat(AsyncPlayerChatEvent event) { + if (CHAT_EVENT_CACHE.getIfPresent(event) != null) { + // PGM created chat event, ignore it + CHAT_EVENT_CACHE.invalidate(event); + return; + } + + event.getRecipients().clear(); + + final MatchPlayer player = PGM.get().getMatchManager().getPlayer(event.getPlayer()); + if (player == null) return; + + Runnable completion = () -> { + try { + boolean sent = processChat(player, event); + if (!sent) event.setCancelled(true); + } catch (TextException e) { + // Allow sub-handlers to throw command exceptions just fine + event.setCancelled(true); + player.sendWarning(e); + } + }; + + if (Bukkit.isPrimaryThread()) completion.run(); + else { + try { + PGM.get().getExecutor().submit(completion).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + } + + private Channel findChannelBySetting(SettingValue setting) { + for (Channel channel : channels) { + if (setting == channel.getSetting()) return channel; + } + + return teamChannel; + } + + public void setChannel(MatchPlayer player, SettingValue value) { + selectedChannel.put(player.getId(), findChannelBySetting(value)); + } + + public void setChannel(MatchPlayer player, Channel channel) { + Channel previous = selectedChannel.put(player.getId(), channel); + + if (channel.getSetting() != null) { + Settings setting = player.getSettings(); + final SettingValue old = setting.getValue(SettingKey.CHAT); + + if (old != channel.getSetting()) { + setting.setValue(SettingKey.CHAT, channel.getSetting()); + } + } + + if (previous != null && previous != channel) { + player.sendMessage(translatable( + "setting.set", + text("chat"), + text(previous.getDisplayName(), NamedTextColor.GRAY), + text(channel.getDisplayName(), NamedTextColor.GREEN))); + } else { + player.sendMessage(translatable( + "setting.get", text("chat"), text(channel.getDisplayName(), NamedTextColor.GREEN))); + } + } + + public Channel getSelectedChannel(MatchPlayer player) { + return selectedChannel.getOrDefault(player.getId(), globalChannel); + } + + public Set> getChannels() { + return channels; + } + + public GlobalChannel getGlobalChannel() { + return globalChannel; + } + + public AdminChannel getAdminChannel() { + return adminChannel; + } + + public TeamChannel getTeamChannel() { + return teamChannel; + } + + public static void broadcastMessage(Component message) { + PGM.get().getChatManager().globalChannel.broadcastMessage(message, null); + } + + public static void broadcastMessage(Component message, Predicate filter) { + PGM.get().getChatManager().globalChannel.broadcastMessage(message, null, filter); + } + + public static void broadcastAdminMessage(Component message) { + PGM.get().getChatManager().adminChannel.broadcastMessage(message, null); + } + + public static void broadcastPartyMessage(Component message, Party party) { + PGM.get().getChatManager().teamChannel.broadcastMessage(message, party); + } + + public static void broadcastPartyMessage( + Component message, Party party, Predicate filter) { + PGM.get().getChatManager().teamChannel.broadcastMessage(message, party, filter); + } +} diff --git a/core/src/main/java/tc/oc/pgm/channels/GlobalChannel.java b/core/src/main/java/tc/oc/pgm/channels/GlobalChannel.java new file mode 100644 index 0000000000..cd65a145a4 --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/channels/GlobalChannel.java @@ -0,0 +1,74 @@ +package tc.oc.pgm.channels; + +import static net.kyori.adventure.text.Component.text; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; +import org.incendo.cloud.context.CommandContext; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.channels.Channel; +import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.api.setting.SettingValue; +import tc.oc.pgm.util.named.NameStyle; + +public class GlobalChannel implements Channel { + + private static final List ALIASES = List.of("g", "all", "shout"); + + @Override + public String getDisplayName() { + return "global"; + } + + @Override + public List getAliases() { + return ALIASES; + } + + @Override + public Character getShortcut() { + return '!'; + } + + @Override + public SettingValue getSetting() { + return SettingValue.CHAT_GLOBAL; + } + + @Override + public boolean supportsRedirect() { + return true; + } + + @Override + public Void getTarget(MatchPlayer sender, CommandContext arguments) { + return null; + } + + @Override + public Collection getViewers(Void unused) { + Set players = new HashSet<>(); + PGM.get() + .getMatchManager() + .getMatches() + .forEachRemaining(match -> players.addAll(match.getPlayers())); + return players; + } + + @Override + public Component formatMessage(Void target, @Nullable MatchPlayer sender, Component message) { + if (sender == null) return message; + return text() + .append(text("<", NamedTextColor.WHITE)) + .append(sender.getName(NameStyle.VERBOSE)) + .append(text(">: ", NamedTextColor.WHITE)) + .append(message) + .build(); + } +} diff --git a/core/src/main/java/tc/oc/pgm/channels/PrivateMessageChannel.java b/core/src/main/java/tc/oc/pgm/channels/PrivateMessageChannel.java new file mode 100644 index 0000000000..43a340ff97 --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/channels/PrivateMessageChannel.java @@ -0,0 +1,248 @@ +package tc.oc.pgm.channels; + +import static net.kyori.adventure.text.Component.space; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.Component.translatable; +import static net.kyori.adventure.text.event.ClickEvent.runCommand; +import static org.incendo.cloud.parser.standard.StringParser.greedyStringParser; +import static tc.oc.pgm.util.text.TextException.exception; +import static tc.oc.pgm.util.text.TextException.usage; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.command.CommandSender; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.component.CommandComponent; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.key.CloudKey; +import org.incendo.cloud.suggestion.SuggestionProvider; +import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.Permissions; +import tc.oc.pgm.api.channels.Channel; +import tc.oc.pgm.api.event.ChannelMessageEvent; +import tc.oc.pgm.api.integration.Integration; +import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.api.setting.SettingKey; +import tc.oc.pgm.api.setting.SettingValue; +import tc.oc.pgm.util.MessageSenderIdentity; +import tc.oc.pgm.util.Players; +import tc.oc.pgm.util.bukkit.OnlinePlayerUUIDMapAdapter; +import tc.oc.pgm.util.bukkit.Sounds; +import tc.oc.pgm.util.named.NameStyle; + +public class PrivateMessageChannel implements Channel { + + private static final List ALIASES = List.of("msg", "tell", "r"); + + private static final CloudKey TARGET_KEY = CloudKey.of("target", MatchPlayer.class); + + private final OnlinePlayerUUIDMapAdapter selectedPlayer, lastMessagedBy; + + public PrivateMessageChannel() { + this.selectedPlayer = new OnlinePlayerUUIDMapAdapter<>(PGM.get()); + this.lastMessagedBy = new OnlinePlayerUUIDMapAdapter<>(PGM.get()); + } + + @Override + public String getDisplayName() { + return "private messages"; + } + + @Override + public List getAliases() { + return ALIASES; + } + + @Override + public Character getShortcut() { + return '@'; + } + + @Override + public String getLoggerFormat(MatchPlayer target) { + return "(DM) %s -> " + target.getBukkit().getDisplayName() + ": %s"; + } + + @Override + public MatchPlayer getTarget(MatchPlayer sender, CommandContext arguments) { + MatchPlayer target = arguments.get(TARGET_KEY); + // Check the user can message the target + checkPermissions(target, sender); + return target; + } + + @Override + public Collection getViewers(MatchPlayer target) { + if (target == null) throw exception("command.playerNotFound"); + return Collections.singletonList(target); + } + + @Override + public void messageSent(ChannelMessageEvent event) { + MatchPlayer sender = Objects.requireNonNull(event.getSender()); + MatchPlayer target = event.getTarget(); + + sender.sendMessage(formatMessage(target, "to", event.getComponent())); + + SettingValue value = target.getSettings().getValue(SettingKey.SOUNDS); + if (value.equals(SettingValue.SOUNDS_ALL) + || value.equals(SettingValue.SOUNDS_CHAT) + || value.equals(SettingValue.SOUNDS_DM)) target.playSound(Sounds.DIRECT_MESSAGE); + + setTarget(lastMessagedBy, sender, target); + setTarget(lastMessagedBy, target, sender); + } + + @Override + public Component formatMessage(MatchPlayer target, MatchPlayer sender, Component message) { + return formatMessage(sender, "from", message); + } + + public Component formatMessage(MatchPlayer player, String direction, Component message) { + return text() + .append(translatable("misc." + direction, NamedTextColor.GRAY, TextDecoration.ITALIC)) + .append(space()) + .append(player.getName(NameStyle.VERBOSE)) + .append(text(": ", NamedTextColor.WHITE)) + .append(message) + .build(); + } + + @Override + public void registerCommand(CommandManager manager) { + manager.command(manager + .commandBuilder("msg", "tell") + .required(CommandComponent.builder() + .key(TARGET_KEY) + .commandManager(manager)) + .optional( + MESSAGE_KEY, + greedyStringParser(), + SuggestionProvider.blockingStrings(Players::suggestPlayers)) + .handler(context -> { + MatchPlayer sender = + context.inject(MatchPlayer.class).orElseThrow(IllegalStateException::new); + + if (!context.contains(MESSAGE_KEY)) { + setTarget(selectedPlayer, sender, context.get(TARGET_KEY)); + PGM.get().getChatManager().setChannel(sender, this); + } else { + PGM.get().getChatManager().process(this, sender, context); + } + })); + + manager.command(manager + .commandBuilder("reply", "r") + .optional( + MESSAGE_KEY, + greedyStringParser(), + SuggestionProvider.blockingStrings(Players::suggestPlayers)) + .handler(context -> { + MatchPlayer sender = + context.inject(MatchPlayer.class).orElseThrow(IllegalStateException::new); + MatchPlayer target = getTarget(lastMessagedBy, sender); + if (target == null) throw exception("command.message.noReply", text("/msg")); + + if (!context.contains(MESSAGE_KEY)) { + setTarget(selectedPlayer, sender, target); + PGM.get().getChatManager().setChannel(sender, this); + } else { + context.store(TARGET_KEY, target); + PGM.get().getChatManager().process(this, sender, context); + } + })); + } + + @Override + public void processChatMessage( + MatchPlayer sender, String message, CommandContext context) { + Channel.super.processChatMessage(sender, message, context); + MatchPlayer target = getTarget(selectedPlayer, sender); + if (target != null) context.store(TARGET_KEY, target); + } + + @Override + public void processChatShortcut( + MatchPlayer sender, String message, CommandContext context) { + if (message.length() == 1) throw usage(getShortcut() + " [message]"); + + int spaceIndex = message.indexOf(' '); + MatchPlayer target = Players.getMatchPlayer( + sender.getBukkit(), message.substring(1, spaceIndex == -1 ? message.length() : spaceIndex)); + if (target == null) throw exception("command.playerNotFound"); + + if (spaceIndex == -1) { + setTarget(selectedPlayer, sender, target); + PGM.get().getChatManager().setChannel(sender, this); + return; + } + + context.store(MESSAGE_KEY, message.substring(spaceIndex + 1).trim()); + context.store(TARGET_KEY, target); + } + + private void checkPermissions(MatchPlayer target, MatchPlayer sender) { + if (sender.equals(target)) throw exception("command.message.self"); + + SettingValue option = sender.getSettings().getValue(SettingKey.MESSAGE); + if (option.equals(SettingValue.MESSAGE_OFF)) + throw exception( + "command.message.disabled", + text("/toggle dm", NamedTextColor.RED).clickEvent(runCommand("/toggle dm"))); + if (option.equals(SettingValue.MESSAGE_FRIEND) + && !Integration.isFriend(target.getBukkit(), sender.getBukkit())) + throw exception( + "command.message.disabled", + text("/toggle dm", NamedTextColor.RED).clickEvent(runCommand("/toggle dm"))); + + option = target.getSettings().getValue(SettingKey.MESSAGE); + if (!sender.getBukkit().hasPermission(Permissions.STAFF)) { + if (option.equals(SettingValue.MESSAGE_OFF)) + throw exception("command.message.blocked", target.getName()); + + if (option.equals(SettingValue.MESSAGE_FRIEND) + && !Integration.isFriend(target.getBukkit(), sender.getBukkit())) + throw exception("command.message.friendsOnly", target.getName()); + + if (Integration.isMuted(target.getBukkit())) + throw exception("moderation.mute.target", target.getName()); + } + } + + public MatchPlayer getTarget( + OnlinePlayerUUIDMapAdapter store, MatchPlayer sender) { + MessageSenderIdentity targetIdentity = store.get(sender.getId()); + if (targetIdentity == null) return null; + + MatchPlayer target = targetIdentity.getPlayer(sender.getBukkit()); + if (target != null) checkPermissions(target, sender); + + return target; + } + + public void setTarget( + OnlinePlayerUUIDMapAdapter store, + MatchPlayer sender, + MatchPlayer target) { + checkPermissions(target, sender); + store.compute(sender.getId(), (uuid, identity) -> { + if (identity != null && identity.getPlayer(sender.getBukkit()) == target) { + return identity; + } + + return new MessageSenderIdentity(sender.getBukkit(), target.getBukkit()); + }); + } + + public boolean canSendVanished(MatchPlayer sender, CommandContext context) { + return context + .optional(TARGET_KEY) + .map(target -> Players.isVisible(target.getBukkit(), sender.getBukkit())) + .orElse(false); + } +} diff --git a/core/src/main/java/tc/oc/pgm/channels/TeamChannel.java b/core/src/main/java/tc/oc/pgm/channels/TeamChannel.java new file mode 100644 index 0000000000..7bd47d7548 --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/channels/TeamChannel.java @@ -0,0 +1,84 @@ +package tc.oc.pgm.channels; + +import static net.kyori.adventure.text.Component.empty; +import static net.kyori.adventure.text.Component.text; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; +import org.incendo.cloud.context.CommandContext; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.api.Permissions; +import tc.oc.pgm.api.channels.Channel; +import tc.oc.pgm.api.party.Party; +import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.api.setting.SettingValue; +import tc.oc.pgm.util.named.NameStyle; + +public class TeamChannel implements Channel { + + private static final List ALIASES = List.of("t"); + + @Override + public String getDisplayName() { + return "team"; + } + + @Override + public List getAliases() { + return ALIASES; + } + + @Override + public SettingValue getSetting() { + return SettingValue.CHAT_TEAM; + } + + @Override + public String getLoggerFormat(Party target) { + return "(" + target.getNameLegacy() + ") %s: %s"; + } + + @Override + public boolean supportsRedirect() { + return true; + } + + @Override + public Party getTarget(MatchPlayer sender, CommandContext arguments) { + return sender.getParty(); + } + + @Override + public Collection getViewers(Party target) { + return target.getMatch().getPlayers().stream() + .filter(viewer -> target.equals(viewer.getParty()) + || (viewer.isObserving() && viewer.getBukkit().hasPermission(Permissions.ADMINCHAT))) + .collect(Collectors.toList()); + } + + @Override + public Collection getBroadcastViewers(Party target) { + return target.getMatch().getPlayers().stream() + .filter(viewer -> target.equals(viewer.getParty()) || viewer.isObserving()) + .collect(Collectors.toList()); + } + + @Override + public Component formatMessage(Party target, @Nullable MatchPlayer sender, Component message) { + return text() + .append(target.getChatPrefix()) + .append( + sender != null + ? text() + .append(sender.getName(NameStyle.VERBOSE)) + .append(text(": ", NamedTextColor.WHITE)) + .build() + : empty()) + .append(message) + .build(); + } +} diff --git a/core/src/main/java/tc/oc/pgm/command/CancelCommand.java b/core/src/main/java/tc/oc/pgm/command/CancelCommand.java index 96aa4d7eaf..3a7b244ece 100644 --- a/core/src/main/java/tc/oc/pgm/command/CancelCommand.java +++ b/core/src/main/java/tc/oc/pgm/command/CancelCommand.java @@ -10,7 +10,7 @@ import org.incendo.cloud.annotations.Permission; import tc.oc.pgm.api.Permissions; import tc.oc.pgm.api.match.Match; -import tc.oc.pgm.listeners.ChatDispatcher; +import tc.oc.pgm.channels.ChatManager; import tc.oc.pgm.restart.CancelRestartEvent; import tc.oc.pgm.restart.RestartManager; import tc.oc.pgm.start.StartMatchModule; @@ -39,7 +39,7 @@ public void cancel(CommandSender sender, Audience audience, Match match) { match.getCountdown().cancelAll(); match.needModule(StartMatchModule.class).setAutoStart(false); - ChatDispatcher.broadcastAdminChatMessage( - translatable("admin.cancelCountdowns.announce", player(sender, NameStyle.FANCY)), match); + ChatManager.broadcastAdminMessage( + translatable("admin.cancelCountdowns.announce", player(sender, NameStyle.FANCY))); } } diff --git a/core/src/main/java/tc/oc/pgm/command/FreeForAllCommand.java b/core/src/main/java/tc/oc/pgm/command/FreeForAllCommand.java index d3be7c8e24..3bc75b5f9c 100644 --- a/core/src/main/java/tc/oc/pgm/command/FreeForAllCommand.java +++ b/core/src/main/java/tc/oc/pgm/command/FreeForAllCommand.java @@ -13,8 +13,8 @@ import org.incendo.cloud.annotations.Permission; import tc.oc.pgm.api.Permissions; import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.channels.ChatManager; import tc.oc.pgm.ffa.FreeForAllMatchModule; -import tc.oc.pgm.listeners.ChatDispatcher; import tc.oc.pgm.util.named.NameStyle; import tc.oc.pgm.util.text.TextParser; @@ -86,12 +86,10 @@ public void max(Match match, CommandSender sender, FreeForAllMatchModule ffa) { } private void sendResizedMessage(Match match, CommandSender sender, String type, int value) { - ChatDispatcher.broadcastAdminChatMessage( - translatable( - "match.resize.announce." + type, - player(sender, NameStyle.FANCY), - translatable("match.info.players", NamedTextColor.YELLOW), - text(value, NamedTextColor.AQUA)), - match); + ChatManager.broadcastAdminMessage(translatable( + "match.resize.announce." + type, + player(sender, NameStyle.FANCY), + translatable("match.info.players", NamedTextColor.YELLOW), + text(value, NamedTextColor.AQUA))); } } diff --git a/core/src/main/java/tc/oc/pgm/command/MapOrderCommand.java b/core/src/main/java/tc/oc/pgm/command/MapOrderCommand.java index 2e37ab821a..6ec4d853bb 100644 --- a/core/src/main/java/tc/oc/pgm/command/MapOrderCommand.java +++ b/core/src/main/java/tc/oc/pgm/command/MapOrderCommand.java @@ -23,7 +23,7 @@ import tc.oc.pgm.api.map.MapInfo; import tc.oc.pgm.api.map.MapOrder; import tc.oc.pgm.api.match.Match; -import tc.oc.pgm.listeners.ChatDispatcher; +import tc.oc.pgm.channels.ChatManager; import tc.oc.pgm.restart.RestartManager; import tc.oc.pgm.util.Audience; import tc.oc.pgm.util.UsernameFormatUtils; @@ -69,13 +69,11 @@ public void setNext( if (mapOrder.getNextMap() != null) { Component mapName = mapOrder.getNextMap().getStyledName(MapNameStyle.COLOR); mapOrder.setNextMap(null); - ChatDispatcher.broadcastAdminChatMessage( - translatable( - "map.setNext.revert", - NamedTextColor.GRAY, - UsernameFormatUtils.formatStaffName(sender, match), - mapName), - match); + ChatManager.broadcastAdminMessage(translatable( + "map.setNext.revert", + NamedTextColor.GRAY, + UsernameFormatUtils.formatStaffName(sender, match), + mapName)); } else { viewer.sendWarning(translatable("map.noNextMap")); } @@ -94,11 +92,10 @@ public void setNext( public static void sendSetNextMessage(@NotNull MapInfo map, CommandSender sender, Match match) { Component mapName = text(map.getName(), NamedTextColor.GOLD); - Component successful = translatable( + ChatManager.broadcastAdminMessage(translatable( "map.setNext", NamedTextColor.GRAY, UsernameFormatUtils.formatStaffName(sender, match), - mapName); - ChatDispatcher.broadcastAdminChatMessage(successful, match); + mapName)); } } diff --git a/core/src/main/java/tc/oc/pgm/command/StartCommand.java b/core/src/main/java/tc/oc/pgm/command/StartCommand.java index 334be79756..dd6d304f8c 100644 --- a/core/src/main/java/tc/oc/pgm/command/StartCommand.java +++ b/core/src/main/java/tc/oc/pgm/command/StartCommand.java @@ -14,7 +14,7 @@ import org.incendo.cloud.annotations.Permission; import tc.oc.pgm.api.Permissions; import tc.oc.pgm.api.match.Match; -import tc.oc.pgm.listeners.ChatDispatcher; +import tc.oc.pgm.channels.ChatManager; import tc.oc.pgm.start.StartCountdown; import tc.oc.pgm.start.StartMatchModule; import tc.oc.pgm.start.UnreadyReason; @@ -48,11 +48,9 @@ public void start( match.getCountdown().cancelAll(StartCountdown.class); start.forceStartCountdown(duration, null); - ChatDispatcher.broadcastAdminChatMessage( - translatable( - "admin.start.announce", - player(sender, NameStyle.FANCY), - duration(duration, NamedTextColor.AQUA)), - match); + ChatManager.broadcastAdminMessage(translatable( + "admin.start.announce", + player(sender, NameStyle.FANCY), + duration(duration, NamedTextColor.AQUA))); } } diff --git a/core/src/main/java/tc/oc/pgm/command/TeamCommand.java b/core/src/main/java/tc/oc/pgm/command/TeamCommand.java index a89b5e764c..020203b198 100644 --- a/core/src/main/java/tc/oc/pgm/command/TeamCommand.java +++ b/core/src/main/java/tc/oc/pgm/command/TeamCommand.java @@ -24,9 +24,9 @@ import tc.oc.pgm.api.party.Competitor; import tc.oc.pgm.api.party.Party; import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.channels.ChatManager; import tc.oc.pgm.join.JoinMatchModule; import tc.oc.pgm.join.JoinRequest; -import tc.oc.pgm.listeners.ChatDispatcher; import tc.oc.pgm.teams.Team; import tc.oc.pgm.teams.TeamMatchModule; import tc.oc.pgm.util.named.NameStyle; @@ -51,14 +51,12 @@ public void force( } else { join.forceJoin(joiner, (Competitor) team); } - ChatDispatcher.broadcastAdminChatMessage( - translatable( - "join.ok.force.announce", - player(sender, NameStyle.FANCY), - joiner.getName(NameStyle.FANCY), - joiner.getParty().getName(), - oldParty.getName()), - joiner.getMatch()); + ChatManager.broadcastAdminMessage(translatable( + "join.ok.force.announce", + player(sender, NameStyle.FANCY), + joiner.getName(NameStyle.FANCY), + joiner.getParty().getName(), + oldParty.getName())); } @Command("shuffle") @@ -80,8 +78,8 @@ public void shuffle( teams.forceJoin(player, null); } - ChatDispatcher.broadcastAdminChatMessage( - translatable("match.shuffle.announce.ok", player(sender, NameStyle.FANCY)), match); + ChatManager.broadcastAdminMessage( + translatable("match.shuffle.announce.ok", player(sender, NameStyle.FANCY))); } @Command("alias ") @@ -106,10 +104,8 @@ public void alias( final Component oldName = team.getName().color(NamedTextColor.GRAY); team.setName(name); - ChatDispatcher.broadcastAdminChatMessage( - translatable( - "match.alias.announce.ok", player(sender, NameStyle.FANCY), oldName, team.getName()), - match); + ChatManager.broadcastAdminMessage(translatable( + "match.alias.announce.ok", player(sender, NameStyle.FANCY), oldName, team.getName())); } @Command("scale ") @@ -125,13 +121,11 @@ public void scale( int maxSize = (int) (team.getMaxPlayers() * scale); team.setMaxSize(maxSize, maxOverfill); - ChatDispatcher.broadcastAdminChatMessage( - translatable( - "match.resize.announce.max", - player(sender, NameStyle.FANCY), - team.getName(), - text(team.getMaxPlayers(), NamedTextColor.AQUA)), - match); + ChatManager.broadcastAdminMessage(translatable( + "match.resize.announce.max", + player(sender, NameStyle.FANCY), + team.getName(), + text(team.getMaxPlayers(), NamedTextColor.AQUA))); } } @@ -151,13 +145,11 @@ public void max( else TextParser.assertInRange(maxOverfill, Range.atLeast(maxPlayers)); team.setMaxSize(maxPlayers, maxOverfill); - ChatDispatcher.broadcastAdminChatMessage( - translatable( - "match.resize.announce.max", - player(sender, NameStyle.FANCY), - team.getName(), - text(team.getMaxPlayers(), NamedTextColor.AQUA)), - match); + ChatManager.broadcastAdminMessage(translatable( + "match.resize.announce.max", + player(sender, NameStyle.FANCY), + team.getName(), + text(team.getMaxPlayers(), NamedTextColor.AQUA))); } } @@ -167,13 +159,11 @@ public void max( public void max(CommandSender sender, Match match, @Argument("teams") Collection teams) { for (Team team : teams) { team.resetMaxSize(); - ChatDispatcher.broadcastAdminChatMessage( - translatable( - "match.resize.announce.max", - player(sender, NameStyle.FANCY), - team.getName(), - text(team.getMaxPlayers(), NamedTextColor.AQUA)), - match); + ChatManager.broadcastAdminMessage(translatable( + "match.resize.announce.max", + player(sender, NameStyle.FANCY), + team.getName(), + text(team.getMaxPlayers(), NamedTextColor.AQUA))); } } @@ -188,13 +178,11 @@ public void min( TextParser.assertInRange(minPlayers, Range.atLeast(0)); for (Team team : teams) { team.setMinSize(minPlayers); - ChatDispatcher.broadcastAdminChatMessage( - translatable( - "match.resize.announce.min", - player(sender, NameStyle.FANCY), - team.getName(), - text(team.getMaxPlayers(), NamedTextColor.AQUA)), - match); + ChatManager.broadcastAdminMessage(translatable( + "match.resize.announce.min", + player(sender, NameStyle.FANCY), + team.getName(), + text(team.getMaxPlayers(), NamedTextColor.AQUA))); } } @@ -204,13 +192,11 @@ public void min( public void min(CommandSender sender, Match match, @Argument("teams") Collection teams) { for (Team team : teams) { team.resetMinSize(); - ChatDispatcher.broadcastAdminChatMessage( - translatable( - "match.resize.announce.min", - player(sender, NameStyle.FANCY), - team.getName(), - text(team.getMaxPlayers(), NamedTextColor.AQUA)), - match); + ChatManager.broadcastAdminMessage(translatable( + "match.resize.announce.min", + player(sender, NameStyle.FANCY), + team.getName(), + text(team.getMaxPlayers(), NamedTextColor.AQUA))); } } } diff --git a/core/src/main/java/tc/oc/pgm/command/TimeLimitCommand.java b/core/src/main/java/tc/oc/pgm/command/TimeLimitCommand.java index 21cee9cb9d..d67f823e35 100644 --- a/core/src/main/java/tc/oc/pgm/command/TimeLimitCommand.java +++ b/core/src/main/java/tc/oc/pgm/command/TimeLimitCommand.java @@ -17,7 +17,7 @@ import tc.oc.pgm.api.Permissions; import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.party.VictoryCondition; -import tc.oc.pgm.listeners.ChatDispatcher; +import tc.oc.pgm.channels.ChatManager; import tc.oc.pgm.timelimit.TimeLimit; import tc.oc.pgm.timelimit.TimeLimitMatchModule; import tc.oc.pgm.util.named.NameStyle; @@ -38,23 +38,20 @@ public void timelimit( @Argument("max-overtime") Duration maxOvertime, @Argument("end-overtime") Duration endOvertime) { time.cancel(); - time.setTimeLimit( - new TimeLimit( - null, - duration.isNegative() ? Duration.ZERO : duration, - overtime, - maxOvertime, - endOvertime, - result.orElse(null), - true)); + time.setTimeLimit(new TimeLimit( + null, + duration.isNegative() ? Duration.ZERO : duration, + overtime, + maxOvertime, + endOvertime, + result.orElse(null), + true)); time.start(); - ChatDispatcher.broadcastAdminChatMessage( - translatable( - "match.timeLimit.announce.commandOutput", - player(sender, NameStyle.FANCY), - clock(duration).color(NamedTextColor.AQUA), - result.map(r -> r.getDescription(match)).orElse(translatable("misc.unknown"))), - match); + ChatManager.broadcastAdminMessage(translatable( + "match.timeLimit.announce.commandOutput", + player(sender, NameStyle.FANCY), + clock(duration).color(NamedTextColor.AQUA), + result.map(r -> r.getDescription(match)).orElse(translatable("misc.unknown")))); } } diff --git a/core/src/main/java/tc/oc/pgm/command/VotingCommand.java b/core/src/main/java/tc/oc/pgm/command/VotingCommand.java index 4cde6acce8..d4db420c7c 100644 --- a/core/src/main/java/tc/oc/pgm/command/VotingCommand.java +++ b/core/src/main/java/tc/oc/pgm/command/VotingCommand.java @@ -24,7 +24,7 @@ import tc.oc.pgm.api.map.MapInfo; import tc.oc.pgm.api.map.MapOrder; import tc.oc.pgm.api.match.Match; -import tc.oc.pgm.listeners.ChatDispatcher; +import tc.oc.pgm.channels.ChatManager; import tc.oc.pgm.rotation.MapPoolManager; import tc.oc.pgm.rotation.pools.VotingPool; import tc.oc.pgm.rotation.vote.MapVotePicker; @@ -66,7 +66,7 @@ public void addMap( } if (vote.addMap(map)) { - ChatDispatcher.broadcastAdminChatMessage(addMessage, match); + ChatManager.broadcastAdminMessage(addMessage); } else { viewer.sendWarning(translatable("vote.limit", NamedTextColor.RED)); } @@ -83,13 +83,11 @@ public void removeMap( @Argument("map") @Greedy MapInfo map) { VotePoolOptions vote = getVoteOptions(mapOrder); if (vote.removeMap(map)) { - ChatDispatcher.broadcastAdminChatMessage( - translatable( - "vote.remove", - NamedTextColor.GRAY, - UsernameFormatUtils.formatStaffName(sender, match), - map.getStyledName(MapNameStyle.COLOR)), - match); + ChatManager.broadcastAdminMessage(translatable( + "vote.remove", + NamedTextColor.GRAY, + UsernameFormatUtils.formatStaffName(sender, match), + map.getStyledName(MapNameStyle.COLOR))); } else { viewer.sendWarning(translatable("map.notFound")); } @@ -102,13 +100,11 @@ public void mode(CommandSender sender, MapOrder mapOrder, Match match) { VotePoolOptions vote = getVoteOptions(mapOrder); Component voteModeName = translatable( vote.toggleMode() ? "vote.mode.replace" : "vote.mode.create", NamedTextColor.LIGHT_PURPLE); - ChatDispatcher.broadcastAdminChatMessage( - translatable( - "vote.toggle", - NamedTextColor.GRAY, - UsernameFormatUtils.formatStaffName(sender, match), - voteModeName), - match); + ChatManager.broadcastAdminMessage(translatable( + "vote.toggle", + NamedTextColor.GRAY, + UsernameFormatUtils.formatStaffName(sender, match), + voteModeName)); } @Command("clear") @@ -131,7 +127,7 @@ public void clearMaps(Audience viewer, CommandSender sender, Match match, MapOrd if (maps.isEmpty()) { viewer.sendWarning(translatable("vote.noMapsFound")); } else { - ChatDispatcher.broadcastAdminChatMessage(clearedMsg, match); + ChatManager.broadcastAdminMessage(clearedMsg); } } diff --git a/core/src/main/java/tc/oc/pgm/command/util/PGMCommandGraph.java b/core/src/main/java/tc/oc/pgm/command/util/PGMCommandGraph.java index 0d5111bdff..88b5752817 100644 --- a/core/src/main/java/tc/oc/pgm/command/util/PGMCommandGraph.java +++ b/core/src/main/java/tc/oc/pgm/command/util/PGMCommandGraph.java @@ -80,7 +80,6 @@ import tc.oc.pgm.command.parsers.TeamsParser; import tc.oc.pgm.command.parsers.VariableParser; import tc.oc.pgm.command.parsers.VictoryConditionParser; -import tc.oc.pgm.listeners.ChatDispatcher; import tc.oc.pgm.modes.Mode; import tc.oc.pgm.rotation.MapPoolManager; import tc.oc.pgm.rotation.pools.MapPool; @@ -134,14 +133,14 @@ protected void registerCommands() { if (ShowXmlCommand.isEnabled()) register(ShowXmlCommand.getInstance()); if (PGM.get().getConfiguration().isVanishEnabled()) register(new VanishCommand()); - register(ChatDispatcher.get()); - manager.command(manager .commandBuilder("pgm") .literal("help") .optional("query", StringParser.greedyStringParser()) .handler(context -> minecraftHelp.queryCommands( context.optional("query").orElse(""), context.sender()))); + + PGM.get().getChatManager().registerCommands(manager); } // Injectors diff --git a/core/src/main/java/tc/oc/pgm/goals/TouchableGoal.java b/core/src/main/java/tc/oc/pgm/goals/TouchableGoal.java index 92647e3323..30030c975b 100644 --- a/core/src/main/java/tc/oc/pgm/goals/TouchableGoal.java +++ b/core/src/main/java/tc/oc/pgm/goals/TouchableGoal.java @@ -21,6 +21,7 @@ import tc.oc.pgm.api.party.event.CompetitorRemoveEvent; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.api.player.ParticipantState; +import tc.oc.pgm.channels.ChatManager; import tc.oc.pgm.goals.events.GoalCompleteEvent; import tc.oc.pgm.goals.events.GoalTouchEvent; import tc.oc.pgm.spawns.events.ParticipantDespawnEvent; @@ -129,15 +130,14 @@ public void touch(final @Nullable ParticipantState toucher) { boolean firstForPlayer = touchingPlayers.add(toucher); boolean firstForPlayerLife = recentTouchingPlayers.add(toucher); - event = - new GoalTouchEvent( - this, - toucher.getParty(), - firstForCompetitor, - toucher, - firstForPlayer, - firstForPlayerLife, - getMatch().getTick().instant); + event = new GoalTouchEvent( + this, + toucher.getParty(), + firstForCompetitor, + toucher, + firstForPlayer, + firstForPlayerLife, + getMatch().getTick().instant); } getMatch().callEvent(event); @@ -179,10 +179,12 @@ public boolean showEnemyTouches() { return false; } + public boolean shouldShowTouched(@Nullable Competitor team) { + return team != null && !isCompleted(team) && hasTouched(team); + } + public boolean shouldShowTouched(@Nullable Competitor team, Party viewer) { - return team != null - && !isCompleted(team) - && hasTouched(team) + return shouldShowTouched(team) && (team == viewer || showEnemyTouches() || viewer.isObserving()); } @@ -192,15 +194,13 @@ protected void sendTouchMessage(@Nullable ParticipantState toucher, boolean incl Component message = getTouchMessage(toucher, false); Audience.console().sendMessage(message); - if (!showEnemyTouches()) { - message = text().append(toucher.getParty().getChatPrefix()).append(message).build(); - } - - for (MatchPlayer viewer : getMatch().getPlayers()) { - if (shouldShowTouched(toucher.getParty(), viewer.getParty()) - && (toucher == null || !toucher.isPlayer(viewer))) { - viewer.sendMessage(message); - } + if (shouldShowTouched(toucher.getParty())) { + if (showEnemyTouches()) + ChatManager.broadcastMessage( + message, viewer -> toucher == null || !toucher.isPlayer(viewer)); + else + ChatManager.broadcastPartyMessage( + message, toucher.getParty(), viewer -> toucher == null || !toucher.isPlayer(viewer)); } if (toucher != null) { diff --git a/core/src/main/java/tc/oc/pgm/listeners/AntiGriefListener.java b/core/src/main/java/tc/oc/pgm/listeners/AntiGriefListener.java index c31bce06b8..fe8e5daa69 100644 --- a/core/src/main/java/tc/oc/pgm/listeners/AntiGriefListener.java +++ b/core/src/main/java/tc/oc/pgm/listeners/AntiGriefListener.java @@ -27,6 +27,7 @@ import tc.oc.pgm.api.match.MatchManager; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.api.player.ParticipantState; +import tc.oc.pgm.channels.ChatManager; import tc.oc.pgm.spawns.events.ObserverKitApplyEvent; import tc.oc.pgm.tnt.TNTMatchModule; import tc.oc.pgm.tracker.Trackers; @@ -91,25 +92,21 @@ private void participantDefuse(Player player, Entity entity) { entity, translatable("moderation.defuse.player", NamedTextColor.RED, owner.getName())); - ChatDispatcher.broadcastAdminChatMessage( - translatable( - "moderation.defuse.alert.player", - NamedTextColor.GRAY, - clicker.getName(), - owner.getName(), - MinecraftComponent.entity(entity.getType()).color(NamedTextColor.DARK_RED)), - clicker.getMatch()); + ChatManager.broadcastAdminMessage(translatable( + "moderation.defuse.alert.player", + NamedTextColor.GRAY, + clicker.getName(), + owner.getName(), + MinecraftComponent.entity(entity.getType()).color(NamedTextColor.DARK_RED))); } else { this.notifyDefuse( clicker, entity, translatable("moderation.defuse.world", NamedTextColor.RED)); - ChatDispatcher.broadcastAdminChatMessage( - translatable( - "moderation.defuse.alert.world", - NamedTextColor.GRAY, - clicker.getName(), - MinecraftComponent.entity(entity.getType()).color(NamedTextColor.DARK_RED)), - clicker.getMatch()); + ChatManager.broadcastAdminMessage(translatable( + "moderation.defuse.alert.world", + NamedTextColor.GRAY, + clicker.getName(), + MinecraftComponent.entity(entity.getType()).color(NamedTextColor.DARK_RED))); } } } diff --git a/core/src/main/java/tc/oc/pgm/listeners/ChatDispatcher.java b/core/src/main/java/tc/oc/pgm/listeners/ChatDispatcher.java index c9e3588ba7..4e2b3347f8 100644 --- a/core/src/main/java/tc/oc/pgm/listeners/ChatDispatcher.java +++ b/core/src/main/java/tc/oc/pgm/listeners/ChatDispatcher.java @@ -1,488 +1,55 @@ package tc.oc.pgm.listeners; -import static net.kyori.adventure.identity.Identity.identity; -import static net.kyori.adventure.text.Component.space; import static net.kyori.adventure.text.Component.text; -import static net.kyori.adventure.text.Component.translatable; -import static tc.oc.pgm.util.text.TextException.exception; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import net.kyori.adventure.identity.Identity; import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.format.TextDecoration; -import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.player.AsyncPlayerChatEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.incendo.cloud.annotation.specifier.Greedy; -import org.incendo.cloud.annotations.Argument; -import org.incendo.cloud.annotations.Command; -import org.incendo.cloud.annotations.CommandDescription; -import org.incendo.cloud.annotations.Permission; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import tc.oc.pgm.api.PGM; -import tc.oc.pgm.api.Permissions; -import tc.oc.pgm.api.integration.Integration; import tc.oc.pgm.api.match.Match; -import tc.oc.pgm.api.match.MatchManager; -import tc.oc.pgm.api.party.Party; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.api.setting.SettingKey; import tc.oc.pgm.api.setting.SettingValue; -import tc.oc.pgm.command.SettingCommand; -import tc.oc.pgm.ffa.Tribute; -import tc.oc.pgm.util.Audience; -import tc.oc.pgm.util.Players; -import tc.oc.pgm.util.bukkit.BukkitUtils; -import tc.oc.pgm.util.bukkit.Sounds; -import tc.oc.pgm.util.channels.Channel; -import tc.oc.pgm.util.event.ChannelMessageEvent; -import tc.oc.pgm.util.named.NameStyle; -import tc.oc.pgm.util.text.TextException; -import tc.oc.pgm.util.text.TextTranslations; +import tc.oc.pgm.channels.AdminChannel; +import tc.oc.pgm.channels.ChatManager; -public class ChatDispatcher implements Listener { +@Deprecated(forRemoval = true) +public class ChatDispatcher { - private static ChatDispatcher INSTANCE = new ChatDispatcher(); + private static final ChatDispatcher INSTANCE = new ChatDispatcher(); public static ChatDispatcher get() { return INSTANCE; // FIXME: no one should need to statically access ChatDispatcher, but community // does this a lot } - private final MatchManager manager; - private final Map lastMessagedBy; - public static final TextComponent ADMIN_CHAT_PREFIX = text() .append(text("[", NamedTextColor.WHITE)) .append(text("A", NamedTextColor.GOLD)) .append(text("] ", NamedTextColor.WHITE)) .build(); - private static final String GLOBAL_SYMBOL = "!"; - private static final String DM_SYMBOL = "@"; - private static final String ADMIN_CHAT_SYMBOL = "$"; - - private static final String GLOBAL_FORMAT = "<%s>: %s"; - private static final String PREFIX_FORMAT = "%s: %s"; - private static final String AC_FORMAT = - TextTranslations.translateLegacy(ADMIN_CHAT_PREFIX) + PREFIX_FORMAT; - - private static final Predicate AC_FILTER = - viewer -> viewer.getBukkit().hasPermission(Permissions.ADMINCHAT); - - public ChatDispatcher() { - this.manager = PGM.get().getMatchManager(); - this.lastMessagedBy = new HashMap<>(); - PGM.get().getServer().getPluginManager().registerEvents(this, PGM.get()); - } - - public boolean isMuted(MatchPlayer player) { - return player != null && Integration.isMuted(player.getBukkit()); - } - - @Command("g|all [message]") - @CommandDescription("Send a message to everyone") - public void sendGlobal( - Match match, - @NotNull MatchPlayer sender, - @Argument(value = "message", suggestions = "players") @Greedy String message) { - if (Integration.isVanished(sender.getBukkit())) { - sendAdmin(match, sender, message); - return; - } - throwMuted(sender); - - send( - match, - sender, - message, - GLOBAL_FORMAT, - getChatFormat(null, sender, message), - match.getPlayers(), - viewer -> true, - SettingValue.CHAT_GLOBAL, - Channel.GLOBAL); - } - - @Command("t [message]") - @CommandDescription("Send a message to your team") - public void sendTeam( - Match match, - @NotNull MatchPlayer sender, - @Argument(value = "message", suggestions = "players") @Greedy String message) { - if (Integration.isVanished(sender.getBukkit())) { - sendAdmin(match, sender, message); - return; - } - - final Party party = sender.getParty(); - - // No team chat when playing free-for-all or match end, default to global chat - if (party instanceof Tribute || match.isFinished()) { - sendGlobal(match, sender, message); - return; - } - - throwMuted(sender); - send( - match, - sender, - message, - TextTranslations.translateLegacy(party.getChatPrefix()) + PREFIX_FORMAT, - getChatFormat(party.getChatPrefix(), sender, message), - match.getPlayers(), - viewer -> party.equals(viewer.getParty()) - || (viewer.isObserving() && viewer.getBukkit().hasPermission(Permissions.ADMINCHAT)), - SettingValue.CHAT_TEAM, - Channel.TEAM); - } - - @Command("a [message]") - @CommandDescription("Send a message to operators") - @Permission(Permissions.ADMINCHAT) - public void sendAdmin( - Match match, - @NotNull MatchPlayer sender, - @Argument(value = "message", suggestions = "players") @Greedy String message) { - // If a player managed to send a default message without permissions, reset their chat channel - if (!sender.getBukkit().hasPermission(Permissions.ADMINCHAT)) { - sender.getSettings().resetValue(SettingKey.CHAT); - SettingKey.CHAT.update(sender); - sender.sendWarning(translatable("misc.noPermission")); - return; - } - - send( - match, - sender, - message != null ? BukkitUtils.colorize(message) : null, - AC_FORMAT, - getChatFormat(ADMIN_CHAT_PREFIX, sender, message), - match.getPlayers(), - AC_FILTER, - SettingValue.CHAT_ADMIN, - Channel.ADMIN); - - // Play sounds for admin chat - if (message != null) { - match.getPlayers().stream() - .filter(AC_FILTER) // Initial filter - .filter(viewer -> !viewer.equals(sender)) // Don't play sound for sender - .forEach(pl -> playSound(pl, Sounds.ADMIN_CHAT)); - } - } - - @Command("msg|tell|pm|dm ") - @CommandDescription("Send a direct message to a player") - public void sendDirect( - Match match, - @NotNull MatchPlayer sender, - @Argument("player") MatchPlayer receiver, - @Argument(value = "message", suggestions = "players") @Greedy String message) { - if (Integration.isVanished(sender.getBukkit())) throw exception("vanish.chat.deny"); - if (receiver.equals(sender)) throw exception("command.message.self"); - - if (!receiver.getBukkit().hasPermission(Permissions.STAFF)) throwMuted(sender); - - SettingValue option = receiver.getSettings().getValue(SettingKey.MESSAGE); - - if (!sender.getBukkit().hasPermission(Permissions.STAFF)) { - if (option.equals(SettingValue.MESSAGE_OFF)) - throw exception("command.message.blocked", receiver.getName()); - - if (option.equals(SettingValue.MESSAGE_FRIEND) - && !Integration.isFriend(receiver.getBukkit(), sender.getBukkit())) - throw exception("command.message.friendsOnly", receiver.getName()); - - if (isMuted(receiver)) throw exception("moderation.mute.target", receiver.getName()); - } - - trackMessage(receiver.getBukkit(), sender.getBukkit()); - - // Send message to receiver - send( - match, - sender, - message, - formatPrivateMessage("misc.from", receiver.getBukkit()), - getChatFormat( - text() - .append(translatable("misc.from", NamedTextColor.GRAY, TextDecoration.ITALIC)) - .append(space()) - .build(), - sender, - message), - Collections.singleton(receiver), - r -> true, - null, - Channel.PRIVATE_RECEIVER); - - // Send message to the sender - send( - match, - receiver, - message, - formatPrivateMessage("misc.to", sender.getBukkit()), - getChatFormat( - text() - .append(translatable("misc.to", NamedTextColor.GRAY, TextDecoration.ITALIC)) - .append(space()) - .build(), - receiver, - message), - Collections.singleton(sender), - s -> true, - null, - Channel.PRIVATE_SENDER); - playSound(receiver, Sounds.DIRECT_MESSAGE); - } - - private String formatPrivateMessage(String key, CommandSender viewer) { - Component action = - translatable(key, NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, true); - return TextTranslations.translateLegacy(action, viewer) + " " + PREFIX_FORMAT; - } - - @Command("reply|r ") - @CommandDescription("Reply to a direct message") - public void sendReply( - Match match, - @NotNull MatchPlayer sender, - @Argument(value = "message", suggestions = "players") @Greedy String message) { - MatchPlayer receiver = manager.getPlayer(getLastMessagedId(sender.getBukkit())); - if (receiver == null) throw exception("command.message.noReply", text("/msg")); - - sendDirect(match, sender, receiver, message); - } - - @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) - public void onChat(AsyncPlayerChatEvent event) { - if (CHAT_EVENT_CACHE.getIfPresent(event) == null) { - event.setCancelled(true); - } else { - CHAT_EVENT_CACHE.invalidate(event); - return; - } - - final MatchPlayer player = manager.getPlayer(event.getPlayer()); - if (player == null) return; - - final String message = event.getMessage(); - - try { - if (message.startsWith(GLOBAL_SYMBOL)) { - sendGlobal(player.getMatch(), player, message.substring(1)); - } else if (message.startsWith(DM_SYMBOL) && message.contains(" ")) { - final String target = message.substring(1, message.indexOf(" ")); - final MatchPlayer receiver = Players.getMatchPlayer(event.getPlayer(), target); - if (receiver == null) { - player.sendWarning(translatable("command.playerNotFound")); - } else { - sendDirect(player.getMatch(), player, receiver, message.substring(2 + target.length())); - } - } else if (message.startsWith(ADMIN_CHAT_SYMBOL) - && player.getBukkit().hasPermission(Permissions.ADMINCHAT)) { - sendAdmin(player.getMatch(), player, event.getMessage().substring(1)); - } else { - sendDefault(player.getMatch(), player, event.getMessage()); - } - } catch (TextException e) { - // Allow sub-handlers to throw command exceptions just fine - player.sendWarning(e); - } - } - - public void sendDefault(Match match, @NotNull MatchPlayer sender, String message) { - switch (sender.getSettings().getValue(SettingKey.CHAT)) { - case CHAT_TEAM: - sendTeam(match, sender, message); - return; - case CHAT_ADMIN: - sendAdmin(match, sender, message); - return; - default: - sendGlobal(match, sender, message); - } - } - - private static final Cache CHAT_EVENT_CACHE = - CacheBuilder.newBuilder() - .weakKeys() - .expireAfterWrite(15, TimeUnit.SECONDS) - .build(); - - public void send( - final @NotNull Match match, - final @NotNull MatchPlayer sender, - final @Nullable String text, - final @NotNull String format, - final @NotNull Component componentMsg, - final @NotNull Collection matchPlayers, - final @NotNull Predicate filter, - final @Nullable SettingValue type, - final @NotNull Channel channel) { - final String message = text == null ? null : text.trim(); - // When a message is empty, this indicates the player wants to change their default chat channel - if (message == null || message.isEmpty()) { - if (type != null) SettingCommand.getInstance().toggle(sender, SettingKey.CHAT, type); - return; - } - - final Set players = matchPlayers.stream() - .filter(filter) - .map(MatchPlayer::getBukkit) - .collect(Collectors.toSet()); - - Runnable completion = - () -> syncSendChat(match, sender, message, format, componentMsg, players, channel); - if (Bukkit.isPrimaryThread()) completion.run(); - else PGM.get().getExecutor().execute(completion); - } - - private void syncSendChat( - final @NotNull Match match, - final @NotNull MatchPlayer sender, - final @NotNull String message, - final @NotNull String format, - final @NotNull Component componentMsg, - final @NotNull Set players, - final @NotNull Channel channel) { - final AsyncPlayerChatEvent event = - new AsyncPlayerChatEvent(false, sender.getBukkit(), message, players); - event.setFormat(format); - CHAT_EVENT_CACHE.put(event, true); - match.callEvent(event); - - if (event.isCancelled()) return; - match.callEvent(new ChannelMessageEvent(channel, sender.getBukkit(), message)); - - Identity senderId = identity(sender.getId()); - - event.getRecipients().stream() - .map(Audience::get) - .forEach(player -> player.sendMessage(senderId, componentMsg)); - } - - private void throwMuted(MatchPlayer player) { - if (isMuted(player)) { - Optional muteReason = - Optional.ofNullable(Integration.getMuteReason(player.getBukkit())); - - Component reason = muteReason.isPresent() - ? text(muteReason.get()) - : translatable("moderation.mute.noReason"); - - throw exception("moderation.mute.message", reason.color(NamedTextColor.AQUA)); - } - } - public static void broadcastAdminChatMessage(Component message, Match match) { - broadcastAdminChatMessage(message, match, Optional.empty()); + ChatManager.broadcastAdminMessage(message); } public static void broadcastAdminChatMessage( Component message, Match match, Optional sound) { - TextComponent formatted = ADMIN_CHAT_PREFIX.append(message); - match.getPlayers().stream().filter(AC_FILTER).forEach(mp -> { - // If provided a sound, play if setting allows - sound.ifPresent(s -> playSound(mp, s)); - mp.sendMessage(formatted); + AdminChannel channel = PGM.get().getChatManager().getAdminChannel(); + Collection viewers = channel.getBroadcastViewers(null); + channel.broadcastMessage(message, null); + + sound.ifPresent(s -> { + viewers.stream() + .filter(player -> { + SettingValue settingValue = player.getSettings().getValue(SettingKey.SOUNDS); + return settingValue.equals(SettingValue.SOUNDS_ALL) + || settingValue.equals(SettingValue.SOUNDS_CHAT); + }) + .forEach(player -> player.playSound(s)); }); - Audience.console().sendMessage(formatted); - } - - public static void playSound(MatchPlayer player, Sound sound) { - SettingValue value = player.getSettings().getValue(SettingKey.SOUNDS); - if (value.equals(SettingValue.SOUNDS_ALL) - || value.equals(SettingValue.SOUNDS_CHAT) - || (sound.equals(Sounds.DIRECT_MESSAGE) && value.equals(SettingValue.SOUNDS_DM))) { - player.playSound(sound); - } - } - - private Component getChatFormat(@Nullable Component prefix, MatchPlayer player, String message) { - Component msg = text(message != null ? message.trim() : ""); - if (prefix == null) - return text() - .append(text("<", NamedTextColor.WHITE)) - .append(player.getName(NameStyle.VERBOSE)) - .append(text(">: ", NamedTextColor.WHITE)) - .append(msg) - .build(); - return text() - .append(prefix) - .append(player.getName(NameStyle.VERBOSE)) - .append(text(": ", NamedTextColor.WHITE)) - .append(msg) - .build(); - } - - @EventHandler(ignoreCancelled = true) - public void onPlayerQuit(PlayerQuitEvent event) { - this.lastMessagedBy.remove(event.getPlayer().getUniqueId()); - } - - private void trackMessage(Player receiver, Player sender) { - this.lastMessagedBy.put(receiver.getUniqueId(), new MessageSenderIdentity(receiver, sender)); - } - - private UUID getLastMessagedId(Player sender) { - MessageSenderIdentity targetIdent = lastMessagedBy.get(sender.getUniqueId()); - if (targetIdent == null) return null; - MatchPlayer target = manager.getPlayer(targetIdent.getPlayerId()); - - // Prevent replying to offline players - if (target == null) return null; - - // Compare last known and current name - String lastKnownName = targetIdent.getName(); - String currentName = Players.getVisibleName(sender, target.getBukkit()); - - // Ensure the target is visible to the viewing sender - boolean visible = Players.isVisible(sender, target.getBukkit()); - - if (currentName.equalsIgnoreCase(lastKnownName) && visible) { - return target.getId(); - } - - return null; - } - - private static class MessageSenderIdentity { - private final UUID playerId; - private final String name; - - public MessageSenderIdentity(Player viewer, Player player) { - this.playerId = player.getUniqueId(); - this.name = Players.getVisibleName(viewer, player); - } - - public UUID getPlayerId() { - return playerId; - } - - public String getName() { - return name; - } } } diff --git a/core/src/main/java/tc/oc/pgm/listeners/JoinLeaveAnnouncer.java b/core/src/main/java/tc/oc/pgm/listeners/JoinLeaveAnnouncer.java index 14cbdd99e9..c684bb8e6f 100644 --- a/core/src/main/java/tc/oc/pgm/listeners/JoinLeaveAnnouncer.java +++ b/core/src/main/java/tc/oc/pgm/listeners/JoinLeaveAnnouncer.java @@ -1,9 +1,7 @@ package tc.oc.pgm.listeners; -import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.Component.translatable; -import java.util.Collection; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.entity.Player; @@ -12,6 +10,7 @@ import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; +import tc.oc.pgm.api.PGM; import tc.oc.pgm.api.Permissions; import tc.oc.pgm.api.integration.Integration; import tc.oc.pgm.api.match.MatchManager; @@ -19,6 +18,7 @@ import tc.oc.pgm.api.player.event.PlayerVanishEvent; import tc.oc.pgm.api.setting.SettingKey; import tc.oc.pgm.api.setting.SettingValue; +import tc.oc.pgm.channels.ChatManager; public class JoinLeaveAnnouncer implements Listener { @@ -73,43 +73,32 @@ public void leave(MatchPlayer player, JoinVisibility visibility) { private void handleJoinLeave(MatchPlayer player, String key, JoinVisibility visibility) { boolean staff = visibility == JoinVisibility.STAFF; + Component component = + translatable(key + (staff ? ".quiet" : ""), NamedTextColor.YELLOW, player.getName()); - Collection viewers = player.getMatch().getPlayers(); - for (MatchPlayer viewer : viewers) { - if (player.equals(viewer)) continue; // Never display own broadcast + ChatManager chatManager = PGM.get().getChatManager(); - if (canView(viewer, player, visibility)) { // Check if viewer setting allows join/leaves - - Component component = - translatable(key + (staff ? ".quiet" : ""), NamedTextColor.YELLOW, player.getName()); - - if (staff) { - component = text().append(ChatDispatcher.ADMIN_CHAT_PREFIX).append(component).build(); - } - - viewer.sendMessage(component); - } - } + (staff ? chatManager.getAdminChannel() : chatManager.getGlobalChannel()) + .broadcastMessage( + component, + null, + viewer -> !player.equals(viewer) && canView(viewer, player, visibility)); } private boolean canView(MatchPlayer viewer, MatchPlayer target, JoinVisibility visibility) { boolean isStaff = viewer.getBukkit().hasPermission(Permissions.STAFF); SettingValue option = viewer.getSettings().getValue(SettingKey.JOIN); - boolean allowed = - option.equals(SettingValue.JOIN_ON) - || areFriends(option, viewer.getBukkit(), target.getBukkit()); + boolean allowed = option.equals(SettingValue.JOIN_ON) + || areFriends(option, viewer.getBukkit(), target.getBukkit()); if (!allowed) return false; - switch (visibility) { - case NONSTAFF: - return !isStaff; - case STAFF: - return isStaff; - default: - return true; - } + return switch (visibility) { + case NONSTAFF -> !isStaff; + case STAFF -> isStaff; + default -> true; + }; } private boolean areFriends(SettingValue value, Player a, Player b) { diff --git a/core/src/main/java/tc/oc/pgm/listeners/PGMListener.java b/core/src/main/java/tc/oc/pgm/listeners/PGMListener.java index 5b0edc9a65..2d5269d806 100644 --- a/core/src/main/java/tc/oc/pgm/listeners/PGMListener.java +++ b/core/src/main/java/tc/oc/pgm/listeners/PGMListener.java @@ -47,6 +47,7 @@ import tc.oc.pgm.api.match.event.MatchLoadEvent; import tc.oc.pgm.api.match.event.MatchStartEvent; import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.channels.ChatManager; import tc.oc.pgm.events.MapPoolAdjustEvent; import tc.oc.pgm.events.PlayerJoinMatchEvent; import tc.oc.pgm.events.PlayerLeavePartyEvent; @@ -343,7 +344,7 @@ public void announceDynamicMapPoolChange(MapPoolAdjustEvent event) { forced = translatable("pool.change.forceTimed", poolName, matchLimit, staffName); } - ChatDispatcher.broadcastAdminChatMessage(forced.color(NamedTextColor.GRAY), event.getMatch()); + ChatManager.broadcastAdminMessage(forced.color(NamedTextColor.GRAY)); } // Broadcast map pool changes due to size diff --git a/core/src/main/java/tc/oc/pgm/util/MessageSenderIdentity.java b/core/src/main/java/tc/oc/pgm/util/MessageSenderIdentity.java new file mode 100644 index 0000000000..6d7671909d --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/util/MessageSenderIdentity.java @@ -0,0 +1,44 @@ +package tc.oc.pgm.util; + +import java.util.UUID; +import org.bukkit.entity.Player; +import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.player.MatchPlayer; + +public class MessageSenderIdentity { + + private final UUID playerId; + private final String name; + + public MessageSenderIdentity(Player viewer, Player player) { + this.playerId = player.getUniqueId(); + this.name = Players.getVisibleName(viewer, player); + } + + public UUID getPlayerId() { + return playerId; + } + + public String getName() { + return name; + } + + public MatchPlayer getPlayer(Player viewer) { + MatchPlayer target = PGM.get().getMatchManager().getPlayer(playerId); + + // Prevent replying to offline players + if (target == null) return null; + + // Compare last known and current name + String currentName = Players.getVisibleName(viewer, target.getBukkit()); + + // Ensure the target is visible to the viewing sender + boolean visible = Players.isVisible(viewer, target.getBukkit()); + + if (currentName.equalsIgnoreCase(name) && visible) { + return target; + } + + return null; + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/bukkit/OnlinePlayerUUIDMapAdapter.java b/util/src/main/java/tc/oc/pgm/util/bukkit/OnlinePlayerUUIDMapAdapter.java new file mode 100644 index 0000000000..c6585c60de --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/bukkit/OnlinePlayerUUIDMapAdapter.java @@ -0,0 +1,32 @@ +package tc.oc.pgm.util.bukkit; + +import java.util.Map; +import java.util.UUID; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.Plugin; + +public class OnlinePlayerUUIDMapAdapter extends ListeningMapAdapter + implements Listener { + public OnlinePlayerUUIDMapAdapter(Plugin plugin) { + super(plugin); + } + + public OnlinePlayerUUIDMapAdapter(Map map, Plugin plugin) { + super(map, plugin); + } + + public boolean isValid(UUID key) { + Player player = Bukkit.getPlayer(key); + return player != null && player.isOnline(); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerQuit(PlayerQuitEvent event) { + this.remove(event.getPlayer().getUniqueId()); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/channels/Channel.java b/util/src/main/java/tc/oc/pgm/util/channels/Channel.java deleted file mode 100644 index 73d977fcad..0000000000 --- a/util/src/main/java/tc/oc/pgm/util/channels/Channel.java +++ /dev/null @@ -1,9 +0,0 @@ -package tc.oc.pgm.util.channels; - -public enum Channel { - GLOBAL, - TEAM, - PRIVATE_SENDER, - PRIVATE_RECEIVER, - ADMIN; -} diff --git a/util/src/main/java/tc/oc/pgm/util/event/ChannelMessageEvent.java b/util/src/main/java/tc/oc/pgm/util/event/ChannelMessageEvent.java deleted file mode 100644 index 5995b3f787..0000000000 --- a/util/src/main/java/tc/oc/pgm/util/event/ChannelMessageEvent.java +++ /dev/null @@ -1,54 +0,0 @@ -package tc.oc.pgm.util.event; - -import org.bukkit.entity.Player; -import org.bukkit.event.HandlerList; -import org.jetbrains.annotations.Nullable; -import tc.oc.pgm.util.channels.Channel; - -public class ChannelMessageEvent extends PreemptiveEvent { - - private Channel channel; - private @Nullable Player sender; - private String message; - - public ChannelMessageEvent(Channel channel, Player sender, String message) { - this.channel = channel; - this.sender = sender; - this.message = message; - } - - public Channel getChannel() { - return channel; - } - - public void setChannel(Channel channel) { - this.channel = channel; - } - - public Player getSender() { - return sender; - } - - public void setSender(Player sender) { - this.sender = sender; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - private static final HandlerList handlers = new HandlerList(); - - @Override - public HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } -}