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