From 9fe05b59e43ad9c79d834a91fe3278b6513f697a Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Thu, 15 Oct 2026 19:55:02 +0200 Subject: [PATCH 1/7] Rework chat into Velocity-managed channels Chat is now handled entirely by VelocityCore through channels with separate readers and writers (Global, Local/Fight, Blue/Red fight team, Team, ServerTeam, Alert and 1:1 direct messages). Each player has a saved default channel; messages from other channels are tagged with the channel name, and every default change is announced. - /global, /local, /tc, /stc, /msg and /r switch the default channel when called without a message - /chat switches, /chat join|leave toggles reading (saved); moderators can join arenas by name and write with /chat msg - '+' sends from global to local and from local/fight to the fight team - Referees write yellow in the fight chat - Subservers no longer receive chat; arenas get a record-only ChatRecordPacket so replays keep their chat - Remove the chat listeners of SpigotCore, FightSystem, MissileWars, TowerRun and TNTLeague Closes #460, #129 --- .../packets/server/ChatRecordPacket.java | 33 +- .../fightsystem/FightSystem.properties | 1 - .../steamwar/fightsystem/fight/FightTeam.java | 4 - .../steamwar/fightsystem/listener/Chat.java | 47 +- .../misslewars/listener/ChatListener.java | 58 --- .../SpigotCore_Main/src/SpigotCore.properties | 1 - .../src/SpigotCore_de.properties | 1 - .../steamwar/tntleague/TNTLeague.properties | 3 - .../tntleague/events/GlobalListener.kt | 17 - TowerRun/src/TowerRun.properties | 3 - .../towerrun/listener/GlobalListener.java | 25 -- .../steamwar/messages/BungeeCore.properties | 32 +- .../messages/BungeeCore_de.properties | 32 +- .../src/de/steamwar/messages/Chatter.java | 9 - .../velocitycore/chat/AlertChannel.java | 70 +++ .../velocitycore/chat/ChatChannel.java | 105 +++++ .../velocitycore/chat/ChatFilter.java | 76 ++++ .../velocitycore/chat/ChatFormat.java | 94 ++++ .../velocitycore/chat/ChatManager.java | 400 ++++++++++++++++++ .../velocitycore/chat/DirectChannel.java | 105 +++++ .../velocitycore/chat/FightTeamChannel.java | 101 +++++ .../velocitycore/chat/GlobalChannel.java | 63 +++ .../velocitycore/chat/LocalChannel.java | 117 +++++ .../velocitycore/chat/ServerTeamChannel.java | 76 ++++ .../velocitycore/chat/TeamChannel.java | 79 ++++ .../velocitycore/commands/AlertCommand.java | 3 +- .../velocitycore/commands/ChatCommand.java | 123 ++++++ .../velocitycore/commands/GlobalCommand.java | 46 ++ .../velocitycore/commands/LocalCommand.java | 17 +- .../velocitycore/commands/MsgCommand.java | 21 +- .../velocitycore/commands/RCommand.java | 2 +- .../commands/ServerTeamchatCommand.java | 13 +- .../commands/TeamchatCommand.java | 22 +- .../velocitycore/discord/DiscordBot.java | 5 +- .../discord/channels/DiscordChatRoom.java | 13 +- .../velocitycore/listeners/ChatListener.java | 160 +------ .../network/handlers/FightInfoHandler.java | 2 + 37 files changed, 1607 insertions(+), 372 deletions(-) rename SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/ChattingEvent.java => CommonCore/Network/src/de/steamwar/network/packets/server/ChatRecordPacket.java (53%) delete mode 100644 MissileWars/src/de/steamwar/misslewars/listener/ChatListener.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/ChattingEvent.java b/CommonCore/Network/src/de/steamwar/network/packets/server/ChatRecordPacket.java similarity index 53% rename from SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/ChattingEvent.java rename to CommonCore/Network/src/de/steamwar/network/packets/server/ChatRecordPacket.java index b39a49431..642cf3358 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/ChattingEvent.java +++ b/CommonCore/Network/src/de/steamwar/network/packets/server/ChatRecordPacket.java @@ -1,7 +1,7 @@ /* * This file is a part of the SteamWar software. * - * Copyright (C) 2025 SteamWar.de-Serverteam + * Copyright (C) 2026 SteamWar.de-Serverteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,21 +17,24 @@ * along with this program. If not, see . */ -package de.steamwar.core.events; -import de.steamwar.core.Core; -import de.steamwar.linkage.Linked; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.player.AsyncPlayerChatEvent; +package de.steamwar.network.packets.server; -@Linked -public class ChattingEvent implements Listener { +import de.steamwar.network.packets.NetworkPacket; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - private void onChat(AsyncPlayerChatEvent event) { - event.setCancelled(true); - Core.MESSAGE.broadcastPrefixless("LOCAL_CHAT", event.getPlayer().getDisplayName(), event.getMessage()); - } +/** + * A chat message written on an arena. The arena only records it for the replay and never displays it. + */ +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Getter +public class ChatRecordPacket extends NetworkPacket { + private static final long serialVersionUID = -4718360932583127458L; + private int playerId; + private String message; } diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.properties b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.properties index c546644f7..1354f2e15 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.properties +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.properties @@ -109,7 +109,6 @@ TEAM_READY = §aTeam ready TEAM_NOT_READY = §c§mTeam ready SKIP_READY = §aSkipping to next event SKIP_NOT_READY = §c§mSkipping to next event -TEAM_CHAT = {0}{1}§8» {0}{2} CHOOSE_KIT = §eChoose kit RESPAWN = §eRespawn MANAGE_PLAYERS = §cManage players diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightTeam.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightTeam.java index 0119bf402..b7820066d 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightTeam.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightTeam.java @@ -256,10 +256,6 @@ public class FightTeam { broadcast(player -> FightSystem.getMessage().send(message, player, params)); } - public void broadcastChat(Player sender, String message) { - broadcast(player -> FightSystem.getMessage().sendPrefixless("TEAM_CHAT", player, ChatMessageType.CHAT, prefix, sender.getName(), message)); - } - private void broadcast(Consumer f) { players.forEach((uuid, fightPlayer) -> { Player player = Bukkit.getPlayer(uuid); diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/listener/Chat.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/listener/Chat.java index 617684d89..d7d19338b 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/listener/Chat.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/listener/Chat.java @@ -19,50 +19,41 @@ package de.steamwar.fightsystem.listener; -import de.steamwar.fightsystem.ArenaMode; import de.steamwar.fightsystem.Config; import de.steamwar.fightsystem.FightSystem; import de.steamwar.fightsystem.fight.Fight; import de.steamwar.fightsystem.fight.FightTeam; import de.steamwar.fightsystem.record.GlobalRecorder; -import de.steamwar.fightsystem.states.FightState; -import de.steamwar.fightsystem.states.StateDependentListener; import de.steamwar.linkage.Linked; +import de.steamwar.network.packets.PacketHandler; +import de.steamwar.network.packets.server.ChatRecordPacket; +import de.steamwar.sql.SteamwarUser; +import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.AsyncPlayerChatEvent; import java.util.logging.Level; +/** + * Chat is handled by Velocity. Messages written on this server are only sent here to be recorded for the replay. + */ @Linked -public class Chat implements Listener { +public class Chat extends PacketHandler { - public Chat() { - new StateDependentListener(ArenaMode.All, FightState.All, this); - } + @Handler + public void handleChatRecord(ChatRecordPacket packet) { + SteamwarUser user = SteamwarUser.byId(packet.getPlayerId()); + String message = packet.getMessage(); + FightSystem.getPlugin().getLogger().log(Level.INFO, user.getUserName() + "» " + message); - @EventHandler - public void handlePlayerChat(AsyncPlayerChatEvent event) { - Player player = event.getPlayer(); - String message = event.getMessage(); - - FightSystem.getPlugin().getLogger().log(Level.INFO, player.getName() + "» " + message); - FightTeam fightTeam = Fight.getPlayerTeam(player); + Player player = Bukkit.getPlayer(user.getUUID()); + FightTeam fightTeam = player != null ? Fight.getPlayerTeam(player) : null; if (fightTeam != null) { - String teamName = fightTeam.getColoredName(); - if (message.startsWith(Config.GameModeConfig.TeamChatPrefix)) { - fightTeam.broadcastChat(player, message.substring(1)); - } else { - broadcastChat("PARTICIPANT_CHAT", teamName, player.getName(), message); - } - } else if (Config.isReferee(player)) { - broadcastChat("FIGHTLEADER_CHAT", player.getName(), message); + GlobalRecorder.getInstance().chat("PARTICIPANT_CHAT", fightTeam.getColoredName(), user.getUserName(), message); + } else if (player != null && Config.isReferee(player)) { + GlobalRecorder.getInstance().chat("FIGHTLEADER_CHAT", user.getUserName(), message); } else { - broadcastChat("SPECTATOR_CHAT", player.getName(), message); + GlobalRecorder.getInstance().chat("SPECTATOR_CHAT", user.getUserName(), message); } - - event.setCancelled(true); } public static void broadcastChat(String message, Object... params) { diff --git a/MissileWars/src/de/steamwar/misslewars/listener/ChatListener.java b/MissileWars/src/de/steamwar/misslewars/listener/ChatListener.java deleted file mode 100644 index cf7a991c5..000000000 --- a/MissileWars/src/de/steamwar/misslewars/listener/ChatListener.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * This file is a part of the SteamWar software. - * - * Copyright (C) 2025 SteamWar.de-Serverteam - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package de.steamwar.misslewars.listener; - -import de.steamwar.linkage.Linked; -import de.steamwar.misslewars.FightState; -import de.steamwar.misslewars.MWTeam; -import de.steamwar.misslewars.MissileWars; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.player.AsyncPlayerChatEvent; - -import java.util.EnumSet; - -@Linked -public class ChatListener extends BasicListener { - - public ChatListener() { - super(EnumSet.allOf(FightState.class)); - } - - @EventHandler - public void onChat(AsyncPlayerChatEvent e) { - e.setCancelled(true); - - Player p = e.getPlayer(); - MWTeam team = MissileWars.getTeam(p); - String message = e.getPlayer().getDisplayName() + "§8» §7" + e.getMessage(); - if (team == null) { - Bukkit.broadcastMessage(message); - } else { - if (e.getMessage().startsWith("+")) { - String teamMessage = "§8[" + team.getColorCode() + "TEAM§8] " + e.getPlayer().getDisplayName() + "§8» §7" + e.getMessage().substring(1); - team.getPlayers().forEach(player -> player.sendMessage(teamMessage)); - } else { - Bukkit.broadcastMessage(message); - } - } - } -} diff --git a/SpigotCore/SpigotCore_Main/src/SpigotCore.properties b/SpigotCore/SpigotCore_Main/src/SpigotCore.properties index e7e13eeb2..566226600 100644 --- a/SpigotCore/SpigotCore_Main/src/SpigotCore.properties +++ b/SpigotCore/SpigotCore_Main/src/SpigotCore.properties @@ -16,7 +16,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # -LOCAL_CHAT = §eLocal §r{0}§8»§7 {1} COMMAND_SYSTEM_ERROR = §cError executing the command! diff --git a/SpigotCore/SpigotCore_Main/src/SpigotCore_de.properties b/SpigotCore/SpigotCore_Main/src/SpigotCore_de.properties index b7cba3c4f..c1cf8e755 100644 --- a/SpigotCore/SpigotCore_Main/src/SpigotCore_de.properties +++ b/SpigotCore/SpigotCore_Main/src/SpigotCore_de.properties @@ -16,7 +16,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # -LOCAL_CHAT = §eLokal §r{0}§8»§7 {1} COMMAND_SYSTEM_ERROR = §cFehler beim Ausführen des Befehls! diff --git a/TNTLeague/src/de/steamwar/tntleague/TNTLeague.properties b/TNTLeague/src/de/steamwar/tntleague/TNTLeague.properties index 311e52637..fc06646ac 100644 --- a/TNTLeague/src/de/steamwar/tntleague/TNTLeague.properties +++ b/TNTLeague/src/de/steamwar/tntleague/TNTLeague.properties @@ -53,6 +53,3 @@ DEALER_REDSTONE = §eRedstone DEALER_BLOCKS = §eBlocks DEALER_TOOLS = §eTools DEALER_ANGLES = §eAngle Blocks - -PARTICIPANT_CHAT = {0} {1}§8» §7{2} -SPECTATOR_CHAT = §7{0}§8» §7{1} \ No newline at end of file diff --git a/TNTLeague/src/de/steamwar/tntleague/events/GlobalListener.kt b/TNTLeague/src/de/steamwar/tntleague/events/GlobalListener.kt index b5f361acc..c03538124 100644 --- a/TNTLeague/src/de/steamwar/tntleague/events/GlobalListener.kt +++ b/TNTLeague/src/de/steamwar/tntleague/events/GlobalListener.kt @@ -22,7 +22,6 @@ package de.steamwar.tntleague.events import de.steamwar.tntleague.config.TNTLeagueWorldConfig import de.steamwar.tntleague.game.TNTLeagueGame import de.steamwar.tntleague.game.TNTLeagueTeam -import de.steamwar.tntleague.message import de.steamwar.tntleague.plugin import org.bukkit.GameMode import org.bukkit.Material @@ -90,20 +89,4 @@ object GlobalListener : Listener { null -> e.respawnLocation = TNTLeagueWorldConfig.lobby } } - - - @EventHandler - fun handlePlayerChat(event: AsyncPlayerChatEvent) { - val player = event.player - val msg = event.message - - val fightTeam = TNTLeagueGame.getTeam(player) - if (fightTeam != null) { - message.broadcastPrefixless("PARTICIPANT_CHAT", fightTeam.name, player.name, msg) - } else { - message.broadcastPrefixless("SPECTATOR_CHAT", player.name, msg) - } - - event.isCancelled = true - } } \ No newline at end of file diff --git a/TowerRun/src/TowerRun.properties b/TowerRun/src/TowerRun.properties index f008ae6cf..55fe89eb1 100644 --- a/TowerRun/src/TowerRun.properties +++ b/TowerRun/src/TowerRun.properties @@ -34,6 +34,3 @@ GAME_TIME = §a{0}:{1} CATCH_UP_WARNING = §4!! §cYou should catch up §4!! COMMAND_START = §aThe game will start soon§8! - -PARTICIPANT_CHAT = {0} {1}§8» §7{2} -SPECTATOR_CHAT = §7{0}§8» §7{1} \ No newline at end of file diff --git a/TowerRun/src/de/steamwar/towerrun/listener/GlobalListener.java b/TowerRun/src/de/steamwar/towerrun/listener/GlobalListener.java index 96248e403..324fddcad 100644 --- a/TowerRun/src/de/steamwar/towerrun/listener/GlobalListener.java +++ b/TowerRun/src/de/steamwar/towerrun/listener/GlobalListener.java @@ -20,19 +20,15 @@ package de.steamwar.towerrun.listener; import de.steamwar.linkage.Linked; -import de.steamwar.towerrun.TowerRun; import de.steamwar.towerrun.config.WorldConfig; import de.steamwar.towerrun.game.TowerRunGame; import de.steamwar.towerrun.game.TowerRunPlayer; -import de.steamwar.towerrun.state.GameState; import de.steamwar.towerrun.state.GameStateBukkitListener; import de.steamwar.towerrun.state.GameStates; import org.bukkit.Bukkit; -import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerRespawnEvent; @@ -71,25 +67,4 @@ public class GlobalListener extends GameStateBukkitListener implements Listener public void onPlayerRespawn(PlayerRespawnEvent event) { event.setRespawnLocation(WorldConfig.SPAWN); } - - @EventHandler - public void onAsyncPlayerChat(AsyncPlayerChatEvent event) { - TowerRunPlayer player = TowerRunPlayer.get(event.getPlayer()); - if (GameState.getCurrentState() == GameStates.RUNNING && (TowerRunGame.isAlive(player) || TowerRunGame.isEscaped(player))) { - String prefix; - if (TowerRunGame.isAlive(player)) { - if (event.getPlayer().getGameMode() == GameMode.SPECTATOR) { - prefix = "§c☠"; - } else { - prefix = "§6❤"; - } - } else { - prefix = "§a✔"; - } - TowerRun.getMessage().broadcastPrefixless("PARTICIPANT_CHAT", prefix, event.getPlayer().getName(), event.getMessage()); - } else { - TowerRun.getMessage().broadcastPrefixless("SPECTATOR_CHAT", event.getPlayer().getName(), event.getMessage()); - } - event.setCancelled(true); - } } diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index 6afbaf7e9..bff90140c 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -574,12 +574,36 @@ CHAT_BC_USAGE = §8/§7bc §8[§emessage§8] CHAT_NO_RECEIVER = §cNobody receives your message CHAT_EMPTY = §cDon\'t write meaningless empty messages. -CHAT_SERVERTEAM = §8STC §e{0}§8» §f{2} -CHAT_DISCORD_SERVERTEAM = §8STC §e{0}§8» §f{2} +CHAT_SERVERTEAM = §e{0}§8» §f{2} CHAT_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_DISCORD_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_TEAM = §8TC §e{0}§8» §f{2} +CHAT_LOCAL = {3}{4}{5}{6}{0}§8» {7}{2} +CHAT_TEAM = §e{0}§8» §f{2} CHAT_MSG = §e{0}§8»§e{1} §7{2} +CHAT_FIGHT_PARTICIPANT = {0} {1}§8» §7{2} +CHAT_FIGHT_SPECTATOR = §7{0}§8» §7{1} +CHAT_FIGHT_REFEREE = §e{0}§8» §e{1} +CHAT_FIGHT_TEAM = {0} {1}§8» §f{2} +CHAT_TAGGED = §8[{0}§8] §r{1} +CHAT_CHANNEL_GLOBAL = §eGlobal +CHAT_CHANNEL_ARENA = §e{0} +CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} +CHAT_CHANNEL_TEAM = §eTC +CHAT_CHANNEL_SERVERTEAM = §eSTC +CHAT_CHANNEL_MSG = §eMSG §7{0} +CHAT_CHANNEL_ALERT = §cAlert +CHAT_CHANNEL_LOCAL = §eLocal +CHAT_CHANNEL_CURRENT = §7You are writing into §e{0}§7. +CHAT_CHANNEL_SWITCHED = §7You are now writing into §e{0}§7. +CHAT_CHANNEL_CLOSED = §e{0} §7was closed. +CHAT_CHANNEL_NOT_SWITCHABLE = §cYou cannot write into this channel by default. +CHAT_NO_WRITE = §cYou cannot write into this channel. +CHAT_FIGHT_NO_TEAM = §cYou are not in a fight team. +GLOBAL_USAGE = §8/§7global §8[§emessage§8] +CHAT_USAGE = §8/§7chat §8[§echannel§8] [§ejoin§8|§eleave§8|§emsg §8<§etext§8>] +CHAT_CHANNEL_UNKNOWN = §cUnknown chat channel. +CHAT_CHANNEL_JOINED = §7You are reading §e{0} §7again. +CHAT_CHANNEL_LEFT = §7You no longer read §e{0}§7. +CHAT_CHANNEL_LEAVE_DEFAULT = §cYou cannot leave the channel you are writing into. Switch to another channel first. #CheckListner CHECK_UNCHECKED = §7You still have §e{0} §7unchecked schematic§8(§7s§8)! diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 5921259a9..3f34d8889 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -539,12 +539,36 @@ CHAT_BC_USAGE = §8/§7bc §8[§eNachricht§8] CHAT_NO_RECEIVER = §cNiemand empfängt deine Nachricht CHAT_EMPTY = §cSchreibe keine inhaltslosen Nachrichten. -CHAT_SERVERTEAM = §8STC §e{0}§8» §f{2} -CHAT_DISCORD_SERVERTEAM = §8STC §e{0}§8» §f{2} +CHAT_SERVERTEAM = §e{0}§8» §f{2} CHAT_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_DISCORD_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_TEAM = §8TC §e{0}§8» §f{2} +CHAT_LOCAL = {3}{4}{5}{6}{0}§8» {7}{2} +CHAT_TEAM = §e{0}§8» §f{2} CHAT_MSG = §e{0}§8»§e{1} §7{2} +CHAT_FIGHT_PARTICIPANT = {0} {1}§8» §7{2} +CHAT_FIGHT_SPECTATOR = §7{0}§8» §7{1} +CHAT_FIGHT_REFEREE = §e{0}§8» §e{1} +CHAT_FIGHT_TEAM = {0} {1}§8» §f{2} +CHAT_TAGGED = §8[{0}§8] §r{1} +CHAT_CHANNEL_GLOBAL = §eGlobal +CHAT_CHANNEL_ARENA = §e{0} +CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} +CHAT_CHANNEL_TEAM = §eTC +CHAT_CHANNEL_SERVERTEAM = §eSTC +CHAT_CHANNEL_MSG = §eMSG §7{0} +CHAT_CHANNEL_ALERT = §cAlert +CHAT_CHANNEL_LOCAL = §eLokal +CHAT_CHANNEL_CURRENT = §7Du schreibst in §e{0}§7. +CHAT_CHANNEL_SWITCHED = §7Du schreibst jetzt in §e{0}§7. +CHAT_CHANNEL_CLOSED = §e{0} §7wurde geschlossen. +CHAT_CHANNEL_NOT_SWITCHABLE = §cIn diesen Kanal kannst du nicht standardmäßig schreiben. +CHAT_NO_WRITE = §cDu kannst nicht in diesen Kanal schreiben. +CHAT_FIGHT_NO_TEAM = §cDu bist in keinem Kampfteam. +GLOBAL_USAGE = §8/§7global §8[§eNachricht§8] +CHAT_USAGE = §8/§7chat §8[§eKanal§8] [§ejoin§8|§eleave§8|§emsg §8<§eText§8>] +CHAT_CHANNEL_UNKNOWN = §cUnbekannter Chatkanal. +CHAT_CHANNEL_JOINED = §7Du liest §e{0} §7wieder mit. +CHAT_CHANNEL_LEFT = §7Du liest §e{0} §7nicht mehr mit. +CHAT_CHANNEL_LEAVE_DEFAULT = §cDu kannst den Kanal, in den du schreibst, nicht verlassen. Wechsle zuerst in einen anderen Kanal. #CheckListner CHECK_UNCHECKED = §7Du hast noch §e{0} §7ungeprüfte Schematic§8(§7s§8)! diff --git a/VelocityCore/src/de/steamwar/messages/Chatter.java b/VelocityCore/src/de/steamwar/messages/Chatter.java index 5cc74f23b..fc34a4f7f 100644 --- a/VelocityCore/src/de/steamwar/messages/Chatter.java +++ b/VelocityCore/src/de/steamwar/messages/Chatter.java @@ -23,8 +23,6 @@ import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.event.ResultedEvent; import com.velocitypowered.api.event.connection.LoginEvent; import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.ServerConnection; -import de.steamwar.persistent.Subserver; import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.UserPerm; import de.steamwar.velocitycore.VelocityCore; @@ -61,13 +59,6 @@ public interface Chatter { return new ChatterGroup(allStream()); } - static ChatterGroup globalChat() { - return new ChatterGroup(Stream.concat(Stream.of(Chatter.console()), allPlayers().filter(player -> { - Subserver subserver = Subserver.getSubserver(player); - return !(Subserver.isArena(subserver) && subserver.getServer() == player.getCurrentServer().map(ServerConnection::getServerInfo).orElse(null)); - }).map(Chatter::of))); - } - static ChatterGroup serverteam() { return new ChatterGroup(allStream() .filter(player -> player.user().hasPerm(UserPerm.TEAM)) diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java new file mode 100644 index 000000000..2c194f888 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java @@ -0,0 +1,70 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.UserPerm; + +import java.util.Arrays; +import java.util.stream.Stream; + +public class AlertChannel extends ChatChannel { + + @Override + public boolean switchable() { + return false; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_ALERT"); + } + + @Override + public Stream readers() { + return Arrays.stream(Chatter.broadcast().getChatters()); + } + + @Override + public boolean canWrite(Chatter sender) { + if (!sender.user().hasPerm(UserPerm.MODERATION)) { + sender.system("CHAT_NO_WRITE"); + return false; + } + return true; + } + + @Override + protected Message format(Chatter receiver, Chatter sender, String message) { + return new Message("ALERT", message.replace('&', '§')); + } + + /** + * Alerts are system messages: they are shown even with hidden chat and bypass the chat filter. + */ + @Override + public void send(Chatter sender, String message, boolean mirror) { + if (!canWrite(sender)) return; + + readers().forEach(reader -> reader.send(true, null, null, format(reader, sender, message))); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java new file mode 100644 index 000000000..8c3be5f98 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java @@ -0,0 +1,105 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Stream; + +public abstract class ChatChannel { + + /** + * @return the id under which this channel is stored as default channel of a player, null if it cannot be stored + */ + public String persistId() { + return null; + } + + public boolean switchable() { + return true; + } + + /** + * @return the channel name shown in front of messages of this channel if it is not the default channel of the receiver + */ + public abstract Message tag(Chatter receiver); + + public abstract Stream readers(); + + /** + * Checks if the sender is allowed to write into this channel and informs the sender otherwise. + */ + public abstract boolean canWrite(Chatter sender); + + protected abstract Message format(Chatter receiver, Chatter sender, String message); + + /** + * Called after a message was delivered to all readers. + * + * @param mirror false if the message originates from the mirror of this channel and should not be sent back + * @return true if the message was delivered to a receiver outside the game + */ + protected boolean delivered(Chatter sender, String message, boolean mirror) { + return false; + } + + public void audit(SteamwarUser sender, String message) { + AuditLog.createChat(AuditLog.SERVER_NAME_VELOCITY, null, sender, message); + } + + public void send(Chatter sender, String message) { + send(sender, message, true); + } + + public void send(Chatter sender, String message, boolean mirror) { + if (!canWrite(sender)) return; + + SteamwarUser user = sender.user(); + String coloredMessage = user.hasPerm(UserPerm.COLOR_CHAT) ? message.replace('&', '§') : message; + if (ChatFilter.filtered(sender, coloredMessage)) return; + + Map receivers = new LinkedHashMap<>(); + readers().forEach(reader -> receivers.putIfAbsent(reader.user().getId(), reader)); + receivers.putIfAbsent(user.getId(), sender); + + boolean noReceiver = true; + for (Chatter receiver : receivers.values()) { + if (!receiver.chatShown()) continue; + + Message msg = format(receiver, sender, coloredMessage); + if (ChatManager.getDefault(receiver) != this) { + msg = new Message("CHAT_TAGGED", tag(receiver), msg); + } + receiver.send(false, null, null, msg); + + if (receiver.user().getId() != user.getId()) noReceiver = false; + } + + if (!delivered(sender, coloredMessage, mirror) && noReceiver) { + sender.system("CHAT_NO_RECEIVER"); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java new file mode 100644 index 000000000..efe8ebb2f --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java @@ -0,0 +1,76 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.Punishment; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.commands.PunishmentCommand; + +import java.util.concurrent.TimeUnit; + +public class ChatFilter { + + private ChatFilter() {} + + public static boolean filtered(Chatter sender, String message) { + if (!sender.chatShown()) { + sender.system("CHAT_RECEIVE"); + return true; + } + + if (message.replace("§[a-f0-9klmno]", "").trim().isEmpty()) { + sender.system("CHAT_EMPTY"); + return true; + } + + SteamwarUser user = sender.user(); + if (!user.hasPerm(UserPerm.TEAM) && (message.contains("http:") || message.contains("https:") || message.contains("www."))) { + sender.system("CHAT_NO_LINKS"); + return true; + } + + if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.Mute)) return true; + + if (message.contains("LIXFEL")) { + specialAlert(sender, "Lixfel", "CHAT_LIXFEL_", 3, 6, 11, 12, 15); + } + if (message.contains("YOYONOW")) { + specialAlert(sender, "YoyoNow", "CHAT_YOYONOW_", 3, 6, 11, 12); + } + if (message.contains("CHAOSCAOT")) { + specialAlert(sender, "Chaoscaot", "CHAT_CHAOSCAOT_", 3, 6, 11, 12, 15, 17); + } + + return false; + } + + private static void specialAlert(Chatter sender, String name, String baseMessage, int... delay) { + sender.system("CHAT_LIXFEL_ACTION_BAR"); + for (int i = 0; i < delay.length; i++) { + int finalI = i; + VelocityCore.schedule(() -> sender.prefixless("CHAT_MSG", name, sender.user(), new Message(baseMessage + (finalI + 1)))).delay(delay[i], TimeUnit.SECONDS).schedule(); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java new file mode 100644 index 000000000..28a114f29 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java @@ -0,0 +1,94 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.network.packets.server.PingPacket; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.Team; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.network.NetworkSender; +import de.steamwar.velocitycore.util.SteamwarPrefix; + +import java.util.Arrays; +import java.util.stream.Collectors; + +public class ChatFormat { + + private ChatFormat() {} + + /** + * Formats a message with the rank and team prefixes of the sender. + */ + public static Message player(String format, Chatter receiver, Chatter sender, Chatter msgReceiver, String message) { + SteamwarUser user = sender.user(); + UserPerm.Prefix prefix = user.prefix(); + + String teamPrefix = ""; + if (user.hasPerm(UserPerm.TEAM) && useSwPrefix(user)) { + teamPrefix = "§eS§8W "; + } else if (user.getTeam() != 0) { + Team team = Team.byId(user.getTeam()); + teamPrefix = "§" + team.getTeamColor() + team.getTeamKuerzel() + " "; + } + + String chatColorCode = user.hasPerm(UserPerm.TEAM) ? "§f" : "§7"; + return new Message(format, + user, + msgReceiver == null ? receiver : msgReceiver, + highlightMentions(message, chatColorCode, receiver), + teamPrefix, + "", + prefix == UserPerm.emptyPrefix ? "§f" : prefix.getColorCode(), + prefix.getChatPrefix().length() == 0 ? "§f" : prefix.getChatPrefix() + " ", + chatColorCode); + } + + private static boolean useSwPrefix(SteamwarUser user) { + if (!user.hasPerm(UserPerm.TEAM)) { + return false; + } + + UserPerm.Prefix prefix = user.prefix(); + + boolean hasTeamPrefix = user.getTeam() != 0; + boolean hasSwPrefix = prefix != UserPerm.emptyPrefix; + + if (!hasSwPrefix) return false; + if (!hasTeamPrefix) return true; + + return SteamwarPrefix.usesSWPrefix(user); + } + + public static String highlightMentions(String message, String returnColor, Chatter player) { + if (!message.contains("@")) return message; + + String mark = "@" + player.user().getUserName(); + return Arrays.stream(message.split(" ")).map(cur -> { + if (cur.equalsIgnoreCase(mark) && player.getPlayer() != null) { + NetworkSender.send(player.getPlayer(), new PingPacket(player.user().getId())); + return "§e" + cur + returnColor; + } + return cur; + }).collect(Collectors.joining(" ")); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java new file mode 100644 index 000000000..a437532a1 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -0,0 +1,400 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.player.ServerPostConnectEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.linkage.Linked; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.persistent.Subserver; +import de.steamwar.sql.Event; +import de.steamwar.sql.Referee; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserConfig; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.EventStarter; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.listeners.BasicListener; +import lombok.Getter; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +/** + * Keeps track of all chat channels and the default channel every player writes into. + */ +@Linked +public class ChatManager extends BasicListener { + + private static final String CONFIG_KEY = "chat.channel"; + private static final String HIDDEN_KEY = "chat.hidden"; + private static final long REFEREE_CACHE_MILLIS = 60_000; + + @Getter + private static final GlobalChannel global = new GlobalChannel(); + @Getter + private static final ServerTeamChannel serverTeam = new ServerTeamChannel(); + @Getter + private static final AlertChannel alert = new AlertChannel(); + + private static final Map locals = new ConcurrentHashMap<>(); + private static final Map fightTeams = new ConcurrentHashMap<>(); + private static final Map fightInfos = new ConcurrentHashMap<>(); + private static final Map teams = new ConcurrentHashMap<>(); + private static final Set directs = ConcurrentHashMap.newKeySet(); + + private static final Map defaults = new ConcurrentHashMap<>(); + private static final Map persisted = new ConcurrentHashMap<>(); + private static final Set globalOnArena = ConcurrentHashMap.newKeySet(); + private static final Set announced = ConcurrentHashMap.newKeySet(); + private static final Map> spies = new ConcurrentHashMap<>(); + private static final Map> hidden = new ConcurrentHashMap<>(); + + private static int refereeEvent = -1; + private static long refereesLoaded = 0; + private static Set referees = Collections.emptySet(); + + public static LocalChannel local(RegisteredServer server) { + locals.keySet().removeIf(ChatManager::unregistered); + return locals.computeIfAbsent(server, LocalChannel::new); + } + + public static FightTeamChannel fightTeam(RegisteredServer server, boolean blue) { + fightTeams.keySet().removeIf(ChatManager::unregistered); + return fightTeams.computeIfAbsent(server, s -> new FightTeamChannel[]{new FightTeamChannel(s, true), new FightTeamChannel(s, false)})[blue ? 0 : 1]; + } + + public static TeamChannel team(int team) { + return teams.computeIfAbsent(team, TeamChannel::new); + } + + public static DirectChannel direct(UUID first, UUID second) { + for (DirectChannel channel : directs) { + if (channel.contains(first) && channel.partner(first).equals(second)) { + return channel; + } + } + + DirectChannel channel = new DirectChannel(first, second); + directs.add(channel); + return channel; + } + + private static boolean unregistered(RegisteredServer server) { + return VelocityCore.getProxy().getServer(server.getServerInfo().getName()).orElse(null) != server; + } + + public static boolean isArena(RegisteredServer server) { + return Subserver.isArena(Subserver.getSubserver(server.getServerInfo())); + } + + private static Optional currentServer(Player player) { + return player.getCurrentServer().map(ServerConnection::getServer); + } + + static boolean readsGlobal(Player player) { + return !currentServer(player).map(ChatManager::isArena).orElse(false) || globalOnArena.contains(player.getUniqueId()); + } + + static Stream spies(RegisteredServer server) { + return spies.entrySet().stream() + .filter(entry -> entry.getValue().contains(server)) + .map(entry -> VelocityCore.getProxy().getPlayer(entry.getKey()).orElse(null)) + .filter(Objects::nonNull) + .map(Chatter::of) + .filter(chatter -> chatter.user().hasPerm(UserPerm.MODERATION)) + .map(Chatter.class::cast); + } + + /** + * @return true if the user is a referee of the event fight running on the server + */ + static boolean isReferee(RegisteredServer server, int userId) { + Subserver subserver = Subserver.getSubserver(server.getServerInfo()); + if (subserver == null || !EventStarter.getEventServer().containsValue(subserver)) return false; + + Event event = Event.get(); + return event != null && referees(event.getEventID()).contains(userId); + } + + private static synchronized Set referees(int eventId) { + if (eventId != refereeEvent || System.currentTimeMillis() - refereesLoaded > REFEREE_CACHE_MILLIS) { + referees = Referee.get(eventId); + refereeEvent = eventId; + refereesLoaded = System.currentTimeMillis(); + } + return referees; + } + + private static Set hidden(UUID uuid) { + return hidden.computeIfAbsent(uuid, u -> { + Set channels = ConcurrentHashMap.newKeySet(); + String config = UserConfig.getConfig(u, HIDDEN_KEY); + if (config != null && !config.isEmpty()) { + channels.addAll(Arrays.asList(config.split(","))); + } + return channels; + }); + } + + /** + * @return false if the player left the channel with the given persist id as reader + */ + static boolean reads(Chatter chatter, String persistId) { + Player player = chatter.getPlayer(); + return player == null || !hidden(player.getUniqueId()).contains(persistId); + } + + private static void saveHidden(UUID uuid) { + Set channels = hidden(uuid); + UserConfig.updatePlayerConfig(uuid, HIDDEN_KEY, channels.isEmpty() ? null : String.join(",", channels)); + } + + /** + * @return true if the channel is the fight chat of an arena the player is not on, which moderators can read without joining the arena + */ + private static boolean isForeignArena(PlayerChatter player, ChatChannel channel) { + return channel instanceof LocalChannel local && local.isArena() && !local.getServer().getPlayersConnected().contains(player.getPlayer()); + } + + public static void join(PlayerChatter player, ChatChannel channel) { + UUID uuid = player.user().getUUID(); + if (isForeignArena(player, channel)) { + spies.computeIfAbsent(uuid, u -> ConcurrentHashMap.newKeySet()).add(((LocalChannel) channel).getServer()); + player.system("CHAT_CHANNEL_JOINED", channel.tag(player)); + return; + } + + if (hidden(uuid).remove(channel.persistId())) { + saveHidden(uuid); + } + player.system("CHAT_CHANNEL_JOINED", channel.tag(player)); + } + + public static void leave(PlayerChatter player, ChatChannel channel) { + UUID uuid = player.user().getUUID(); + if (isForeignArena(player, channel)) { + Set servers = spies.get(uuid); + if (servers != null) servers.remove(((LocalChannel) channel).getServer()); + player.system("CHAT_CHANNEL_LEFT", channel.tag(player)); + return; + } + + String id = channel.persistId(); + if (id.equals(getDefault(player).persistId())) { + player.system("CHAT_CHANNEL_LEAVE_DEFAULT"); + return; + } + + if (hidden(uuid).add(id)) { + saveHidden(uuid); + } + player.system("CHAT_CHANNEL_LEFT", channel.tag(player)); + } + + public static FightInfoPacket getFightInfo(RegisteredServer server) { + return fightInfos.get(server); + } + + /** + * Updates the fight teams of an arena. Players who just joined a fight team write into the fight chat by default. + */ + public static void updateFightInfo(RegisteredServer server, FightInfoPacket packet) { + fightInfos.keySet().removeIf(s -> s.getPlayersConnected().isEmpty()); + FightInfoPacket previous = fightInfos.put(server, packet); + + Set joined = new HashSet<>(packet.getBluePlayers()); + joined.addAll(packet.getRedPlayers()); + if (previous != null) { + previous.getBluePlayers().forEach(joined::remove); + previous.getRedPlayers().forEach(joined::remove); + } + + for (Player player : server.getPlayersConnected()) { + if (joined.contains(SteamwarUser.get(player.getUniqueId()).getId())) { + setDefault(Chatter.of(player), local(server)); + } + } + } + + /** + * @return the channel the receiver writes into by default; non-player chatters (console, Discord) use the global chat + */ + public static ChatChannel getDefault(Chatter chatter) { + Player player = chatter.getPlayer(); + if (player == null) return global; + + return defaults.computeIfAbsent(player.getUniqueId(), uuid -> initialDefault(player, currentServer(player).orElse(null))); + } + + private static ChatChannel initialDefault(Player player, RegisteredServer server) { + if (server != null && isArena(server)) return local(server); + return resolvePersisted(player, server); + } + + private static ChatChannel resolvePersisted(Player player, RegisteredServer server) { + String id = persisted.computeIfAbsent(player.getUniqueId(), uuid -> Optional.ofNullable(UserConfig.getConfig(uuid, CONFIG_KEY)).orElse(global.persistId())); + SteamwarUser user = SteamwarUser.get(player.getUniqueId()); + return switch (id) { + case "local" -> server != null && !isArena(server) ? local(server) : global; + case "team" -> user.getTeam() != 0 ? team(user.getTeam()) : global; + case "serverteam" -> user.hasPerm(UserPerm.TEAM) ? serverTeam : global; + default -> global; + }; + } + + /** + * Makes the channel the default channel of the player because the player chose it. + */ + public static void switchTo(PlayerChatter player, ChatChannel channel) { + if (!channel.switchable() || isForeignArena(player, channel)) { + player.system("CHAT_CHANNEL_NOT_SWITCHABLE"); + return; + } + if (!channel.canWrite(player)) return; + + UUID uuid = player.user().getUUID(); + if (channel == global && currentServer(player.getPlayer()).map(ChatManager::isArena).orElse(false)) { + globalOnArena.add(uuid); + } else if (channel != global) { + globalOnArena.remove(uuid); + } + + String id = channel.persistId(); + if (id != null && !id.equals(persisted.get(uuid))) { + persisted.put(uuid, id); + UserConfig.updatePlayerConfig(uuid, CONFIG_KEY, id); + } + if (id != null && hidden(uuid).remove(id)) { + saveHidden(uuid); + } + + defaults.put(uuid, channel); + player.system("CHAT_CHANNEL_SWITCHED", channel.tag(player)); + } + + private static void setDefault(PlayerChatter player, ChatChannel channel) { + UUID uuid = player.user().getUUID(); + if (channel != global) globalOnArena.remove(uuid); + + ChatChannel previous = defaults.put(uuid, channel); + if (announced.add(uuid)) { + player.system("CHAT_CHANNEL_CURRENT", channel.tag(player)); + } else if (previous != channel && !player.parseToPlain(previous.tag(player)).equals(player.parseToPlain(channel.tag(player)))) { + player.system("CHAT_CHANNEL_SWITCHED", channel.tag(player)); + } + } + + /** + * Handles a chat message the player typed without a command. + */ + public static void chat(PlayerChatter sender, String message) { + ChatChannel channel = getDefault(sender); + if (channel instanceof TeamChannel team && team.getTeam() != sender.user().getTeam()) { + channel = resolvePersisted(sender.getPlayer(), currentServer(sender.getPlayer()).orElse(null)); + setDefault(sender, channel); + } + + if (message.startsWith("+")) { + channel = quickChannel(sender, channel); + message = message.substring(1); + } + + channel.send(sender, message); + channel.audit(sender.user(), message); + } + + /** + * The channel a message prefixed with '+' is sent to: the local chat when writing into the global chat, + * the fight team chat when writing into the local/fight chat. + */ + private static ChatChannel quickChannel(PlayerChatter sender, ChatChannel current) { + RegisteredServer server = currentServer(sender.getPlayer()).orElse(null); + if (server == null) return current; + + if (current instanceof LocalChannel) { + FightTeamChannel blue = fightTeam(server, true); + if (blue.isMember(sender)) return blue; + + FightTeamChannel red = fightTeam(server, false); + if (red.isMember(sender)) return red; + + return global; + } + + return local(server); + } + + @Subscribe + public void onServerPostConnect(ServerPostConnectEvent event) { + Player player = event.getPlayer(); + RegisteredServer server = currentServer(player).orElse(null); + if (server == null) return; + + UUID uuid = player.getUniqueId(); + globalOnArena.remove(uuid); + + ChatChannel current = defaults.get(uuid); + ChatChannel next; + if (isArena(server)) { + next = local(server); + } else if (!announced.contains(uuid) || current instanceof LocalChannel || (event.getPreviousServer() != null && isArena(event.getPreviousServer()))) { + next = resolvePersisted(player, server); + } else { + next = current; + } + + setDefault(Chatter.of(player), next); + } + + @Subscribe + public void onDisconnect(DisconnectEvent event) { + UUID uuid = event.getPlayer().getUniqueId(); + defaults.remove(uuid); + persisted.remove(uuid); + globalOnArena.remove(uuid); + announced.remove(uuid); + spies.remove(uuid); + hidden.remove(uuid); + + Iterator it = directs.iterator(); + while (it.hasNext()) { + DirectChannel channel = it.next(); + if (!channel.contains(uuid)) continue; + it.remove(); + + Player partner = VelocityCore.getProxy().getPlayer(channel.partner(uuid)).orElse(null); + if (partner != null && defaults.get(partner.getUniqueId()) == channel) { + PlayerChatter chatter = Chatter.of(partner); + chatter.system("CHAT_CHANNEL_CLOSED", channel.tag(chatter)); + defaults.put(partner.getUniqueId(), resolvePersisted(partner, currentServer(partner).orElse(null))); + chatter.system("CHAT_CHANNEL_SWITCHED", defaults.get(partner.getUniqueId()).tag(chatter)); + } + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java new file mode 100644 index 000000000..b369bafda --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java @@ -0,0 +1,105 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.IgnoreSystem; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.velocitycore.VelocityCore; + +import java.util.UUID; +import java.util.stream.Stream; + +import static de.steamwar.persistent.Storage.lastChats; + +/** + * A temporary channel between two players, closed as soon as one of them goes offline. + */ +public class DirectChannel extends ChatChannel { + + private final UUID first; + private final UUID second; + + DirectChannel(UUID first, UUID second) { + this.first = first; + this.second = second; + } + + public boolean contains(UUID uuid) { + return first.equals(uuid) || second.equals(uuid); + } + + public UUID partner(UUID uuid) { + return first.equals(uuid) ? second : first; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_MSG", SteamwarUser.get(partner(receiver.user().getUUID()))); + } + + @Override + public Stream readers() { + return Stream.of(first, second) + .map(uuid -> VelocityCore.getProxy().getPlayer(uuid).orElse(null)) + .filter(player -> player != null) + .map(Chatter::of); + } + + @Override + public boolean canWrite(Chatter sender) { + UUID partner = partner(sender.user().getUUID()); + if (VelocityCore.getProxy().getPlayer(partner).isEmpty()) { + sender.system("MSG_OFFLINE"); + return false; + } + + if (IgnoreSystem.isIgnored(partner, sender.user().getUUID())) { + sender.system("MSG_IGNORED"); + return false; + } + return true; + } + + @Override + protected Message format(Chatter receiver, Chatter sender, String message) { + return ChatFormat.player("CHAT_MSG", receiver, sender, Chatter.of(partner(sender.user().getUUID())), message); + } + + @Override + protected boolean delivered(Chatter sender, String message, boolean mirror) { + Player senderPlayer = VelocityCore.getProxy().getPlayer(sender.user().getUUID()).orElse(null); + Player partnerPlayer = VelocityCore.getProxy().getPlayer(partner(sender.user().getUUID())).orElse(null); + if (senderPlayer != null && partnerPlayer != null) { + lastChats.put(senderPlayer, partnerPlayer); + lastChats.put(partnerPlayer, senderPlayer); + } + return false; + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createSensitiveCommand(AuditLog.SERVER_NAME_VELOCITY, null, sender, "/msg " + SteamwarUser.get(partner(sender.getUUID())).getUserName() + " " + message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java new file mode 100644 index 000000000..93b80e600 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java @@ -0,0 +1,101 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import lombok.Getter; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +/** + * The team chat of the blue or red team of a fight. + */ +public class FightTeamChannel extends ChatChannel { + + @Getter + private final RegisteredServer server; + private final boolean blue; + + FightTeamChannel(RegisteredServer server, boolean blue) { + this.server = server; + this.blue = blue; + } + + @Override + public boolean switchable() { + return false; + } + + private List members() { + FightInfoPacket info = ChatManager.getFightInfo(server); + if (info == null) return Collections.emptyList(); + return blue ? info.getBluePlayers() : info.getRedPlayers(); + } + + private String teamName() { + FightInfoPacket info = ChatManager.getFightInfo(server); + if (info == null) return ""; + return blue ? info.getBlueName() : info.getRedName(); + } + + public boolean isMember(Chatter chatter) { + return members().contains(chatter.user().getId()); + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_FIGHT_TEAM", server.getServerInfo().getName(), teamName()); + } + + @Override + public Stream readers() { + List members = members(); + return Stream.concat( + server.getPlayersConnected().stream().map(Chatter::of).filter(chatter -> members.contains(chatter.user().getId())), + ChatManager.spies(server)); + } + + @Override + public boolean canWrite(Chatter sender) { + if (!isMember(sender) || sender.getPlayer() == null || !server.getPlayersConnected().contains(sender.getPlayer())) { + sender.system("CHAT_FIGHT_NO_TEAM"); + return false; + } + return true; + } + + @Override + protected Message format(Chatter receiver, Chatter sender, String message) { + return new Message("CHAT_FIGHT_TEAM", teamName(), sender.user(), ChatFormat.highlightMentions(message, "§f", receiver)); + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createChat(server.getServerInfo().getName(), null, sender, "+" + message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java new file mode 100644 index 000000000..a4146c1d2 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java @@ -0,0 +1,63 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.velocitycore.discord.DiscordBot; + +import java.util.stream.Stream; + +public class GlobalChannel extends ChatChannel { + + @Override + public String persistId() { + return "global"; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_GLOBAL"); + } + + @Override + public Stream readers() { + return Stream.concat(Stream.of(Chatter.console()), Chatter.allPlayers().filter(ChatManager::readsGlobal).map(Chatter::of).filter(chatter -> ChatManager.reads(chatter, persistId()))); + } + + @Override + public boolean canWrite(Chatter sender) { + return true; + } + + @Override + protected Message format(Chatter receiver, Chatter sender, String message) { + return ChatFormat.player("CHAT_GLOBAL", receiver, sender, null, message); + } + + @Override + protected boolean delivered(Chatter sender, String message, boolean mirror) { + if (mirror) { + DiscordBot.withBot(bot -> bot.getIngameChat().send(false, null, null, format(bot.getIngameChat(), sender, message))); + } + return true; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java new file mode 100644 index 000000000..5eaf90aa9 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java @@ -0,0 +1,117 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.network.packets.server.ChatRecordPacket; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.network.NetworkSender; +import lombok.Getter; + +import java.util.stream.Stream; + +/** + * The chat of all players on one server. On arenas this is the fight chat. + */ +public class LocalChannel extends ChatChannel { + + @Getter + private final RegisteredServer server; + + LocalChannel(RegisteredServer server) { + this.server = server; + } + + public boolean isArena() { + return ChatManager.isArena(server); + } + + @Override + public String persistId() { + return "local"; + } + + @Override + public Message tag(Chatter receiver) { + if (isArena()) { + return new Message("CHAT_CHANNEL_ARENA", server.getServerInfo().getName()); + } + return new Message("CHAT_CHANNEL_LOCAL"); + } + + @Override + public Stream readers() { + Stream players = server.getPlayersConnected().stream().map(Chatter::of).filter(chatter -> ChatManager.reads(chatter, persistId())).map(Chatter.class::cast); + if (isArena()) { + players = Stream.concat(players, ChatManager.spies(server)); + } + return players; + } + + @Override + public boolean canWrite(Chatter sender) { + boolean onServer = sender.getPlayer() != null && server.getPlayersConnected().contains(sender.getPlayer()); + // Moderators can write into fight chats they are not on (/chatspy) + if (!onServer && !(isArena() && sender.user().hasPerm(UserPerm.MODERATION))) { + sender.system("CHAT_NO_WRITE"); + return false; + } + return true; + } + + @Override + protected Message format(Chatter receiver, Chatter sender, String message) { + if (!isArena()) { + return ChatFormat.player("CHAT_LOCAL", receiver, sender, null, message); + } + + String highlighted = ChatFormat.highlightMentions(message, "§7", receiver); + FightInfoPacket info = ChatManager.getFightInfo(server); + int id = sender.user().getId(); + if (info != null && info.getBluePlayers().contains(id)) { + return new Message("CHAT_FIGHT_PARTICIPANT", info.getBlueName(), sender.user(), highlighted); + } else if (info != null && info.getRedPlayers().contains(id)) { + return new Message("CHAT_FIGHT_PARTICIPANT", info.getRedName(), sender.user(), highlighted); + } else if (ChatManager.isReferee(server, id)) { + return new Message("CHAT_FIGHT_REFEREE", sender.user(), highlighted); + } + return new Message("CHAT_FIGHT_SPECTATOR", sender.user(), highlighted); + } + + @Override + protected boolean delivered(Chatter sender, String message, boolean mirror) { + if (isArena()) { + // Arenas never display chat from Velocity, they only record it for replays + NetworkSender.send(server, new ChatRecordPacket(sender.user().getId(), message)); + } + return false; + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createChat(server.getServerInfo().getName(), null, sender, message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java new file mode 100644 index 000000000..46950b1cb --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java @@ -0,0 +1,76 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.discord.DiscordBot; + +import java.util.Arrays; +import java.util.stream.Stream; + +public class ServerTeamChannel extends ChatChannel { + + @Override + public String persistId() { + return "serverteam"; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_SERVERTEAM"); + } + + @Override + public Stream readers() { + return Arrays.stream(Chatter.serverteam().getChatters()).filter(chatter -> ChatManager.reads(chatter, persistId())); + } + + @Override + public boolean canWrite(Chatter sender) { + if (!sender.user().hasPerm(UserPerm.TEAM)) { + sender.system("CHAT_NO_WRITE"); + return false; + } + return true; + } + + @Override + protected Message format(Chatter receiver, Chatter sender, String message) { + return ChatFormat.player("CHAT_SERVERTEAM", receiver, sender, null, message); + } + + @Override + protected boolean delivered(Chatter sender, String message, boolean mirror) { + if (mirror) { + DiscordBot.withBot(bot -> bot.getServerTeamChat().send(false, null, null, ChatFormat.player("CHAT_GLOBAL", bot.getServerTeamChat(), sender, null, message))); + } + return true; + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createCommand(AuditLog.SERVER_NAME_VELOCITY, null, sender, "/stc " + message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java new file mode 100644 index 000000000..f29a80f4d --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java @@ -0,0 +1,79 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.velocitycore.commands.StreamingCommand; +import lombok.Getter; + +import java.util.stream.Stream; + +public class TeamChannel extends ChatChannel { + + @Getter + private final int team; + + TeamChannel(int team) { + this.team = team; + } + + @Override + public String persistId() { + return "team"; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_TEAM"); + } + + @Override + public Stream readers() { + return Chatter.allPlayers() + .map(Chatter::of) + .filter(chatter -> chatter.user().getTeam() == team) + .filter(StreamingCommand::isNotStreaming) + .filter(chatter -> ChatManager.reads(chatter, persistId())) + .map(Chatter.class::cast); + } + + @Override + public boolean canWrite(Chatter sender) { + if (sender.user().getTeam() != team) { + sender.system("TC_NO_TEAM"); + return false; + } + return true; + } + + @Override + protected Message format(Chatter receiver, Chatter sender, String message) { + return ChatFormat.player("CHAT_TEAM", receiver, sender, null, message); + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createSensitiveCommand(AuditLog.SERVER_NAME_VELOCITY, null, sender, "/tc " + message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java index ea8467898..552691d2f 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java @@ -23,6 +23,7 @@ import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.chat.ChatManager; import de.steamwar.velocitycore.discord.DiscordBot; @Linked @@ -40,7 +41,7 @@ public class AlertCommand extends SWCommand { } String s = String.join(" ", message); - Chatter.broadcast().system("ALERT", s.replace('&', '§')); + ChatManager.getAlert().send(sender, s); if ("-discord".equals(sendToDiscord)) { DiscordBot.withBot(bot -> bot.getAnnouncementChannel().send(s)); diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java new file mode 100644 index 000000000..cd563a81c --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java @@ -0,0 +1,123 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.commands; + +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.command.PreviousArguments; +import de.steamwar.command.SWCommand; +import de.steamwar.command.TypeMapper; +import de.steamwar.linkage.Linked; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.persistent.Subserver; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.chat.ChatChannel; +import de.steamwar.velocitycore.chat.ChatManager; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@Linked +public class ChatCommand extends SWCommand { + + public ChatCommand() { + super("chat"); + } + + @Register(description = "CHAT_USAGE") + public void current(PlayerChatter sender) { + sender.system("CHAT_CHANNEL_CURRENT", ChatManager.getDefault(sender).tag(sender)); + } + + @Register + public void switchTo(PlayerChatter sender, ChatChannel channel) { + ChatManager.switchTo(sender, channel); + } + + @Register + public void joinOrLeave(PlayerChatter sender, ChatChannel channel, @StaticValue(value = {"leave", "join"}, allowISE = true) boolean join) { + if (join) { + ChatManager.join(sender, channel); + } else { + ChatManager.leave(sender, channel); + } + } + + @Register + public void msg(PlayerChatter sender, ChatChannel channel, @StaticValue("msg") String msg, @ErrorMessage(value = "CHAT_USAGE", allowEAs = false) String... message) { + if (!sender.user().hasPerm(UserPerm.MODERATION)) { + sender.system("CHAT_NO_WRITE"); + return; + } + + channel.send(sender, String.join(" ", message)); + } + + @ClassMapper(value = ChatChannel.class, local = true) + public TypeMapper channelTypeMapper() { + return new TypeMapper<>() { + @Override + public ChatChannel map(Chatter sender, PreviousArguments previousArguments, String s) { + return switch (s.toLowerCase()) { + case "global" -> ChatManager.getGlobal(); + case "local" -> sender.getPlayer() == null ? null : sender.getPlayer().getCurrentServer().map(ServerConnection::getServer).map(ChatManager::local).orElse(null); + case "team", "tc" -> sender.user().getTeam() != 0 ? ChatManager.team(sender.user().getTeam()) : null; + case "serverteam", "stc" -> sender.user().hasPerm(UserPerm.TEAM) ? ChatManager.getServerTeam() : null; + default -> arena(sender, s); + }; + } + + @Override + public boolean validate(Chatter sender, ChatChannel value, MessageSender messageSender) { + return !messageSender.send(value == null, "CHAT_CHANNEL_UNKNOWN"); + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + List channels = new ArrayList<>(List.of("global", "local")); + if (sender.user().getTeam() != 0) channels.add("team"); + if (sender.user().hasPerm(UserPerm.TEAM)) channels.add("serverteam"); + if (sender.user().hasPerm(UserPerm.MODERATION)) { + List subserverList = Subserver.getServerList(); + synchronized (subserverList) { + subserverList.stream().filter(Subserver::isArena).map(subserver -> subserver.getServer().getName()).forEach(channels::add); + } + } + return channels; + } + }; + } + + /** + * Moderators can address the fight chat of every arena by its server name. + */ + private static ChatChannel arena(Chatter sender, String name) { + if (!sender.user().hasPerm(UserPerm.MODERATION)) return null; + + Subserver subserver = Subserver.getSubserver(VelocityCore.getProxy().getServer(name).map(RegisteredServer::getServerInfo).orElse(null)); + if (!Subserver.isArena(subserver)) return null; + + return ChatManager.local(subserver.getRegisteredServer()); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java new file mode 100644 index 000000000..6ce66d439 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java @@ -0,0 +1,46 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.commands; + +import de.steamwar.command.SWCommand; +import de.steamwar.linkage.Linked; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.velocitycore.chat.ChatManager; + +@Linked +public class GlobalCommand extends SWCommand { + + public GlobalCommand() { + super("global"); + } + + @Register(description = "GLOBAL_USAGE") + public void genericCommand(Chatter sender, String... message) { + if (message.length > 0) { + ChatManager.getGlobal().send(sender, String.join(" ", message)); + } else if (sender instanceof PlayerChatter player) { + ChatManager.switchTo(player, ChatManager.getGlobal()); + } else { + sender.system("GLOBAL_USAGE"); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java index 40f48fbf6..ab52364de 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java @@ -19,10 +19,12 @@ package de.steamwar.velocitycore.commands; +import com.velocitypowered.api.proxy.ServerConnection; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.PlayerChatter; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatChannel; +import de.steamwar.velocitycore.chat.ChatManager; @Linked public class LocalCommand extends SWCommand { @@ -31,8 +33,15 @@ public class LocalCommand extends SWCommand { super("local", "bc", "bauchat"); } - @Register - public void genericCommand(PlayerChatter player, String... message) { - ChatListener.localChat(player, String.join(" ", message)); + @Register(description = "CHAT_BC_USAGE") + public void genericCommand(PlayerChatter sender, String... message) { + ChatChannel channel = sender.getPlayer().getCurrentServer().map(ServerConnection::getServer).map(ChatManager::local).orElse(null); + if (channel == null) return; + + if (message.length == 0) { + ChatManager.switchTo(sender, channel); + } else { + channel.send(sender, String.join(" ", message)); + } } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java index 622aa2dc5..67cbd21a4 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java @@ -22,12 +22,10 @@ package de.steamwar.velocitycore.commands; import com.velocitypowered.api.proxy.Player; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; -import de.steamwar.messages.Chatter; -import de.steamwar.messages.ChatterGroup; import de.steamwar.messages.PlayerChatter; -import de.steamwar.sql.IgnoreSystem; import de.steamwar.velocitycore.VelocityCore; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatChannel; +import de.steamwar.velocitycore.chat.ChatManager; import static de.steamwar.persistent.Storage.lastChats; @@ -39,7 +37,7 @@ public class MsgCommand extends SWCommand { } @Register(description = "MSG_USAGE") - public void genericCommand(PlayerChatter sender, @ErrorMessage(value = "MSG_OFFLINE") Player target, @ErrorMessage(value = "MSG_USAGE", allowEAs = false) String... message) { + public void genericCommand(PlayerChatter sender, @ErrorMessage(value = "MSG_OFFLINE") Player target, String... message) { msg(sender, target, message); } @@ -49,15 +47,12 @@ public class MsgCommand extends SWCommand { return; } - if (IgnoreSystem.isIgnored(target.getUniqueId(), sender.user().getUUID())) { - sender.system("MSG_IGNORED"); - return; + ChatChannel channel = ChatManager.direct(sender.user().getUUID(), target.getUniqueId()); + if (args.length == 0) { + ChatManager.switchTo(sender, channel); + } else { + channel.send(sender, String.join(" ", args)); } - - Chatter receiver = Chatter.of(target); - ChatListener.sendChat(sender, new ChatterGroup(sender, receiver), "CHAT_MSG", receiver, String.join(" ", args)); - lastChats.put(sender.getPlayer(), target); - lastChats.put(target, sender.getPlayer()); } public static void remove(Player player) { diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java index ae86f2492..65582e856 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java @@ -33,7 +33,7 @@ public class RCommand extends SWCommand { } @Register(description = "R_USAGE") - public void genericCommand(PlayerChatter sender, @ErrorMessage(value = "R_USAGE", allowEAs = false) String... message) { + public void genericCommand(PlayerChatter sender, String... message) { MsgCommand.msg(sender, lastChats.get(sender.getPlayer()), message); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java index 0b04183e0..ea3932b47 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java @@ -22,8 +22,9 @@ package de.steamwar.velocitycore.commands; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; import de.steamwar.sql.UserPerm; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatManager; @Linked public class ServerTeamchatCommand extends SWCommand { @@ -33,7 +34,13 @@ public class ServerTeamchatCommand extends SWCommand { } @Register(description = "STC_USAGE") - public void genericCommand(Chatter sender, @ErrorMessage(value = "STC_USAGE", allowEAs = false) String... message) { - ChatListener.sendChat(sender, Chatter.serverteam(), "CHAT_SERVERTEAM", null, String.join(" ", message)); + public void genericCommand(Chatter sender, String... message) { + if (message.length > 0) { + ChatManager.getServerTeam().send(sender, String.join(" ", message)); + } else if (sender instanceof PlayerChatter player) { + ChatManager.switchTo(player, ChatManager.getServerTeam()); + } else { + sender.system("STC_USAGE"); + } } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java index e9b1d4d26..1f6d8458d 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java @@ -22,9 +22,9 @@ package de.steamwar.velocitycore.commands; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; -import de.steamwar.messages.ChatterGroup; -import de.steamwar.sql.SteamwarUser; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.velocitycore.chat.ChatChannel; +import de.steamwar.velocitycore.chat.ChatManager; @Linked public class TeamchatCommand extends SWCommand { @@ -34,14 +34,20 @@ public class TeamchatCommand extends SWCommand { } @Register(description = "TC_USAGE") - public void genericCommand(Chatter sender, @ErrorMessage(value = "TC_USAGE", allowEAs = false) String... args) { - SteamwarUser user = sender.user(); - - if (user.getTeam() == 0) { + public void genericCommand(Chatter sender, String... args) { + int team = sender.user().getTeam(); + if (team == 0) { sender.system("TC_NO_TEAM"); return; } - ChatListener.sendChat(sender, new ChatterGroup(Chatter.allStream().filter(p -> p.user().getTeam() == user.getTeam()).filter(StreamingCommand::isNotStreaming)), "CHAT_TEAM", null, String.join(" ", args)); + ChatChannel channel = ChatManager.team(team); + if (args.length > 0) { + channel.send(sender, String.join(" ", args)); + } else if (sender instanceof PlayerChatter player) { + ChatManager.switchTo(player, channel); + } else { + sender.system("TC_USAGE"); + } } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java b/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java index 7e7dba1ed..4b557bb7b 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java @@ -21,6 +21,7 @@ package de.steamwar.velocitycore.discord; import de.steamwar.command.SWCommand; import de.steamwar.messages.Chatter; +import de.steamwar.velocitycore.chat.ChatManager; import de.steamwar.sql.Event; import de.steamwar.velocitycore.VelocityCore; import de.steamwar.velocitycore.discord.channels.*; @@ -181,9 +182,9 @@ public class DiscordBot { // There is a hard limit of 30 messages per minute to send as a webhook, thus with 8 webhooks we can send // 240 messages per minute. Which means 4 every second. I looked at the WGS fights and there were around // ~70 in a short burst and then rather long no new message. - ingameChat = new DiscordChatRoom(config.channel("ingame"), "CHAT_DISCORD_GLOBAL", Chatter::globalChat, 8); + ingameChat = new DiscordChatRoom(config.channel("ingame"), ChatManager.getGlobal(), 8); // 60 messages per minute should be enough for the server team! - serverTeamChat = new DiscordChatRoom(config.channel("serverteam"), "CHAT_DISCORD_SERVERTEAM", Chatter::serverteam, 2); + serverTeamChat = new DiscordChatRoom(config.channel("serverteam"), ChatManager.getServerTeam(), 2); VelocityCore.schedule(() -> { try { diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java index ef02ce11d..6c69a5164 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java @@ -20,22 +20,17 @@ package de.steamwar.velocitycore.discord.channels; import de.steamwar.messages.Chatter; -import de.steamwar.messages.ChatterGroup; import de.steamwar.sql.Punishment; import de.steamwar.sql.SteamwarUser; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatChannel; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import java.util.function.Supplier; - public class DiscordChatRoom extends DiscordChannel { - private final String format; - private final Supplier target; + private final ChatChannel target; - public DiscordChatRoom(String channel, String format, Supplier target, int maxNumberOfWebhooks) { + public DiscordChatRoom(String channel, ChatChannel target, int maxNumberOfWebhooks) { super(channel, maxNumberOfWebhooks); - this.format = format; this.target = target; } @@ -47,6 +42,6 @@ public class DiscordChatRoom extends DiscordChannel { return; } - ChatListener.sendChat(Chatter.of(user.getUUID(), true), target.get(), format, null, event.getMessage().getContentDisplay().replace('§', '&').replace('\n', ' ')); + target.send(Chatter.of(user.getUUID(), true), event.getMessage().getContentDisplay().replace('§', '&').replace('\n', ' '), false); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java index ad522fee7..1da67ed66 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java @@ -27,30 +27,18 @@ import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.event.player.TabCompleteEvent; import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.ServerConnection; -import com.velocitypowered.api.proxy.server.ServerInfo; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; -import de.steamwar.messages.ChatterGroup; -import de.steamwar.messages.Message; -import de.steamwar.messages.PlayerChatter; -import de.steamwar.network.packets.server.PingPacket; -import de.steamwar.persistent.Subserver; import de.steamwar.sql.*; import de.steamwar.velocitycore.ArenaMode; import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.chat.ChatManager; import de.steamwar.velocitycore.commands.PunishmentCommand; -import de.steamwar.velocitycore.discord.DiscordBot; -import de.steamwar.velocitycore.network.NetworkSender; -import de.steamwar.velocitycore.util.SteamwarPrefix; -import java.util.Arrays; import java.util.List; import java.util.Set; -import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Collectors; @Linked public class ChatListener extends BasicListener { @@ -138,21 +126,7 @@ public class ChatListener extends BasicListener { return; } - Subserver subserver = Subserver.getSubserver(player); - String serverName = AuditLog.SERVER_NAME_VELOCITY; - if (Subserver.isArena(subserver) && subserver.getServer() == player.getCurrentServer().orElseThrow().getServerInfo()) { - serverName = subserver.getServer().getName(); - localChat(Chatter.of(player), message); - } else if (message.startsWith("+")) { - serverName = player.getCurrentServer() - .map(ServerConnection::getServerInfo) - .map(ServerInfo::getName) - .orElse(serverName); - localChat(Chatter.of(player), message.substring(1)); - } else { - sendChat(Chatter.of(player), Chatter.globalChat(), "CHAT_GLOBAL", null, message); - } - AuditLog.createChat(serverName, null, user, message); + ChatManager.chat(Chatter.of(player), message); } private static boolean isMistypedCommand(Player player, String message) { @@ -172,115 +146,6 @@ public class ChatListener extends BasicListener { return isCommand; } - public static void sendChat(Chatter sender, ChatterGroup receivers, String format, Chatter msgReceiver, String message) { - SteamwarUser user = sender.user(); - final String coloredMessage = user.hasPerm(UserPerm.COLOR_CHAT) ? message.replace('&', '§') : message; - if (chatFilter(sender, coloredMessage)) return; - - boolean useSwPrefix = useSwPrefix(user); - - boolean noReceiver = true; - for (Chatter player : receivers.getChatters()) { - if (player.chatShown()) { - chatToReciever(player, msgReceiver, user, format, coloredMessage, useSwPrefix); - if (sender.user().getId() != player.user().getId()) noReceiver = false; - } - - } - - if (format.equals("CHAT_GLOBAL")) { - DiscordBot.withBot(bot -> chatToReciever(bot.getIngameChat(), msgReceiver, user, format, coloredMessage, useSwPrefix)); - } else if (format.equals("CHAT_SERVERTEAM")) { - DiscordBot.withBot(bot -> chatToReciever(bot.getServerTeamChat(), msgReceiver, user, "CHAT_GLOBAL", coloredMessage, useSwPrefix)); - } else if (noReceiver) { - sender.system("CHAT_NO_RECEIVER"); - } - } - - public static void localChat(PlayerChatter sender, String message) { - if (message.isEmpty()) { - sender.system("CHAT_BC_USAGE"); - return; - } - - if (ChatListener.filteredCommand(sender, message)) return; - - if (!message.startsWith("/") && chatFilter(sender, message)) return; - - sender.getPlayer().spoofChatInput(message); - } - - private static boolean chatFilter(Chatter sender, String message) { - if (!sender.chatShown()) { - sender.system("CHAT_RECEIVE"); - return true; - } - - if (message.replace("§[a-f0-9klmno]", "").trim().isEmpty()) { - sender.system("CHAT_EMPTY"); - return true; - } - - SteamwarUser user = sender.user(); - if (!user.hasPerm(UserPerm.TEAM) && (message.contains("http:") || message.contains("https:") || message.contains("www."))) { - sender.system("CHAT_NO_LINKS"); - return true; - } - - if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.Mute)) return true; - - if (message.contains("LIXFEL")) { - specialAlert(sender, "Lixfel", "CHAT_LIXFEL_", 3, 6, 11, 12, 15); - } - if (message.contains("YOYONOW")) { - specialAlert(sender, "YoyoNow", "CHAT_YOYONOW_", 3, 6, 11, 12); - } - if (message.contains("CHAOSCAOT")) { - specialAlert(sender, "Chaoscaot", "CHAT_CHAOSCAOT_", 3, 6, 11, 12, 15, 17); - } - - return false; - } - - private static void chatToReciever(Chatter receiver, Chatter msgReceiver, SteamwarUser sender, String format, String message, boolean useSwPrefix) { - UserPerm.Prefix prefix = sender.prefix(); - - String teamPrefix = ""; - if (sender.hasPerm(UserPerm.TEAM) && useSwPrefix) { - teamPrefix = "§eS§8W "; - } else if (sender.getTeam() != 0) { - Team team = Team.byId(sender.getTeam()); - teamPrefix = "§" + team.getTeamColor() + team.getTeamKuerzel() + " "; - } - - String chatColorCode = sender.hasPerm(UserPerm.TEAM) ? "§f" : "§7"; - receiver.prefixless(format, - sender, - msgReceiver == null ? receiver : msgReceiver, - highlightMentions(message, chatColorCode, receiver), - teamPrefix, - "", - prefix == UserPerm.emptyPrefix ? "§f" : prefix.getColorCode(), - prefix.getChatPrefix().length() == 0 ? "§f" : prefix.getChatPrefix() + " ", - chatColorCode); - } - - private static boolean useSwPrefix(SteamwarUser user) { - if (!user.hasPerm(UserPerm.TEAM)) { - return false; - } - - UserPerm.Prefix prefix = user.prefix(); - - boolean hasTeamPrefix = user.getTeam() != 0; - boolean hasSwPrefix = prefix != UserPerm.emptyPrefix; - - if (!hasSwPrefix) return false; - if (!hasTeamPrefix) return true; - - return SteamwarPrefix.usesSWPrefix(user); - } - private static boolean filteredCommand(Chatter sender, String message) { String command = message.split(" ", 2)[0]; if (command.startsWith("/") && command.contains(":")) { @@ -290,27 +155,6 @@ public class ChatListener extends BasicListener { return false; } - private static void specialAlert(Chatter sender, String name, String baseMessage, int... delay) { - sender.system("CHAT_LIXFEL_ACTION_BAR"); - for (int i = 0; i < delay.length; i++) { - int finalI = i; - VelocityCore.schedule(() -> sender.prefixless("CHAT_MSG", name, sender.user(), new Message(baseMessage + (finalI + 1)))).delay(delay[i], TimeUnit.SECONDS).schedule(); - } - } - - private static String highlightMentions(String message, String returnColor, Chatter player) { - if (!message.contains("@")) return message; - - String mark = "@" + player.user().getUserName(); - return Arrays.stream(message.split(" ")).map(cur -> { - if (cur.equalsIgnoreCase(mark) && player.getPlayer() != null) { - NetworkSender.send(player.getPlayer(), new PingPacket(player.user().getId())); - return "§e" + cur + returnColor; - } - return cur; - }).collect(Collectors.joining(" ")); - } - @Subscribe public void onTabCompleteResponseEvent(TabCompleteEvent e) { List suggestions = e.getSuggestions(); diff --git a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java index 704acfec4..e14f10c26 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java +++ b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java @@ -24,6 +24,7 @@ import com.velocitypowered.api.proxy.server.RegisteredServer; import de.steamwar.linkage.Linked; import de.steamwar.network.packets.PacketHandler; import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.velocitycore.chat.ChatManager; import de.steamwar.velocitycore.network.NetworkSender; import de.steamwar.velocitycore.network.ServerMetaInfo; import de.steamwar.velocitycore.tablist.TablistManager; @@ -51,6 +52,7 @@ public class FightInfoHandler extends PacketHandler { FightInfoPacket lobbyPacket = packet.withServerName(server.getServerInfo().getName()); TablistManager.newFightInfo(server, packet); + ChatManager.updateFightInfo(server, packet); lobbys.removeIf(s -> s.getPlayersConnected().isEmpty()); lobbys.forEach(lobby -> NetworkSender.send(lobby, lobbyPacket)); From b21ff415a88df8db45bc7b963707914c8f96b6c5 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Thu, 15 Oct 2026 20:06:49 +0200 Subject: [PATCH 2/7] Show channel tags as plain gray prefix Messages from a channel that is not the receiver's default are now prefixed with the plain channel name in dark gray instead of brackets. Direct messages get no tag, as their format already shows sender and receiver. --- .../src/de/steamwar/messages/BungeeCore.properties | 2 +- .../src/de/steamwar/messages/BungeeCore_de.properties | 2 +- .../de/steamwar/velocitycore/chat/ChatChannel.java | 11 +++++++++-- .../de/steamwar/velocitycore/chat/DirectChannel.java | 5 +++++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index bff90140c..b617fd32f 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -583,7 +583,7 @@ CHAT_FIGHT_PARTICIPANT = {0} {1}§8» §7{2} CHAT_FIGHT_SPECTATOR = §7{0}§8» §7{1} CHAT_FIGHT_REFEREE = §e{0}§8» §e{1} CHAT_FIGHT_TEAM = {0} {1}§8» §f{2} -CHAT_TAGGED = §8[{0}§8] §r{1} +CHAT_TAGGED = §8{0} §r{1} CHAT_CHANNEL_GLOBAL = §eGlobal CHAT_CHANNEL_ARENA = §e{0} CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 3f34d8889..e253ce72f 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -548,7 +548,7 @@ CHAT_FIGHT_PARTICIPANT = {0} {1}§8» §7{2} CHAT_FIGHT_SPECTATOR = §7{0}§8» §7{1} CHAT_FIGHT_REFEREE = §e{0}§8» §e{1} CHAT_FIGHT_TEAM = {0} {1}§8» §f{2} -CHAT_TAGGED = §8[{0}§8] §r{1} +CHAT_TAGGED = §8{0} §r{1} CHAT_CHANNEL_GLOBAL = §eGlobal CHAT_CHANNEL_ARENA = §e{0} CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java index 8c3be5f98..8d25acb19 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java @@ -47,6 +47,13 @@ public abstract class ChatChannel { */ public abstract Message tag(Chatter receiver); + /** + * @return false if the message format already shows the channel, so no tag is put in front + */ + protected boolean tagged() { + return true; + } + public abstract Stream readers(); /** @@ -90,8 +97,8 @@ public abstract class ChatChannel { if (!receiver.chatShown()) continue; Message msg = format(receiver, sender, coloredMessage); - if (ChatManager.getDefault(receiver) != this) { - msg = new Message("CHAT_TAGGED", tag(receiver), msg); + if (tagged() && ChatManager.getDefault(receiver) != this) { + msg = new Message("CHAT_TAGGED", receiver.parseToPlain(tag(receiver)), msg); } receiver.send(false, null, null, msg); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java index b369bafda..0871357b5 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java @@ -59,6 +59,11 @@ public class DirectChannel extends ChatChannel { return new Message("CHAT_CHANNEL_MSG", SteamwarUser.get(partner(receiver.user().getUUID()))); } + @Override + protected boolean tagged() { + return false; + } + @Override public Stream readers() { return Stream.of(first, second) From 317cb0adf87694ddc2041b2f0b3fe0fd1a93b2f3 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 08:48:10 +0200 Subject: [PATCH 3/7] Add channel icons and hovers to chat messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every chat message now starts with a fixed icon of its channel (🌍 Global, ⌂ Local, ⚔ fight, ⚑ fight team, 👥 TC, ⚙ STC, ✉ DM), highlighted yellow if it is the receiver's default channel. Hovering the icon shows the channel name, hovering the sender shows their team name and shorthand. --- .../steamwar/messages/BungeeCore.properties | 28 +++++++++++++++---- .../messages/BungeeCore_de.properties | 28 +++++++++++++++---- .../velocitycore/chat/AlertChannel.java | 7 +++-- .../velocitycore/chat/ChatChannel.java | 24 ++++++++++------ .../velocitycore/chat/ChatFilter.java | 2 +- .../velocitycore/chat/ChatFormat.java | 18 +++++++++--- .../velocitycore/chat/DirectChannel.java | 7 +++-- .../velocitycore/chat/FightTeamChannel.java | 15 ++++++++-- .../velocitycore/chat/GlobalChannel.java | 10 +++++-- .../velocitycore/chat/LocalChannel.java | 22 +++++++++++---- .../velocitycore/chat/ServerTeamChannel.java | 10 +++++-- .../velocitycore/chat/TeamChannel.java | 8 +++++- 12 files changed, 135 insertions(+), 44 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index b617fd32f..744541237 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -574,16 +574,32 @@ CHAT_BC_USAGE = §8/§7bc §8[§emessage§8] CHAT_NO_RECEIVER = §cNobody receives your message CHAT_EMPTY = §cDon\'t write meaningless empty messages. -CHAT_SERVERTEAM = §e{0}§8» §f{2} -CHAT_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_LOCAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_TEAM = §e{0}§8» §f{2} -CHAT_MSG = §e{0}§8»§e{1} §7{2} +CHAT_SERVERTEAM_SENDER = §e{0} +CHAT_SERVERTEAM = §8» §f{2} +CHAT_GLOBAL_SENDER = {3}{4}{5}{6}{0} +CHAT_GLOBAL = §8» {7}{2} +CHAT_LOCAL_SENDER = {3}{4}{5}{6}{0} +CHAT_LOCAL = §8» {7}{2} +CHAT_TEAM_SENDER = §e{0} +CHAT_TEAM = §8» §f{2} +CHAT_MSG_SENDER = §e{0} +CHAT_MSG = §8»§e{1} §7{2} +CHAT_SPECIAL_ALERT = §e{0}§8»§e{1} §7{2} CHAT_FIGHT_PARTICIPANT = {0} {1}§8» §7{2} CHAT_FIGHT_SPECTATOR = §7{0}§8» §7{1} CHAT_FIGHT_REFEREE = §e{0}§8» §e{1} CHAT_FIGHT_TEAM = {0} {1}§8» §f{2} -CHAT_TAGGED = §8{0} §r{1} +CHAT_ICON = {0}{1} +CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Prefix: §r{1}{2} +CHAT_ICON_GLOBAL = 🌍 +CHAT_ICON_LOCAL = ⌂ +CHAT_ICON_FIGHT = ⚔ +CHAT_ICON_FIGHT_FOREIGN = ⚔ §8{0} +CHAT_ICON_FIGHT_TEAM = {0}⚑ +CHAT_ICON_FIGHT_TEAM_FOREIGN = {0}⚑ §8{1} +CHAT_ICON_TEAM = 👥 +CHAT_ICON_SERVERTEAM = ⚙ +CHAT_ICON_MSG = ✉ CHAT_CHANNEL_GLOBAL = §eGlobal CHAT_CHANNEL_ARENA = §e{0} CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index e253ce72f..437a53667 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -539,16 +539,32 @@ CHAT_BC_USAGE = §8/§7bc §8[§eNachricht§8] CHAT_NO_RECEIVER = §cNiemand empfängt deine Nachricht CHAT_EMPTY = §cSchreibe keine inhaltslosen Nachrichten. -CHAT_SERVERTEAM = §e{0}§8» §f{2} -CHAT_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_LOCAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_TEAM = §e{0}§8» §f{2} -CHAT_MSG = §e{0}§8»§e{1} §7{2} +CHAT_SERVERTEAM_SENDER = §e{0} +CHAT_SERVERTEAM = §8» §f{2} +CHAT_GLOBAL_SENDER = {3}{4}{5}{6}{0} +CHAT_GLOBAL = §8» {7}{2} +CHAT_LOCAL_SENDER = {3}{4}{5}{6}{0} +CHAT_LOCAL = §8» {7}{2} +CHAT_TEAM_SENDER = §e{0} +CHAT_TEAM = §8» §f{2} +CHAT_MSG_SENDER = §e{0} +CHAT_MSG = §8»§e{1} §7{2} +CHAT_SPECIAL_ALERT = §e{0}§8»§e{1} §7{2} CHAT_FIGHT_PARTICIPANT = {0} {1}§8» §7{2} CHAT_FIGHT_SPECTATOR = §7{0}§8» §7{1} CHAT_FIGHT_REFEREE = §e{0}§8» §e{1} CHAT_FIGHT_TEAM = {0} {1}§8» §f{2} -CHAT_TAGGED = §8{0} §r{1} +CHAT_ICON = {0}{1} +CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Kürzel: §r{1}{2} +CHAT_ICON_GLOBAL = 🌍 +CHAT_ICON_LOCAL = ⌂ +CHAT_ICON_FIGHT = ⚔ +CHAT_ICON_FIGHT_FOREIGN = ⚔ §8{0} +CHAT_ICON_FIGHT_TEAM = {0}⚑ +CHAT_ICON_FIGHT_TEAM_FOREIGN = {0}⚑ §8{1} +CHAT_ICON_TEAM = 👥 +CHAT_ICON_SERVERTEAM = ⚙ +CHAT_ICON_MSG = ✉ CHAT_CHANNEL_GLOBAL = §eGlobal CHAT_CHANNEL_ARENA = §e{0} CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java index 2c194f888..a86964fb0 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java @@ -23,6 +23,7 @@ package de.steamwar.velocitycore.chat; import de.steamwar.messages.Chatter; import de.steamwar.messages.Message; import de.steamwar.sql.UserPerm; +import net.kyori.adventure.text.Component; import java.util.Arrays; import java.util.stream.Stream; @@ -54,8 +55,8 @@ public class AlertChannel extends ChatChannel { } @Override - protected Message format(Chatter receiver, Chatter sender, String message) { - return new Message("ALERT", message.replace('&', '§')); + protected Component format(Chatter receiver, Chatter sender, String message) { + return receiver.parse(true, new Message("ALERT", message.replace('&', '§'))); } /** @@ -65,6 +66,6 @@ public class AlertChannel extends ChatChannel { public void send(Chatter sender, String message, boolean mirror) { if (!canWrite(sender)) return; - readers().forEach(reader -> reader.send(true, null, null, format(reader, sender, message))); + readers().forEach(reader -> reader.sendMessage(format(reader, sender, message))); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java index 8d25acb19..9e8e5861f 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java @@ -24,6 +24,8 @@ import de.steamwar.messages.Message; import de.steamwar.sql.AuditLog; import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.UserPerm; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; import java.util.LinkedHashMap; import java.util.Map; @@ -43,15 +45,16 @@ public abstract class ChatChannel { } /** - * @return the channel name shown in front of messages of this channel if it is not the default channel of the receiver + * @return the channel name shown in channel related system messages */ public abstract Message tag(Chatter receiver); /** - * @return false if the message format already shows the channel, so no tag is put in front + * @return the icon always shown in front of messages of this channel, null if the message format already shows the channel. + * The icon is highlighted if this is the default channel of the receiver. */ - protected boolean tagged() { - return true; + protected Message icon(Chatter receiver) { + return null; } public abstract Stream readers(); @@ -61,7 +64,7 @@ public abstract class ChatChannel { */ public abstract boolean canWrite(Chatter sender); - protected abstract Message format(Chatter receiver, Chatter sender, String message); + protected abstract Component format(Chatter receiver, Chatter sender, String message); /** * Called after a message was delivered to all readers. @@ -96,11 +99,14 @@ public abstract class ChatChannel { for (Chatter receiver : receivers.values()) { if (!receiver.chatShown()) continue; - Message msg = format(receiver, sender, coloredMessage); - if (tagged() && ChatManager.getDefault(receiver) != this) { - msg = new Message("CHAT_TAGGED", receiver.parseToPlain(tag(receiver)), msg); + Component msg = format(receiver, sender, coloredMessage); + Message icon = icon(receiver); + if (icon != null) { + Component iconComponent = receiver.parse(new Message("CHAT_ICON", ChatManager.getDefault(receiver) == this ? "§e" : "§7", icon)) + .hoverEvent(HoverEvent.showText(receiver.parse(tag(receiver)))); + msg = Component.text().append(iconComponent).append(Component.text(" ")).append(msg).build(); } - receiver.send(false, null, null, msg); + receiver.sendMessage(msg); if (receiver.user().getId() != user.getId()) noReceiver = false; } diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java index efe8ebb2f..51c2841c1 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java @@ -70,7 +70,7 @@ public class ChatFilter { sender.system("CHAT_LIXFEL_ACTION_BAR"); for (int i = 0; i < delay.length; i++) { int finalI = i; - VelocityCore.schedule(() -> sender.prefixless("CHAT_MSG", name, sender.user(), new Message(baseMessage + (finalI + 1)))).delay(delay[i], TimeUnit.SECONDS).schedule(); + VelocityCore.schedule(() -> sender.prefixless("CHAT_SPECIAL_ALERT", name, sender.user(), new Message(baseMessage + (finalI + 1)))).delay(delay[i], TimeUnit.SECONDS).schedule(); } } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java index 28a114f29..aeb8064e9 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java @@ -28,6 +28,8 @@ import de.steamwar.sql.Team; import de.steamwar.sql.UserPerm; import de.steamwar.velocitycore.network.NetworkSender; import de.steamwar.velocitycore.util.SteamwarPrefix; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; import java.util.Arrays; import java.util.stream.Collectors; @@ -37,9 +39,10 @@ public class ChatFormat { private ChatFormat() {} /** - * Formats a message with the rank and team prefixes of the sender. + * Formats a message with the rank and team prefixes of the sender. The sender part ({@code _SENDER}) + * shows the team of the sender on hover. */ - public static Message player(String format, Chatter receiver, Chatter sender, Chatter msgReceiver, String message) { + public static Component player(String format, Chatter receiver, Chatter sender, Chatter msgReceiver, String message) { SteamwarUser user = sender.user(); UserPerm.Prefix prefix = user.prefix(); @@ -52,7 +55,7 @@ public class ChatFormat { } String chatColorCode = user.hasPerm(UserPerm.TEAM) ? "§f" : "§7"; - return new Message(format, + Object[] params = { user, msgReceiver == null ? receiver : msgReceiver, highlightMentions(message, chatColorCode, receiver), @@ -60,7 +63,14 @@ public class ChatFormat { "", prefix == UserPerm.emptyPrefix ? "§f" : prefix.getColorCode(), prefix.getChatPrefix().length() == 0 ? "§f" : prefix.getChatPrefix() + " ", - chatColorCode); + chatColorCode}; + + Component senderPart = receiver.parse(new Message(format + "_SENDER", params)); + if (user.getTeam() != 0) { + Team team = Team.byId(user.getTeam()); + senderPart = senderPart.hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM", team.getTeamName(), "§" + team.getTeamColor(), team.getTeamKuerzel())))); + } + return Component.text().append(senderPart).append(receiver.parse(new Message(format, params))).build(); } private static boolean useSwPrefix(SteamwarUser user) { diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java index 0871357b5..3695c7015 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java @@ -27,6 +27,7 @@ import de.steamwar.sql.AuditLog; import de.steamwar.sql.IgnoreSystem; import de.steamwar.sql.SteamwarUser; import de.steamwar.velocitycore.VelocityCore; +import net.kyori.adventure.text.Component; import java.util.UUID; import java.util.stream.Stream; @@ -60,8 +61,8 @@ public class DirectChannel extends ChatChannel { } @Override - protected boolean tagged() { - return false; + protected Message icon(Chatter receiver) { + return new Message("CHAT_ICON_MSG"); } @Override @@ -88,7 +89,7 @@ public class DirectChannel extends ChatChannel { } @Override - protected Message format(Chatter receiver, Chatter sender, String message) { + protected Component format(Chatter receiver, Chatter sender, String message) { return ChatFormat.player("CHAT_MSG", receiver, sender, Chatter.of(partner(sender.user().getUUID())), message); } diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java index 93b80e600..3768fa75c 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java @@ -27,6 +27,7 @@ import de.steamwar.network.packets.common.FightInfoPacket; import de.steamwar.sql.AuditLog; import de.steamwar.sql.SteamwarUser; import lombok.Getter; +import net.kyori.adventure.text.Component; import java.util.Collections; import java.util.List; @@ -72,6 +73,16 @@ public class FightTeamChannel extends ChatChannel { return new Message("CHAT_CHANNEL_FIGHT_TEAM", server.getServerInfo().getName(), teamName()); } + @Override + protected Message icon(Chatter receiver) { + String teamName = teamName(); + String color = teamName.length() >= 2 && teamName.charAt(0) == '§' ? teamName.substring(0, 2) : "§7"; + if (receiver.getPlayer() != null && server.getPlayersConnected().contains(receiver.getPlayer())) { + return new Message("CHAT_ICON_FIGHT_TEAM", color); + } + return new Message("CHAT_ICON_FIGHT_TEAM_FOREIGN", color, server.getServerInfo().getName()); + } + @Override public Stream readers() { List members = members(); @@ -90,8 +101,8 @@ public class FightTeamChannel extends ChatChannel { } @Override - protected Message format(Chatter receiver, Chatter sender, String message) { - return new Message("CHAT_FIGHT_TEAM", teamName(), sender.user(), ChatFormat.highlightMentions(message, "§f", receiver)); + protected Component format(Chatter receiver, Chatter sender, String message) { + return receiver.parse(new Message("CHAT_FIGHT_TEAM", teamName(), sender.user(), ChatFormat.highlightMentions(message, "§f", receiver))); } @Override diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java index a4146c1d2..cc2219cdb 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java @@ -23,6 +23,7 @@ package de.steamwar.velocitycore.chat; import de.steamwar.messages.Chatter; import de.steamwar.messages.Message; import de.steamwar.velocitycore.discord.DiscordBot; +import net.kyori.adventure.text.Component; import java.util.stream.Stream; @@ -38,6 +39,11 @@ public class GlobalChannel extends ChatChannel { return new Message("CHAT_CHANNEL_GLOBAL"); } + @Override + protected Message icon(Chatter receiver) { + return new Message("CHAT_ICON_GLOBAL"); + } + @Override public Stream readers() { return Stream.concat(Stream.of(Chatter.console()), Chatter.allPlayers().filter(ChatManager::readsGlobal).map(Chatter::of).filter(chatter -> ChatManager.reads(chatter, persistId()))); @@ -49,14 +55,14 @@ public class GlobalChannel extends ChatChannel { } @Override - protected Message format(Chatter receiver, Chatter sender, String message) { + protected Component format(Chatter receiver, Chatter sender, String message) { return ChatFormat.player("CHAT_GLOBAL", receiver, sender, null, message); } @Override protected boolean delivered(Chatter sender, String message, boolean mirror) { if (mirror) { - DiscordBot.withBot(bot -> bot.getIngameChat().send(false, null, null, format(bot.getIngameChat(), sender, message))); + DiscordBot.withBot(bot -> bot.getIngameChat().sendMessage(format(bot.getIngameChat(), sender, message))); } return true; } diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java index 5eaf90aa9..27f7b6363 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java @@ -30,6 +30,7 @@ import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.UserPerm; import de.steamwar.velocitycore.network.NetworkSender; import lombok.Getter; +import net.kyori.adventure.text.Component; import java.util.stream.Stream; @@ -62,6 +63,17 @@ public class LocalChannel extends ChatChannel { return new Message("CHAT_CHANNEL_LOCAL"); } + @Override + protected Message icon(Chatter receiver) { + if (!isArena()) return new Message("CHAT_ICON_LOCAL"); + + // Moderators reading an arena they are not on need to know which arena it is + if (receiver.getPlayer() != null && server.getPlayersConnected().contains(receiver.getPlayer())) { + return new Message("CHAT_ICON_FIGHT"); + } + return new Message("CHAT_ICON_FIGHT_FOREIGN", server.getServerInfo().getName()); + } + @Override public Stream readers() { Stream players = server.getPlayersConnected().stream().map(Chatter::of).filter(chatter -> ChatManager.reads(chatter, persistId())).map(Chatter.class::cast); @@ -83,7 +95,7 @@ public class LocalChannel extends ChatChannel { } @Override - protected Message format(Chatter receiver, Chatter sender, String message) { + protected Component format(Chatter receiver, Chatter sender, String message) { if (!isArena()) { return ChatFormat.player("CHAT_LOCAL", receiver, sender, null, message); } @@ -92,13 +104,13 @@ public class LocalChannel extends ChatChannel { FightInfoPacket info = ChatManager.getFightInfo(server); int id = sender.user().getId(); if (info != null && info.getBluePlayers().contains(id)) { - return new Message("CHAT_FIGHT_PARTICIPANT", info.getBlueName(), sender.user(), highlighted); + return receiver.parse(new Message("CHAT_FIGHT_PARTICIPANT", info.getBlueName(), sender.user(), highlighted)); } else if (info != null && info.getRedPlayers().contains(id)) { - return new Message("CHAT_FIGHT_PARTICIPANT", info.getRedName(), sender.user(), highlighted); + return receiver.parse(new Message("CHAT_FIGHT_PARTICIPANT", info.getRedName(), sender.user(), highlighted)); } else if (ChatManager.isReferee(server, id)) { - return new Message("CHAT_FIGHT_REFEREE", sender.user(), highlighted); + return receiver.parse(new Message("CHAT_FIGHT_REFEREE", sender.user(), highlighted)); } - return new Message("CHAT_FIGHT_SPECTATOR", sender.user(), highlighted); + return receiver.parse(new Message("CHAT_FIGHT_SPECTATOR", sender.user(), highlighted)); } @Override diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java index 46950b1cb..1860633d2 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java @@ -26,6 +26,7 @@ import de.steamwar.sql.AuditLog; import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.UserPerm; import de.steamwar.velocitycore.discord.DiscordBot; +import net.kyori.adventure.text.Component; import java.util.Arrays; import java.util.stream.Stream; @@ -42,6 +43,11 @@ public class ServerTeamChannel extends ChatChannel { return new Message("CHAT_CHANNEL_SERVERTEAM"); } + @Override + protected Message icon(Chatter receiver) { + return new Message("CHAT_ICON_SERVERTEAM"); + } + @Override public Stream readers() { return Arrays.stream(Chatter.serverteam().getChatters()).filter(chatter -> ChatManager.reads(chatter, persistId())); @@ -57,14 +63,14 @@ public class ServerTeamChannel extends ChatChannel { } @Override - protected Message format(Chatter receiver, Chatter sender, String message) { + protected Component format(Chatter receiver, Chatter sender, String message) { return ChatFormat.player("CHAT_SERVERTEAM", receiver, sender, null, message); } @Override protected boolean delivered(Chatter sender, String message, boolean mirror) { if (mirror) { - DiscordBot.withBot(bot -> bot.getServerTeamChat().send(false, null, null, ChatFormat.player("CHAT_GLOBAL", bot.getServerTeamChat(), sender, null, message))); + DiscordBot.withBot(bot -> bot.getServerTeamChat().sendMessage(ChatFormat.player("CHAT_GLOBAL", bot.getServerTeamChat(), sender, null, message))); } return true; } diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java index f29a80f4d..f362099b3 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java @@ -26,6 +26,7 @@ import de.steamwar.sql.AuditLog; import de.steamwar.sql.SteamwarUser; import de.steamwar.velocitycore.commands.StreamingCommand; import lombok.Getter; +import net.kyori.adventure.text.Component; import java.util.stream.Stream; @@ -48,6 +49,11 @@ public class TeamChannel extends ChatChannel { return new Message("CHAT_CHANNEL_TEAM"); } + @Override + protected Message icon(Chatter receiver) { + return new Message("CHAT_ICON_TEAM"); + } + @Override public Stream readers() { return Chatter.allPlayers() @@ -68,7 +74,7 @@ public class TeamChannel extends ChatChannel { } @Override - protected Message format(Chatter receiver, Chatter sender, String message) { + protected Component format(Chatter receiver, Chatter sender, String message) { return ChatFormat.player("CHAT_TEAM", receiver, sender, null, message); } From 975d42c17f26012578a12f6adb68cfbcd735443d Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 08:57:45 +0200 Subject: [PATCH 4/7] Add team hovers to fight chat and make DMs non-default - Names in the fight chat show the sender's team on hover like everywhere else; the Blue/Red team prefix is only hoverable in event fights, where it is the actual team - Server team members using the SW prefix show the SteamWar team - Direct messages can no longer be the default channel; /msg and /r require a message again --- .../steamwar/messages/BungeeCore.properties | 13 +++-- .../messages/BungeeCore_de.properties | 13 +++-- .../velocitycore/chat/ChatFormat.java | 47 +++++++++++++++++-- .../velocitycore/chat/ChatManager.java | 33 +++++++------ .../velocitycore/chat/DirectChannel.java | 5 ++ .../velocitycore/chat/FightTeamChannel.java | 2 +- .../velocitycore/chat/LocalChannel.java | 8 ++-- .../velocitycore/commands/MsgCommand.java | 10 +--- .../velocitycore/commands/RCommand.java | 2 +- 9 files changed, 90 insertions(+), 43 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index 744541237..c9db69bb8 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -585,10 +585,14 @@ CHAT_TEAM = §8» §f{2} CHAT_MSG_SENDER = §e{0} CHAT_MSG = §8»§e{1} §7{2} CHAT_SPECIAL_ALERT = §e{0}§8»§e{1} §7{2} -CHAT_FIGHT_PARTICIPANT = {0} {1}§8» §7{2} -CHAT_FIGHT_SPECTATOR = §7{0}§8» §7{1} -CHAT_FIGHT_REFEREE = §e{0}§8» §e{1} -CHAT_FIGHT_TEAM = {0} {1}§8» §f{2} +CHAT_FIGHT_PARTICIPANT_SENDER = {0}{1} +CHAT_FIGHT_PARTICIPANT = §8» §7{0} +CHAT_FIGHT_SPECTATOR_SENDER = §7{1} +CHAT_FIGHT_SPECTATOR = §8» §7{0} +CHAT_FIGHT_REFEREE_SENDER = §e{1} +CHAT_FIGHT_REFEREE = §8» §e{0} +CHAT_FIGHT_TEAM_SENDER = {0}{1} +CHAT_FIGHT_TEAM = §8» §f{0} CHAT_ICON = {0}{1} CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Prefix: §r{1}{2} CHAT_ICON_GLOBAL = 🌍 @@ -610,7 +614,6 @@ CHAT_CHANNEL_ALERT = §cAlert CHAT_CHANNEL_LOCAL = §eLocal CHAT_CHANNEL_CURRENT = §7You are writing into §e{0}§7. CHAT_CHANNEL_SWITCHED = §7You are now writing into §e{0}§7. -CHAT_CHANNEL_CLOSED = §e{0} §7was closed. CHAT_CHANNEL_NOT_SWITCHABLE = §cYou cannot write into this channel by default. CHAT_NO_WRITE = §cYou cannot write into this channel. CHAT_FIGHT_NO_TEAM = §cYou are not in a fight team. diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 437a53667..091998787 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -550,10 +550,14 @@ CHAT_TEAM = §8» §f{2} CHAT_MSG_SENDER = §e{0} CHAT_MSG = §8»§e{1} §7{2} CHAT_SPECIAL_ALERT = §e{0}§8»§e{1} §7{2} -CHAT_FIGHT_PARTICIPANT = {0} {1}§8» §7{2} -CHAT_FIGHT_SPECTATOR = §7{0}§8» §7{1} -CHAT_FIGHT_REFEREE = §e{0}§8» §e{1} -CHAT_FIGHT_TEAM = {0} {1}§8» §f{2} +CHAT_FIGHT_PARTICIPANT_SENDER = {0}{1} +CHAT_FIGHT_PARTICIPANT = §8» §7{0} +CHAT_FIGHT_SPECTATOR_SENDER = §7{1} +CHAT_FIGHT_SPECTATOR = §8» §7{0} +CHAT_FIGHT_REFEREE_SENDER = §e{1} +CHAT_FIGHT_REFEREE = §8» §e{0} +CHAT_FIGHT_TEAM_SENDER = {0}{1} +CHAT_FIGHT_TEAM = §8» §f{0} CHAT_ICON = {0}{1} CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Kürzel: §r{1}{2} CHAT_ICON_GLOBAL = 🌍 @@ -575,7 +579,6 @@ CHAT_CHANNEL_ALERT = §cAlert CHAT_CHANNEL_LOCAL = §eLokal CHAT_CHANNEL_CURRENT = §7Du schreibst in §e{0}§7. CHAT_CHANNEL_SWITCHED = §7Du schreibst jetzt in §e{0}§7. -CHAT_CHANNEL_CLOSED = §e{0} §7wurde geschlossen. CHAT_CHANNEL_NOT_SWITCHABLE = §cIn diesen Kanal kannst du nicht standardmäßig schreiben. CHAT_NO_WRITE = §cDu kannst nicht in diesen Kanal schreiben. CHAT_FIGHT_NO_TEAM = §cDu bist in keinem Kampfteam. diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java index aeb8064e9..ad38e020c 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java @@ -20,6 +20,7 @@ package de.steamwar.velocitycore.chat; +import com.velocitypowered.api.proxy.server.RegisteredServer; import de.steamwar.messages.Chatter; import de.steamwar.messages.Message; import de.steamwar.network.packets.server.PingPacket; @@ -29,6 +30,7 @@ import de.steamwar.sql.UserPerm; import de.steamwar.velocitycore.network.NetworkSender; import de.steamwar.velocitycore.util.SteamwarPrefix; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.event.HoverEvent; import java.util.Arrays; @@ -65,14 +67,49 @@ public class ChatFormat { prefix.getChatPrefix().length() == 0 ? "§f" : prefix.getChatPrefix() + " ", chatColorCode}; - Component senderPart = receiver.parse(new Message(format + "_SENDER", params)); - if (user.getTeam() != 0) { - Team team = Team.byId(user.getTeam()); - senderPart = senderPart.hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM", team.getTeamName(), "§" + team.getTeamColor(), team.getTeamKuerzel())))); - } + Component senderPart = withTeamHover(receiver, user, receiver.parse(new Message(format + "_SENDER", params))); return Component.text().append(senderPart).append(receiver.parse(new Message(format, params))).build(); } + /** + * Formats a fight chat message. The fight team name is only hoverable in event fights, where it is the actual team. + * + * @param coloredTeamName the fight team of the sender, null if the sender is not in a fight team + */ + public static Component fight(Chatter receiver, RegisteredServer server, Chatter sender, String coloredTeamName, String format, String message) { + TextComponent.Builder line = Component.text(); + String nameColor = ""; + if (coloredTeamName != null) { + Component team = Chatter.SERIALIZER.deserialize(coloredTeamName); + Team eventTeam = ChatManager.eventTeam(server, coloredTeamName); + if (eventTeam != null) { + team = team.hoverEvent(HoverEvent.showText(teamHover(receiver, eventTeam))); + } + line.append(team).append(Component.text(" ")); + nameColor = coloredTeamName.length() >= 2 && coloredTeamName.charAt(0) == '§' ? coloredTeamName.substring(0, 2) : ""; + } + + line.append(withTeamHover(receiver, sender.user(), receiver.parse(new Message(format + "_SENDER", nameColor, sender.user())))); + line.append(receiver.parse(new Message(format, message))); + return line.build(); + } + + /** + * Shows the team of the user when hovering the component, the SteamWar team for server team members using the SW prefix. + */ + public static Component withTeamHover(Chatter receiver, SteamwarUser user, Component component) { + if (useSwPrefix(user)) { + return component.hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM", "SteamWar", "§e", "S§8W")))); + } + if (user.getTeam() == 0) return component; + + return component.hoverEvent(HoverEvent.showText(teamHover(receiver, Team.byId(user.getTeam())))); + } + + private static Component teamHover(Chatter receiver, Team team) { + return receiver.parse(new Message("CHAT_HOVER_TEAM", team.getTeamName(), "§" + team.getTeamColor(), team.getTeamKuerzel())); + } + private static boolean useSwPrefix(SteamwarUser user) { if (!user.hasPerm(UserPerm.TEAM)) { return false; diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java index a437532a1..c8565b8e1 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -33,6 +33,7 @@ import de.steamwar.network.packets.common.FightInfoPacket; import de.steamwar.persistent.Subserver; import de.steamwar.sql.Event; import de.steamwar.sql.Referee; +import de.steamwar.sql.Team; import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.UserConfig; import de.steamwar.sql.UserPerm; @@ -221,6 +222,23 @@ public class ChatManager extends BasicListener { return fightInfos.get(server); } + /** + * @return the team of an event fight on the server whose colored name is shown, null if the server runs no event fight + */ + static Team eventTeam(RegisteredServer server, String coloredTeamName) { + Subserver subserver = Subserver.getSubserver(server.getServerInfo()); + if (subserver == null) return null; + + String kuerzel = coloredTeamName.replaceAll("§.", ""); + for (Map.Entry entry : new ArrayList<>(EventStarter.getEventServer().entrySet())) { + if (entry.getValue() != subserver) continue; + + Team team = Team.byId(entry.getKey()); + if (team != null && team.getTeamKuerzel().equals(kuerzel)) return team; + } + return null; + } + /** * Updates the fight teams of an arena. Players who just joined a fight team write into the fight chat by default. */ @@ -382,19 +400,6 @@ public class ChatManager extends BasicListener { spies.remove(uuid); hidden.remove(uuid); - Iterator it = directs.iterator(); - while (it.hasNext()) { - DirectChannel channel = it.next(); - if (!channel.contains(uuid)) continue; - it.remove(); - - Player partner = VelocityCore.getProxy().getPlayer(channel.partner(uuid)).orElse(null); - if (partner != null && defaults.get(partner.getUniqueId()) == channel) { - PlayerChatter chatter = Chatter.of(partner); - chatter.system("CHAT_CHANNEL_CLOSED", channel.tag(chatter)); - defaults.put(partner.getUniqueId(), resolvePersisted(partner, currentServer(partner).orElse(null))); - chatter.system("CHAT_CHANNEL_SWITCHED", defaults.get(partner.getUniqueId()).tag(chatter)); - } - } + directs.removeIf(channel -> channel.contains(uuid)); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java index 3695c7015..f0a79923e 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java @@ -60,6 +60,11 @@ public class DirectChannel extends ChatChannel { return new Message("CHAT_CHANNEL_MSG", SteamwarUser.get(partner(receiver.user().getUUID()))); } + @Override + public boolean switchable() { + return false; + } + @Override protected Message icon(Chatter receiver) { return new Message("CHAT_ICON_MSG"); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java index 3768fa75c..869d7c242 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java @@ -102,7 +102,7 @@ public class FightTeamChannel extends ChatChannel { @Override protected Component format(Chatter receiver, Chatter sender, String message) { - return receiver.parse(new Message("CHAT_FIGHT_TEAM", teamName(), sender.user(), ChatFormat.highlightMentions(message, "§f", receiver))); + return ChatFormat.fight(receiver, server, sender, teamName(), "CHAT_FIGHT_TEAM", ChatFormat.highlightMentions(message, "§f", receiver)); } @Override diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java index 27f7b6363..12aece2ae 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java @@ -104,13 +104,13 @@ public class LocalChannel extends ChatChannel { FightInfoPacket info = ChatManager.getFightInfo(server); int id = sender.user().getId(); if (info != null && info.getBluePlayers().contains(id)) { - return receiver.parse(new Message("CHAT_FIGHT_PARTICIPANT", info.getBlueName(), sender.user(), highlighted)); + return ChatFormat.fight(receiver, server, sender, info.getBlueName(), "CHAT_FIGHT_PARTICIPANT", highlighted); } else if (info != null && info.getRedPlayers().contains(id)) { - return receiver.parse(new Message("CHAT_FIGHT_PARTICIPANT", info.getRedName(), sender.user(), highlighted)); + return ChatFormat.fight(receiver, server, sender, info.getRedName(), "CHAT_FIGHT_PARTICIPANT", highlighted); } else if (ChatManager.isReferee(server, id)) { - return receiver.parse(new Message("CHAT_FIGHT_REFEREE", sender.user(), highlighted)); + return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_REFEREE", highlighted); } - return receiver.parse(new Message("CHAT_FIGHT_SPECTATOR", sender.user(), highlighted)); + return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_SPECTATOR", highlighted); } @Override diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java index 67cbd21a4..4db12170e 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java @@ -24,7 +24,6 @@ import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.PlayerChatter; import de.steamwar.velocitycore.VelocityCore; -import de.steamwar.velocitycore.chat.ChatChannel; import de.steamwar.velocitycore.chat.ChatManager; import static de.steamwar.persistent.Storage.lastChats; @@ -37,7 +36,7 @@ public class MsgCommand extends SWCommand { } @Register(description = "MSG_USAGE") - public void genericCommand(PlayerChatter sender, @ErrorMessage(value = "MSG_OFFLINE") Player target, String... message) { + public void genericCommand(PlayerChatter sender, @ErrorMessage(value = "MSG_OFFLINE") Player target, @ErrorMessage(value = "MSG_USAGE", allowEAs = false) String... message) { msg(sender, target, message); } @@ -47,12 +46,7 @@ public class MsgCommand extends SWCommand { return; } - ChatChannel channel = ChatManager.direct(sender.user().getUUID(), target.getUniqueId()); - if (args.length == 0) { - ChatManager.switchTo(sender, channel); - } else { - channel.send(sender, String.join(" ", args)); - } + ChatManager.direct(sender.user().getUUID(), target.getUniqueId()).send(sender, String.join(" ", args)); } public static void remove(Player player) { diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java index 65582e856..ae86f2492 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java @@ -33,7 +33,7 @@ public class RCommand extends SWCommand { } @Register(description = "R_USAGE") - public void genericCommand(PlayerChatter sender, String... message) { + public void genericCommand(PlayerChatter sender, @ErrorMessage(value = "R_USAGE", allowEAs = false) String... message) { MsgCommand.msg(sender, lastChats.get(sender.getPlayer()), message); } } From 65cc6119b04cecb929d2fef9c4ed9d122ce4e0c0 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 09:00:28 +0200 Subject: [PATCH 5/7] Show default channel notice with its icon The notice on join, on every default change and for /chat is now " is your default channel" in green without the SteamWar prefix; hovering the icon shows the channel name. --- .../steamwar/messages/BungeeCore.properties | 3 +-- .../messages/BungeeCore_de.properties | 3 +-- .../velocitycore/chat/ChatManager.java | 22 ++++++++++++++++--- .../velocitycore/commands/ChatCommand.java | 2 +- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index c9db69bb8..36fdc2847 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -612,8 +612,7 @@ CHAT_CHANNEL_SERVERTEAM = §eSTC CHAT_CHANNEL_MSG = §eMSG §7{0} CHAT_CHANNEL_ALERT = §cAlert CHAT_CHANNEL_LOCAL = §eLocal -CHAT_CHANNEL_CURRENT = §7You are writing into §e{0}§7. -CHAT_CHANNEL_SWITCHED = §7You are now writing into §e{0}§7. +CHAT_CHANNEL_DEFAULT = §ais your default channel CHAT_CHANNEL_NOT_SWITCHABLE = §cYou cannot write into this channel by default. CHAT_NO_WRITE = §cYou cannot write into this channel. CHAT_FIGHT_NO_TEAM = §cYou are not in a fight team. diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 091998787..0c54dcccf 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -577,8 +577,7 @@ CHAT_CHANNEL_SERVERTEAM = §eSTC CHAT_CHANNEL_MSG = §eMSG §7{0} CHAT_CHANNEL_ALERT = §cAlert CHAT_CHANNEL_LOCAL = §eLokal -CHAT_CHANNEL_CURRENT = §7Du schreibst in §e{0}§7. -CHAT_CHANNEL_SWITCHED = §7Du schreibst jetzt in §e{0}§7. +CHAT_CHANNEL_DEFAULT = §aist dein Standardkanal CHAT_CHANNEL_NOT_SWITCHABLE = §cIn diesen Kanal kannst du nicht standardmäßig schreiben. CHAT_NO_WRITE = §cDu kannst nicht in diesen Kanal schreiben. CHAT_FIGHT_NO_TEAM = §cDu bist in keinem Kampfteam. diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java index c8565b8e1..95359244e 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -28,6 +28,7 @@ import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.server.RegisteredServer; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; import de.steamwar.messages.PlayerChatter; import de.steamwar.network.packets.common.FightInfoPacket; import de.steamwar.persistent.Subserver; @@ -41,6 +42,8 @@ import de.steamwar.velocitycore.EventStarter; import de.steamwar.velocitycore.VelocityCore; import de.steamwar.velocitycore.listeners.BasicListener; import lombok.Getter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -313,7 +316,7 @@ public class ChatManager extends BasicListener { } defaults.put(uuid, channel); - player.system("CHAT_CHANNEL_SWITCHED", channel.tag(player)); + notifyDefault(player, channel); } private static void setDefault(PlayerChatter player, ChatChannel channel) { @@ -322,12 +325,25 @@ public class ChatManager extends BasicListener { ChatChannel previous = defaults.put(uuid, channel); if (announced.add(uuid)) { - player.system("CHAT_CHANNEL_CURRENT", channel.tag(player)); + notifyDefault(player, channel); } else if (previous != channel && !player.parseToPlain(previous.tag(player)).equals(player.parseToPlain(channel.tag(player)))) { - player.system("CHAT_CHANNEL_SWITCHED", channel.tag(player)); + notifyDefault(player, channel); } } + /** + * Tells the player which channel they write into, showing the channel icon with its name on hover. + */ + public static void notifyDefault(PlayerChatter player, ChatChannel channel) { + Message icon = channel.icon(player); + Component iconComponent = icon == null ? player.parse(channel.tag(player)) : player.parse(new Message("CHAT_ICON", "§a", icon)); + player.sendMessage(Component.text() + .append(iconComponent.hoverEvent(HoverEvent.showText(player.parse(channel.tag(player))))) + .append(Component.text(" ")) + .append(player.parse(new Message("CHAT_CHANNEL_DEFAULT"))) + .build()); + } + /** * Handles a chat message the player typed without a command. */ diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java index cd563a81c..b7a5bb050 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java @@ -47,7 +47,7 @@ public class ChatCommand extends SWCommand { @Register(description = "CHAT_USAGE") public void current(PlayerChatter sender) { - sender.system("CHAT_CHANNEL_CURRENT", ChatManager.getDefault(sender).tag(sender)); + ChatManager.notifyDefault(sender, ChatManager.getDefault(sender)); } @Register From 4c758799155b69303ffea31080bd0ee0f338d5c4 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 09:07:13 +0200 Subject: [PATCH 6/7] Keep spectators in fight chat and treat spectate servers as fight servers - '+' from a spectator on a fight server stays in the fight chat - Event spectate servers (static servers with a spectate port) use a local fight chat by default and hide the global chat like arenas --- .../src/de/steamwar/velocitycore/EventStarter.java | 11 ++++++++++- .../src/de/steamwar/velocitycore/VelocityCore.java | 2 +- .../de/steamwar/velocitycore/chat/ChatManager.java | 10 +++++++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java b/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java index 5790e53e3..0c2c0aa8d 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java +++ b/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java @@ -31,8 +31,10 @@ import net.kyori.adventure.text.event.ClickEvent; import java.sql.Timestamp; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Queue; +import java.util.Set; import java.util.concurrent.TimeUnit; import static de.steamwar.persistent.Storage.eventServer; @@ -43,8 +45,15 @@ public class EventStarter { @Getter private static final Map spectatePorts = new HashMap<>(); - public static void addSpectateServer(int port, String command) { + private static final Set spectateServers = new HashSet<>(); + + public static void addSpectateServer(int port, String command, String serverName) { spectatePorts.put(port, command); + spectateServers.add(serverName); + } + + public static boolean isSpectateServer(String serverName) { + return spectateServers.contains(serverName); } public EventStarter() { diff --git a/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java b/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java index 6240f0c0f..1701a10d3 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java +++ b/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java @@ -236,7 +236,7 @@ public class VelocityCore implements ReloadablePlugin { String cmd = cmds.remove(0); if (server.getSpectatePort() != 0) { - EventStarter.addSpectateServer(server.getSpectatePort(), cmd); + EventStarter.addSpectateServer(server.getSpectatePort(), cmd, entry.getKey()); } new ServerSwitchCommand(cmd, entry.getKey(), server.getSpectatePort() != 0, cmds.toArray(new String[0])); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java index 95359244e..24082e65e 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -113,8 +113,11 @@ public class ChatManager extends BasicListener { return VelocityCore.getProxy().getServer(server.getServerInfo().getName()).orElse(null) != server; } + /** + * @return true for arenas and event spectate servers, which use a fight chat instead of the global chat + */ public static boolean isArena(RegisteredServer server) { - return Subserver.isArena(Subserver.getSubserver(server.getServerInfo())); + return Subserver.isArena(Subserver.getSubserver(server.getServerInfo())) || EventStarter.isSpectateServer(server.getServerInfo().getName()); } private static Optional currentServer(Player player) { @@ -371,14 +374,15 @@ public class ChatManager extends BasicListener { RegisteredServer server = currentServer(sender.getPlayer()).orElse(null); if (server == null) return current; - if (current instanceof LocalChannel) { + if (current instanceof LocalChannel local) { FightTeamChannel blue = fightTeam(server, true); if (blue.isMember(sender)) return blue; FightTeamChannel red = fightTeam(server, false); if (red.isMember(sender)) return red; - return global; + // Spectators on fight servers stay in the fight chat + return local.isArena() ? local : global; } return local(server); From 904a850a3ff9070d07f7e216efebfbe29a37a84e Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 09:16:35 +0200 Subject: [PATCH 7/7] Remove duplicated chat channel logic - Shared send-or-switch helper for /global, /local, /tc and /stc - Shared helpers for "is on server" and the player's current local channel - Read fight infos from TablistManager instead of keeping a second map - /chat resolves fight servers (arenas and spectate servers) by name in one place - Reuse the global and participant formats instead of identical keys --- .../steamwar/messages/BungeeCore.properties | 3 -- .../messages/BungeeCore_de.properties | 3 -- .../velocitycore/chat/ChatFormat.java | 4 +- .../velocitycore/chat/ChatManager.java | 42 ++++++++++++++++--- .../velocitycore/chat/FightTeamChannel.java | 6 +-- .../velocitycore/chat/LocalChannel.java | 17 ++++---- .../velocitycore/commands/ChatCommand.java | 22 ++++------ .../velocitycore/commands/GlobalCommand.java | 9 +--- .../velocitycore/commands/LocalCommand.java | 9 +--- .../commands/ServerTeamchatCommand.java | 9 +--- .../commands/TeamchatCommand.java | 11 +---- .../network/handlers/FightInfoHandler.java | 4 +- .../velocitycore/tablist/TablistManager.java | 12 +++++- 13 files changed, 74 insertions(+), 77 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index 36fdc2847..6dee34b20 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -578,8 +578,6 @@ CHAT_SERVERTEAM_SENDER = §e{0} CHAT_SERVERTEAM = §8» §f{2} CHAT_GLOBAL_SENDER = {3}{4}{5}{6}{0} CHAT_GLOBAL = §8» {7}{2} -CHAT_LOCAL_SENDER = {3}{4}{5}{6}{0} -CHAT_LOCAL = §8» {7}{2} CHAT_TEAM_SENDER = §e{0} CHAT_TEAM = §8» §f{2} CHAT_MSG_SENDER = §e{0} @@ -591,7 +589,6 @@ CHAT_FIGHT_SPECTATOR_SENDER = §7{1} CHAT_FIGHT_SPECTATOR = §8» §7{0} CHAT_FIGHT_REFEREE_SENDER = §e{1} CHAT_FIGHT_REFEREE = §8» §e{0} -CHAT_FIGHT_TEAM_SENDER = {0}{1} CHAT_FIGHT_TEAM = §8» §f{0} CHAT_ICON = {0}{1} CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Prefix: §r{1}{2} diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 0c54dcccf..7f930eacf 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -543,8 +543,6 @@ CHAT_SERVERTEAM_SENDER = §e{0} CHAT_SERVERTEAM = §8» §f{2} CHAT_GLOBAL_SENDER = {3}{4}{5}{6}{0} CHAT_GLOBAL = §8» {7}{2} -CHAT_LOCAL_SENDER = {3}{4}{5}{6}{0} -CHAT_LOCAL = §8» {7}{2} CHAT_TEAM_SENDER = §e{0} CHAT_TEAM = §8» §f{2} CHAT_MSG_SENDER = §e{0} @@ -556,7 +554,6 @@ CHAT_FIGHT_SPECTATOR_SENDER = §7{1} CHAT_FIGHT_SPECTATOR = §8» §7{0} CHAT_FIGHT_REFEREE_SENDER = §e{1} CHAT_FIGHT_REFEREE = §8» §e{0} -CHAT_FIGHT_TEAM_SENDER = {0}{1} CHAT_FIGHT_TEAM = §8» §f{0} CHAT_ICON = {0}{1} CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Kürzel: §r{1}{2} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java index ad38e020c..f5b945dc4 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java @@ -76,7 +76,7 @@ public class ChatFormat { * * @param coloredTeamName the fight team of the sender, null if the sender is not in a fight team */ - public static Component fight(Chatter receiver, RegisteredServer server, Chatter sender, String coloredTeamName, String format, String message) { + public static Component fight(Chatter receiver, RegisteredServer server, Chatter sender, String coloredTeamName, String senderFormat, String format, String message) { TextComponent.Builder line = Component.text(); String nameColor = ""; if (coloredTeamName != null) { @@ -89,7 +89,7 @@ public class ChatFormat { nameColor = coloredTeamName.length() >= 2 && coloredTeamName.charAt(0) == '§' ? coloredTeamName.substring(0, 2) : ""; } - line.append(withTeamHover(receiver, sender.user(), receiver.parse(new Message(format + "_SENDER", nameColor, sender.user())))); + line.append(withTeamHover(receiver, sender.user(), receiver.parse(new Message(senderFormat, nameColor, sender.user())))); line.append(receiver.parse(new Message(format, message))); return line.build(); } diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java index 24082e65e..a88b55c30 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -41,6 +41,7 @@ import de.steamwar.sql.UserPerm; import de.steamwar.velocitycore.EventStarter; import de.steamwar.velocitycore.VelocityCore; import de.steamwar.velocitycore.listeners.BasicListener; +import de.steamwar.velocitycore.tablist.TablistManager; import lombok.Getter; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.HoverEvent; @@ -68,7 +69,6 @@ public class ChatManager extends BasicListener { private static final Map locals = new ConcurrentHashMap<>(); private static final Map fightTeams = new ConcurrentHashMap<>(); - private static final Map fightInfos = new ConcurrentHashMap<>(); private static final Map teams = new ConcurrentHashMap<>(); private static final Set directs = ConcurrentHashMap.newKeySet(); @@ -120,6 +120,38 @@ public class ChatManager extends BasicListener { return Subserver.isArena(Subserver.getSubserver(server.getServerInfo())) || EventStarter.isSpectateServer(server.getServerInfo().getName()); } + /** + * @return all servers using a fight chat (arenas and event spectate servers) + */ + public static Stream fightServers() { + return VelocityCore.getProxy().getAllServers().stream().filter(ChatManager::isArena); + } + + /** + * @return the local channel of the server the player is on, null while the player is on no server + */ + public static LocalChannel currentLocal(Player player) { + return currentServer(player).map(ChatManager::local).orElse(null); + } + + static boolean onServer(Chatter chatter, RegisteredServer server) { + Player player = chatter.getPlayer(); + return player != null && server.getPlayersConnected().contains(player); + } + + /** + * Sends the message into the channel or makes it the default channel of the player if there is no message. + */ + public static void sendOrSwitch(Chatter sender, ChatChannel channel, String[] message, String usage) { + if (message.length > 0) { + channel.send(sender, String.join(" ", message)); + } else if (sender instanceof PlayerChatter player) { + switchTo(player, channel); + } else { + sender.system(usage); + } + } + private static Optional currentServer(Player player) { return player.getCurrentServer().map(ServerConnection::getServer); } @@ -186,7 +218,7 @@ public class ChatManager extends BasicListener { * @return true if the channel is the fight chat of an arena the player is not on, which moderators can read without joining the arena */ private static boolean isForeignArena(PlayerChatter player, ChatChannel channel) { - return channel instanceof LocalChannel local && local.isArena() && !local.getServer().getPlayersConnected().contains(player.getPlayer()); + return channel instanceof LocalChannel local && local.isArena() && !onServer(player, local.getServer()); } public static void join(PlayerChatter player, ChatChannel channel) { @@ -225,7 +257,7 @@ public class ChatManager extends BasicListener { } public static FightInfoPacket getFightInfo(RegisteredServer server) { - return fightInfos.get(server); + return TablistManager.getFightInfo(server); } /** @@ -248,9 +280,7 @@ public class ChatManager extends BasicListener { /** * Updates the fight teams of an arena. Players who just joined a fight team write into the fight chat by default. */ - public static void updateFightInfo(RegisteredServer server, FightInfoPacket packet) { - fightInfos.keySet().removeIf(s -> s.getPlayersConnected().isEmpty()); - FightInfoPacket previous = fightInfos.put(server, packet); + public static void updateFightInfo(RegisteredServer server, FightInfoPacket previous, FightInfoPacket packet) { Set joined = new HashSet<>(packet.getBluePlayers()); joined.addAll(packet.getRedPlayers()); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java index 869d7c242..4e018ddd4 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java @@ -77,7 +77,7 @@ public class FightTeamChannel extends ChatChannel { protected Message icon(Chatter receiver) { String teamName = teamName(); String color = teamName.length() >= 2 && teamName.charAt(0) == '§' ? teamName.substring(0, 2) : "§7"; - if (receiver.getPlayer() != null && server.getPlayersConnected().contains(receiver.getPlayer())) { + if (ChatManager.onServer(receiver, server)) { return new Message("CHAT_ICON_FIGHT_TEAM", color); } return new Message("CHAT_ICON_FIGHT_TEAM_FOREIGN", color, server.getServerInfo().getName()); @@ -93,7 +93,7 @@ public class FightTeamChannel extends ChatChannel { @Override public boolean canWrite(Chatter sender) { - if (!isMember(sender) || sender.getPlayer() == null || !server.getPlayersConnected().contains(sender.getPlayer())) { + if (!isMember(sender) || !ChatManager.onServer(sender, server)) { sender.system("CHAT_FIGHT_NO_TEAM"); return false; } @@ -102,7 +102,7 @@ public class FightTeamChannel extends ChatChannel { @Override protected Component format(Chatter receiver, Chatter sender, String message) { - return ChatFormat.fight(receiver, server, sender, teamName(), "CHAT_FIGHT_TEAM", ChatFormat.highlightMentions(message, "§f", receiver)); + return ChatFormat.fight(receiver, server, sender, teamName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_TEAM", ChatFormat.highlightMentions(message, "§f", receiver)); } @Override diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java index 12aece2ae..b4e4892d9 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java @@ -68,7 +68,7 @@ public class LocalChannel extends ChatChannel { if (!isArena()) return new Message("CHAT_ICON_LOCAL"); // Moderators reading an arena they are not on need to know which arena it is - if (receiver.getPlayer() != null && server.getPlayersConnected().contains(receiver.getPlayer())) { + if (ChatManager.onServer(receiver, server)) { return new Message("CHAT_ICON_FIGHT"); } return new Message("CHAT_ICON_FIGHT_FOREIGN", server.getServerInfo().getName()); @@ -85,9 +85,8 @@ public class LocalChannel extends ChatChannel { @Override public boolean canWrite(Chatter sender) { - boolean onServer = sender.getPlayer() != null && server.getPlayersConnected().contains(sender.getPlayer()); - // Moderators can write into fight chats they are not on (/chatspy) - if (!onServer && !(isArena() && sender.user().hasPerm(UserPerm.MODERATION))) { + // Moderators can write into fight chats they are not on (/chat msg) + if (!ChatManager.onServer(sender, server) && !(isArena() && sender.user().hasPerm(UserPerm.MODERATION))) { sender.system("CHAT_NO_WRITE"); return false; } @@ -97,20 +96,20 @@ public class LocalChannel extends ChatChannel { @Override protected Component format(Chatter receiver, Chatter sender, String message) { if (!isArena()) { - return ChatFormat.player("CHAT_LOCAL", receiver, sender, null, message); + return ChatFormat.player("CHAT_GLOBAL", receiver, sender, null, message); } String highlighted = ChatFormat.highlightMentions(message, "§7", receiver); FightInfoPacket info = ChatManager.getFightInfo(server); int id = sender.user().getId(); if (info != null && info.getBluePlayers().contains(id)) { - return ChatFormat.fight(receiver, server, sender, info.getBlueName(), "CHAT_FIGHT_PARTICIPANT", highlighted); + return ChatFormat.fight(receiver, server, sender, info.getBlueName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_PARTICIPANT", highlighted); } else if (info != null && info.getRedPlayers().contains(id)) { - return ChatFormat.fight(receiver, server, sender, info.getRedName(), "CHAT_FIGHT_PARTICIPANT", highlighted); + return ChatFormat.fight(receiver, server, sender, info.getRedName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_PARTICIPANT", highlighted); } else if (ChatManager.isReferee(server, id)) { - return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_REFEREE", highlighted); + return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_REFEREE_SENDER", "CHAT_FIGHT_REFEREE", highlighted); } - return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_SPECTATOR", highlighted); + return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_SPECTATOR_SENDER", "CHAT_FIGHT_SPECTATOR", highlighted); } @Override diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java index b7a5bb050..23e5ea697 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java @@ -20,17 +20,13 @@ package de.steamwar.velocitycore.commands; -import com.velocitypowered.api.proxy.ServerConnection; -import com.velocitypowered.api.proxy.server.RegisteredServer; import de.steamwar.command.PreviousArguments; import de.steamwar.command.SWCommand; import de.steamwar.command.TypeMapper; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; import de.steamwar.messages.PlayerChatter; -import de.steamwar.persistent.Subserver; import de.steamwar.sql.UserPerm; -import de.steamwar.velocitycore.VelocityCore; import de.steamwar.velocitycore.chat.ChatChannel; import de.steamwar.velocitycore.chat.ChatManager; @@ -81,7 +77,7 @@ public class ChatCommand extends SWCommand { public ChatChannel map(Chatter sender, PreviousArguments previousArguments, String s) { return switch (s.toLowerCase()) { case "global" -> ChatManager.getGlobal(); - case "local" -> sender.getPlayer() == null ? null : sender.getPlayer().getCurrentServer().map(ServerConnection::getServer).map(ChatManager::local).orElse(null); + case "local" -> sender.getPlayer() == null ? null : ChatManager.currentLocal(sender.getPlayer()); case "team", "tc" -> sender.user().getTeam() != 0 ? ChatManager.team(sender.user().getTeam()) : null; case "serverteam", "stc" -> sender.user().hasPerm(UserPerm.TEAM) ? ChatManager.getServerTeam() : null; default -> arena(sender, s); @@ -99,10 +95,7 @@ public class ChatCommand extends SWCommand { if (sender.user().getTeam() != 0) channels.add("team"); if (sender.user().hasPerm(UserPerm.TEAM)) channels.add("serverteam"); if (sender.user().hasPerm(UserPerm.MODERATION)) { - List subserverList = Subserver.getServerList(); - synchronized (subserverList) { - subserverList.stream().filter(Subserver::isArena).map(subserver -> subserver.getServer().getName()).forEach(channels::add); - } + ChatManager.fightServers().map(server -> server.getServerInfo().getName()).forEach(channels::add); } return channels; } @@ -110,14 +103,15 @@ public class ChatCommand extends SWCommand { } /** - * Moderators can address the fight chat of every arena by its server name. + * Moderators can address the fight chat of every fight server by its server name. */ private static ChatChannel arena(Chatter sender, String name) { if (!sender.user().hasPerm(UserPerm.MODERATION)) return null; - Subserver subserver = Subserver.getSubserver(VelocityCore.getProxy().getServer(name).map(RegisteredServer::getServerInfo).orElse(null)); - if (!Subserver.isArena(subserver)) return null; - - return ChatManager.local(subserver.getRegisteredServer()); + return ChatManager.fightServers() + .filter(server -> server.getServerInfo().getName().equalsIgnoreCase(name)) + .findFirst() + .map(ChatManager::local) + .orElse(null); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java index 6ce66d439..489fa83b8 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java @@ -23,7 +23,6 @@ package de.steamwar.velocitycore.commands; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; -import de.steamwar.messages.PlayerChatter; import de.steamwar.velocitycore.chat.ChatManager; @Linked @@ -35,12 +34,6 @@ public class GlobalCommand extends SWCommand { @Register(description = "GLOBAL_USAGE") public void genericCommand(Chatter sender, String... message) { - if (message.length > 0) { - ChatManager.getGlobal().send(sender, String.join(" ", message)); - } else if (sender instanceof PlayerChatter player) { - ChatManager.switchTo(player, ChatManager.getGlobal()); - } else { - sender.system("GLOBAL_USAGE"); - } + ChatManager.sendOrSwitch(sender, ChatManager.getGlobal(), message, "GLOBAL_USAGE"); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java index ab52364de..b52a014f3 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java @@ -19,7 +19,6 @@ package de.steamwar.velocitycore.commands; -import com.velocitypowered.api.proxy.ServerConnection; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.PlayerChatter; @@ -35,13 +34,9 @@ public class LocalCommand extends SWCommand { @Register(description = "CHAT_BC_USAGE") public void genericCommand(PlayerChatter sender, String... message) { - ChatChannel channel = sender.getPlayer().getCurrentServer().map(ServerConnection::getServer).map(ChatManager::local).orElse(null); + ChatChannel channel = ChatManager.currentLocal(sender.getPlayer()); if (channel == null) return; - if (message.length == 0) { - ChatManager.switchTo(sender, channel); - } else { - channel.send(sender, String.join(" ", message)); - } + ChatManager.sendOrSwitch(sender, channel, message, "CHAT_BC_USAGE"); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java index ea3932b47..024c118da 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java @@ -22,7 +22,6 @@ package de.steamwar.velocitycore.commands; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; -import de.steamwar.messages.PlayerChatter; import de.steamwar.sql.UserPerm; import de.steamwar.velocitycore.chat.ChatManager; @@ -35,12 +34,6 @@ public class ServerTeamchatCommand extends SWCommand { @Register(description = "STC_USAGE") public void genericCommand(Chatter sender, String... message) { - if (message.length > 0) { - ChatManager.getServerTeam().send(sender, String.join(" ", message)); - } else if (sender instanceof PlayerChatter player) { - ChatManager.switchTo(player, ChatManager.getServerTeam()); - } else { - sender.system("STC_USAGE"); - } + ChatManager.sendOrSwitch(sender, ChatManager.getServerTeam(), message, "STC_USAGE"); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java index 1f6d8458d..39de54578 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java @@ -22,8 +22,6 @@ package de.steamwar.velocitycore.commands; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; -import de.steamwar.messages.PlayerChatter; -import de.steamwar.velocitycore.chat.ChatChannel; import de.steamwar.velocitycore.chat.ChatManager; @Linked @@ -41,13 +39,6 @@ public class TeamchatCommand extends SWCommand { return; } - ChatChannel channel = ChatManager.team(team); - if (args.length > 0) { - channel.send(sender, String.join(" ", args)); - } else if (sender instanceof PlayerChatter player) { - ChatManager.switchTo(player, channel); - } else { - sender.system("TC_USAGE"); - } + ChatManager.sendOrSwitch(sender, ChatManager.team(team), args, "TC_USAGE"); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java index e14f10c26..b7ad0d196 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java +++ b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java @@ -51,8 +51,8 @@ public class FightInfoHandler extends PacketHandler { FightInfoPacket lobbyPacket = packet.withServerName(server.getServerInfo().getName()); - TablistManager.newFightInfo(server, packet); - ChatManager.updateFightInfo(server, packet); + FightInfoPacket previous = TablistManager.newFightInfo(server, packet); + ChatManager.updateFightInfo(server, previous, packet); lobbys.removeIf(s -> s.getPlayersConnected().isEmpty()); lobbys.forEach(lobby -> NetworkSender.send(lobby, lobbyPacket)); diff --git a/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java index 7d2fc7ae7..9631720d0 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java @@ -45,9 +45,17 @@ public class TablistManager extends BasicListener implements Disable { private static final Map fightInfos = new HashMap<>(); - public static synchronized void newFightInfo(RegisteredServer info, FightInfoPacket packet) { - fightInfos.put(info, packet); + /** + * @return the previous fight info of the server, null if there was none + */ + public static synchronized FightInfoPacket newFightInfo(RegisteredServer info, FightInfoPacket packet) { + FightInfoPacket previous = fightInfos.put(info, packet); fightInfos.keySet().removeIf(serverInfo -> serverInfo.getPlayersConnected().isEmpty()); + return previous; + } + + public static synchronized FightInfoPacket getFightInfo(RegisteredServer server) { + return fightInfos.get(server); } private final Map tablists = new HashMap<>();