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 b39a4943..642cf335 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 c546644f..1354f2e1 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 0119bf40..b7820066 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 617684d8..d7d19338 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 cf7a991c..00000000
--- 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 e7e13eeb..56622660 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 b7cba3c4..c1cf8e75 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 311e5263..fc06646a 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 b5f361ac..c0353812 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 f008ae6c..55fe89eb 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 96248e40..324fddca 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 6afbaf7e..bff90140 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 5921259a..3f34d888 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 5cc74f23..fc34a4f7 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 00000000..2c194f88
--- /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 00000000..8c3be5f9
--- /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 00000000..efe8ebb2
--- /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 00000000..28a114f2
--- /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 00000000..a437532a
--- /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 00000000..b369bafd
--- /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 00000000..93b80e60
--- /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 00000000..a4146c1d
--- /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 00000000..5eaf90aa
--- /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 00000000..46950b1c
--- /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 00000000..f29a80f4
--- /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 ea846789..552691d2 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 00000000..cd563a81
--- /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 00000000..6ce66d43
--- /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 40f48fbf..ab52364d 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 622aa2dc..67cbd21a 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 ae86f249..65582e85 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 0b04183e..ea3932b4 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 e9b1d4d2..1f6d8458 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 7e7dba1e..4b557bb7 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 ef02ce11..6c69a516 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 ad522fee..1da67ed6 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 704acfec..e14f10c2 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));