diff --git a/CommonCore/Network/src/de/steamwar/network/packets/server/ChatRecordPacket.java b/CommonCore/Network/src/de/steamwar/network/packets/server/ChatRecordPacket.java
new file mode 100644
index 00000000..642cf335
--- /dev/null
+++ b/CommonCore/Network/src/de/steamwar/network/packets/server/ChatRecordPacket.java
@@ -0,0 +1,40 @@
+/*
+ * 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.network.packets.server;
+
+import de.steamwar.network.packets.NetworkPacket;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * 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 798a2932..2f4f87a0 100644
--- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties
+++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties
@@ -573,12 +573,51 @@ 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_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_MSG = §e{0}§8»§e{1} §7{2}
+CHAT_SERVERTEAM_SENDER = §e{0}
+CHAT_SERVERTEAM = §8» §f{2}
+CHAT_GLOBAL_SENDER = {3}{4}{5}{6}{0}
+CHAT_GLOBAL = §8» {7}{2}
+CHAT_TEAM_SENDER = §e{0}
+CHAT_TEAM = §8» §f{2}
+CHAT_MSG_SENDER = §e{0}
+CHAT_MSG = §8»§e{1} §7{2}
+CHAT_SPECIAL_ALERT = §e{0}§8»§e{1} §7{2}
+CHAT_FIGHT_PARTICIPANT_SENDER = {0}{1}
+CHAT_FIGHT_PARTICIPANT = §8» §7{0}
+CHAT_FIGHT_SPECTATOR_SENDER = §7{1}
+CHAT_FIGHT_SPECTATOR = §8» §7{0}
+CHAT_FIGHT_REFEREE_SENDER = §e{1}
+CHAT_FIGHT_REFEREE = §8» §e{0}
+CHAT_FIGHT_TEAM = §8» §f{0}
+CHAT_ICON = {0}{1}
+CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Prefix: §r{1}{2}
+CHAT_ICON_GLOBAL = 🌍
+CHAT_ICON_LOCAL = ⌂
+CHAT_ICON_FIGHT = ⚔
+CHAT_ICON_FIGHT_FOREIGN = ⚔ §8{0}
+CHAT_ICON_FIGHT_TEAM = {0}⚑
+CHAT_ICON_FIGHT_TEAM_FOREIGN = {0}⚑ §8{1}
+CHAT_ICON_TEAM = 👥
+CHAT_ICON_SERVERTEAM = ⚙
+CHAT_ICON_MSG = ✉
+CHAT_CHANNEL_GLOBAL = §eGlobal
+CHAT_CHANNEL_ARENA = §e{0}
+CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1}
+CHAT_CHANNEL_TEAM = §eTC
+CHAT_CHANNEL_SERVERTEAM = §eSTC
+CHAT_CHANNEL_MSG = §eMSG §7{0}
+CHAT_CHANNEL_ALERT = §cAlert
+CHAT_CHANNEL_LOCAL = §eLocal
+CHAT_CHANNEL_DEFAULT = §ais your default channel
+CHAT_CHANNEL_NOT_SWITCHABLE = §cYou cannot write into this channel by default.
+CHAT_NO_WRITE = §cYou cannot write into this channel.
+CHAT_FIGHT_NO_TEAM = §cYou are not in a fight team.
+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 d07b6c7b..2cedc5b1 100644
--- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties
+++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties
@@ -538,12 +538,51 @@ 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_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_MSG = §e{0}§8»§e{1} §7{2}
+CHAT_SERVERTEAM_SENDER = §e{0}
+CHAT_SERVERTEAM = §8» §f{2}
+CHAT_GLOBAL_SENDER = {3}{4}{5}{6}{0}
+CHAT_GLOBAL = §8» {7}{2}
+CHAT_TEAM_SENDER = §e{0}
+CHAT_TEAM = §8» §f{2}
+CHAT_MSG_SENDER = §e{0}
+CHAT_MSG = §8»§e{1} §7{2}
+CHAT_SPECIAL_ALERT = §e{0}§8»§e{1} §7{2}
+CHAT_FIGHT_PARTICIPANT_SENDER = {0}{1}
+CHAT_FIGHT_PARTICIPANT = §8» §7{0}
+CHAT_FIGHT_SPECTATOR_SENDER = §7{1}
+CHAT_FIGHT_SPECTATOR = §8» §7{0}
+CHAT_FIGHT_REFEREE_SENDER = §e{1}
+CHAT_FIGHT_REFEREE = §8» §e{0}
+CHAT_FIGHT_TEAM = §8» §f{0}
+CHAT_ICON = {0}{1}
+CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Kürzel: §r{1}{2}
+CHAT_ICON_GLOBAL = 🌍
+CHAT_ICON_LOCAL = ⌂
+CHAT_ICON_FIGHT = ⚔
+CHAT_ICON_FIGHT_FOREIGN = ⚔ §8{0}
+CHAT_ICON_FIGHT_TEAM = {0}⚑
+CHAT_ICON_FIGHT_TEAM_FOREIGN = {0}⚑ §8{1}
+CHAT_ICON_TEAM = 👥
+CHAT_ICON_SERVERTEAM = ⚙
+CHAT_ICON_MSG = ✉
+CHAT_CHANNEL_GLOBAL = §eGlobal
+CHAT_CHANNEL_ARENA = §e{0}
+CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1}
+CHAT_CHANNEL_TEAM = §eTC
+CHAT_CHANNEL_SERVERTEAM = §eSTC
+CHAT_CHANNEL_MSG = §eMSG §7{0}
+CHAT_CHANNEL_ALERT = §cAlert
+CHAT_CHANNEL_LOCAL = §eLokal
+CHAT_CHANNEL_DEFAULT = §aist dein Standardkanal
+CHAT_CHANNEL_NOT_SWITCHABLE = §cIn diesen Kanal kannst du nicht standardmäßig schreiben.
+CHAT_NO_WRITE = §cDu kannst nicht in diesen Kanal schreiben.
+CHAT_FIGHT_NO_TEAM = §cDu bist in keinem Kampfteam.
+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/EventStarter.java b/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java
index 5790e53e..0c2c0aa8 100644
--- a/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java
+++ b/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java
@@ -31,8 +31,10 @@ import net.kyori.adventure.text.event.ClickEvent;
import java.sql.Timestamp;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
import java.util.Queue;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
import static de.steamwar.persistent.Storage.eventServer;
@@ -43,8 +45,15 @@ public class EventStarter {
@Getter
private static final Map spectatePorts = new HashMap<>();
- public static void addSpectateServer(int port, String command) {
+ private static final Set spectateServers = new HashSet<>();
+
+ public static void addSpectateServer(int port, String command, String serverName) {
spectatePorts.put(port, command);
+ spectateServers.add(serverName);
+ }
+
+ public static boolean isSpectateServer(String serverName) {
+ return spectateServers.contains(serverName);
}
public EventStarter() {
diff --git a/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java b/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java
index 6240f0c0..1701a10d 100644
--- a/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java
+++ b/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java
@@ -236,7 +236,7 @@ public class VelocityCore implements ReloadablePlugin {
String cmd = cmds.remove(0);
if (server.getSpectatePort() != 0) {
- EventStarter.addSpectateServer(server.getSpectatePort(), cmd);
+ EventStarter.addSpectateServer(server.getSpectatePort(), cmd, entry.getKey());
}
new ServerSwitchCommand(cmd, entry.getKey(), server.getSpectatePort() != 0, cmds.toArray(new String[0]));
diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java
new file mode 100644
index 00000000..a86964fb
--- /dev/null
+++ b/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java
@@ -0,0 +1,71 @@
+/*
+ * 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 net.kyori.adventure.text.Component;
+
+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 Component format(Chatter receiver, Chatter sender, String message) {
+ return receiver.parse(true, 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.sendMessage(format(reader, sender, message)));
+ }
+}
diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java
new file mode 100644
index 00000000..9e8e5861
--- /dev/null
+++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java
@@ -0,0 +1,118 @@
+/*
+ * 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 net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.event.HoverEvent;
+
+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 channel related system messages
+ */
+ public abstract Message tag(Chatter receiver);
+
+ /**
+ * @return the icon always shown in front of messages of this channel, null if the message format already shows the channel.
+ * The icon is highlighted if this is the default channel of the receiver.
+ */
+ protected Message icon(Chatter receiver) {
+ return null;
+ }
+
+ 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 Component 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;
+
+ Component msg = format(receiver, sender, coloredMessage);
+ Message icon = icon(receiver);
+ if (icon != null) {
+ Component iconComponent = receiver.parse(new Message("CHAT_ICON", ChatManager.getDefault(receiver) == this ? "§e" : "§7", icon))
+ .hoverEvent(HoverEvent.showText(receiver.parse(tag(receiver))));
+ msg = Component.text().append(iconComponent).append(Component.text(" ")).append(msg).build();
+ }
+ receiver.sendMessage(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..51c2841c
--- /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_SPECIAL_ALERT", name, sender.user(), new Message(baseMessage + (finalI + 1)))).delay(delay[i], TimeUnit.SECONDS).schedule();
+ }
+ }
+}
diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java
new file mode 100644
index 00000000..f5b945dc
--- /dev/null
+++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java
@@ -0,0 +1,141 @@
+/*
+ * 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.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 net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.TextComponent;
+import net.kyori.adventure.text.event.HoverEvent;
+
+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. The sender part ({@code _SENDER})
+ * shows the team of the sender on hover.
+ */
+ public static Component 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";
+ Object[] params = {
+ user,
+ msgReceiver == null ? receiver : msgReceiver,
+ highlightMentions(message, chatColorCode, receiver),
+ teamPrefix,
+ "",
+ prefix == UserPerm.emptyPrefix ? "§f" : prefix.getColorCode(),
+ prefix.getChatPrefix().length() == 0 ? "§f" : prefix.getChatPrefix() + " ",
+ chatColorCode};
+
+ Component senderPart = withTeamHover(receiver, user, receiver.parse(new Message(format + "_SENDER", params)));
+ return Component.text().append(senderPart).append(receiver.parse(new Message(format, params))).build();
+ }
+
+ /**
+ * Formats a fight chat message. The fight team name is only hoverable in event fights, where it is the actual team.
+ *
+ * @param coloredTeamName the fight team of the sender, null if the sender is not in a fight team
+ */
+ public static Component fight(Chatter receiver, RegisteredServer server, Chatter sender, String coloredTeamName, String senderFormat, String format, String message) {
+ TextComponent.Builder line = Component.text();
+ String nameColor = "";
+ if (coloredTeamName != null) {
+ Component team = Chatter.SERIALIZER.deserialize(coloredTeamName);
+ Team eventTeam = ChatManager.eventTeam(server, coloredTeamName);
+ if (eventTeam != null) {
+ team = team.hoverEvent(HoverEvent.showText(teamHover(receiver, eventTeam)));
+ }
+ line.append(team).append(Component.text(" "));
+ nameColor = coloredTeamName.length() >= 2 && coloredTeamName.charAt(0) == '§' ? coloredTeamName.substring(0, 2) : "";
+ }
+
+ line.append(withTeamHover(receiver, sender.user(), receiver.parse(new Message(senderFormat, nameColor, sender.user()))));
+ line.append(receiver.parse(new Message(format, message)));
+ return line.build();
+ }
+
+ /**
+ * Shows the team of the user when hovering the component, the SteamWar team for server team members using the SW prefix.
+ */
+ public static Component withTeamHover(Chatter receiver, SteamwarUser user, Component component) {
+ if (useSwPrefix(user)) {
+ return component.hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM", "SteamWar", "§e", "S§8W"))));
+ }
+ if (user.getTeam() == 0) return component;
+
+ return component.hoverEvent(HoverEvent.showText(teamHover(receiver, Team.byId(user.getTeam()))));
+ }
+
+ private static Component teamHover(Chatter receiver, Team team) {
+ return receiver.parse(new Message("CHAT_HOVER_TEAM", team.getTeamName(), "§" + team.getTeamColor(), team.getTeamKuerzel()));
+ }
+
+ private static boolean useSwPrefix(SteamwarUser user) {
+ if (!user.hasPerm(UserPerm.TEAM)) {
+ return false;
+ }
+
+ 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..a88b55c3
--- /dev/null
+++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java
@@ -0,0 +1,455 @@
+/*
+ * 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.Message;
+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.Team;
+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 de.steamwar.velocitycore.tablist.TablistManager;
+import lombok.Getter;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.event.HoverEvent;
+
+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 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;
+ }
+
+ /**
+ * @return true for arenas and event spectate servers, which use a fight chat instead of the global chat
+ */
+ public static boolean isArena(RegisteredServer server) {
+ return Subserver.isArena(Subserver.getSubserver(server.getServerInfo())) || EventStarter.isSpectateServer(server.getServerInfo().getName());
+ }
+
+ /**
+ * @return all servers using a fight chat (arenas and event spectate servers)
+ */
+ public static Stream fightServers() {
+ return VelocityCore.getProxy().getAllServers().stream().filter(ChatManager::isArena);
+ }
+
+ /**
+ * @return the local channel of the server the player is on, null while the player is on no server
+ */
+ public static LocalChannel currentLocal(Player player) {
+ return currentServer(player).map(ChatManager::local).orElse(null);
+ }
+
+ static boolean onServer(Chatter chatter, RegisteredServer server) {
+ Player player = chatter.getPlayer();
+ return player != null && server.getPlayersConnected().contains(player);
+ }
+
+ /**
+ * Sends the message into the channel or makes it the default channel of the player if there is no message.
+ */
+ public static void sendOrSwitch(Chatter sender, ChatChannel channel, String[] message, String usage) {
+ if (message.length > 0) {
+ channel.send(sender, String.join(" ", message));
+ } else if (sender instanceof PlayerChatter player) {
+ switchTo(player, channel);
+ } else {
+ sender.system(usage);
+ }
+ }
+
+ private static Optional currentServer(Player player) {
+ return player.getCurrentServer().map(ServerConnection::getServer);
+ }
+
+ 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() && !onServer(player, local.getServer());
+ }
+
+ 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 TablistManager.getFightInfo(server);
+ }
+
+ /**
+ * @return the team of an event fight on the server whose colored name is shown, null if the server runs no event fight
+ */
+ static Team eventTeam(RegisteredServer server, String coloredTeamName) {
+ Subserver subserver = Subserver.getSubserver(server.getServerInfo());
+ if (subserver == null) return null;
+
+ String kuerzel = coloredTeamName.replaceAll("§.", "");
+ for (Map.Entry entry : new ArrayList<>(EventStarter.getEventServer().entrySet())) {
+ if (entry.getValue() != subserver) continue;
+
+ Team team = Team.byId(entry.getKey());
+ if (team != null && team.getTeamKuerzel().equals(kuerzel)) return team;
+ }
+ return null;
+ }
+
+ /**
+ * Updates the fight teams of an arena. Players who just joined a fight team write into the fight chat by default.
+ */
+ public static void updateFightInfo(RegisteredServer server, FightInfoPacket previous, FightInfoPacket 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);
+ notifyDefault(player, channel);
+ }
+
+ 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)) {
+ notifyDefault(player, channel);
+ } else if (previous != channel && !player.parseToPlain(previous.tag(player)).equals(player.parseToPlain(channel.tag(player)))) {
+ notifyDefault(player, channel);
+ }
+ }
+
+ /**
+ * Tells the player which channel they write into, showing the channel icon with its name on hover.
+ */
+ public static void notifyDefault(PlayerChatter player, ChatChannel channel) {
+ Message icon = channel.icon(player);
+ Component iconComponent = icon == null ? player.parse(channel.tag(player)) : player.parse(new Message("CHAT_ICON", "§a", icon));
+ player.sendMessage(Component.text()
+ .append(iconComponent.hoverEvent(HoverEvent.showText(player.parse(channel.tag(player)))))
+ .append(Component.text(" "))
+ .append(player.parse(new Message("CHAT_CHANNEL_DEFAULT")))
+ .build());
+ }
+
+ /**
+ * Handles a chat message the player typed without a command.
+ */
+ 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 local) {
+ FightTeamChannel blue = fightTeam(server, true);
+ if (blue.isMember(sender)) return blue;
+
+ FightTeamChannel red = fightTeam(server, false);
+ if (red.isMember(sender)) return red;
+
+ // Spectators on fight servers stay in the fight chat
+ return local.isArena() ? local : 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);
+
+ directs.removeIf(channel -> channel.contains(uuid));
+ }
+}
diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java
new file mode 100644
index 00000000..f0a79923
--- /dev/null
+++ b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java
@@ -0,0 +1,116 @@
+/*
+ * 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 net.kyori.adventure.text.Component;
+
+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 boolean switchable() {
+ return false;
+ }
+
+ @Override
+ protected Message icon(Chatter receiver) {
+ return new Message("CHAT_ICON_MSG");
+ }
+
+ @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 Component 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..4e018ddd
--- /dev/null
+++ b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java
@@ -0,0 +1,112 @@
+/*
+ * 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 net.kyori.adventure.text.Component;
+
+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
+ protected Message icon(Chatter receiver) {
+ String teamName = teamName();
+ String color = teamName.length() >= 2 && teamName.charAt(0) == '§' ? teamName.substring(0, 2) : "§7";
+ if (ChatManager.onServer(receiver, server)) {
+ return new Message("CHAT_ICON_FIGHT_TEAM", color);
+ }
+ return new Message("CHAT_ICON_FIGHT_TEAM_FOREIGN", color, server.getServerInfo().getName());
+ }
+
+ @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) || !ChatManager.onServer(sender, server)) {
+ sender.system("CHAT_FIGHT_NO_TEAM");
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected Component format(Chatter receiver, Chatter sender, String message) {
+ return ChatFormat.fight(receiver, server, sender, teamName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_TEAM", 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..cc2219cd
--- /dev/null
+++ b/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java
@@ -0,0 +1,69 @@
+/*
+ * 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 net.kyori.adventure.text.Component;
+
+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
+ protected Message icon(Chatter receiver) {
+ return new Message("CHAT_ICON_GLOBAL");
+ }
+
+ @Override
+ public Stream readers() {
+ return Stream.concat(Stream.of(Chatter.console()), Chatter.allPlayers().filter(ChatManager::readsGlobal).map(Chatter::of).filter(chatter -> ChatManager.reads(chatter, persistId())));
+ }
+
+ @Override
+ public boolean canWrite(Chatter sender) {
+ return true;
+ }
+
+ @Override
+ protected Component format(Chatter receiver, Chatter sender, String message) {
+ return ChatFormat.player("CHAT_GLOBAL", receiver, sender, null, message);
+ }
+
+ @Override
+ protected boolean delivered(Chatter sender, String message, boolean mirror) {
+ if (mirror) {
+ DiscordBot.withBot(bot -> bot.getIngameChat().sendMessage(format(bot.getIngameChat(), sender, message)));
+ }
+ return true;
+ }
+}
diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java
new file mode 100644
index 00000000..b4e4892d
--- /dev/null
+++ b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java
@@ -0,0 +1,128 @@
+/*
+ * 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 net.kyori.adventure.text.Component;
+
+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
+ protected Message icon(Chatter receiver) {
+ if (!isArena()) return new Message("CHAT_ICON_LOCAL");
+
+ // Moderators reading an arena they are not on need to know which arena it is
+ if (ChatManager.onServer(receiver, server)) {
+ return new Message("CHAT_ICON_FIGHT");
+ }
+ return new Message("CHAT_ICON_FIGHT_FOREIGN", server.getServerInfo().getName());
+ }
+
+ @Override
+ public Stream readers() {
+ Stream players = server.getPlayersConnected().stream().map(Chatter::of).filter(chatter -> ChatManager.reads(chatter, persistId())).map(Chatter.class::cast);
+ if (isArena()) {
+ players = Stream.concat(players, ChatManager.spies(server));
+ }
+ return players;
+ }
+
+ @Override
+ public boolean canWrite(Chatter sender) {
+ // Moderators can write into fight chats they are not on (/chat msg)
+ if (!ChatManager.onServer(sender, server) && !(isArena() && sender.user().hasPerm(UserPerm.MODERATION))) {
+ sender.system("CHAT_NO_WRITE");
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected Component format(Chatter receiver, Chatter sender, String message) {
+ if (!isArena()) {
+ return ChatFormat.player("CHAT_GLOBAL", receiver, sender, null, message);
+ }
+
+ String highlighted = ChatFormat.highlightMentions(message, "§7", receiver);
+ FightInfoPacket info = ChatManager.getFightInfo(server);
+ int id = sender.user().getId();
+ if (info != null && info.getBluePlayers().contains(id)) {
+ return ChatFormat.fight(receiver, server, sender, info.getBlueName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_PARTICIPANT", highlighted);
+ } else if (info != null && info.getRedPlayers().contains(id)) {
+ return ChatFormat.fight(receiver, server, sender, info.getRedName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_PARTICIPANT", highlighted);
+ } else if (ChatManager.isReferee(server, id)) {
+ return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_REFEREE_SENDER", "CHAT_FIGHT_REFEREE", highlighted);
+ }
+ return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_SPECTATOR_SENDER", "CHAT_FIGHT_SPECTATOR", 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..1860633d
--- /dev/null
+++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java
@@ -0,0 +1,82 @@
+/*
+ * 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 net.kyori.adventure.text.Component;
+
+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
+ protected Message icon(Chatter receiver) {
+ return new Message("CHAT_ICON_SERVERTEAM");
+ }
+
+ @Override
+ public Stream readers() {
+ return Arrays.stream(Chatter.serverteam().getChatters()).filter(chatter -> ChatManager.reads(chatter, persistId()));
+ }
+
+ @Override
+ public boolean canWrite(Chatter sender) {
+ if (!sender.user().hasPerm(UserPerm.TEAM)) {
+ sender.system("CHAT_NO_WRITE");
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected Component format(Chatter receiver, Chatter sender, String message) {
+ return ChatFormat.player("CHAT_SERVERTEAM", receiver, sender, null, message);
+ }
+
+ @Override
+ protected boolean delivered(Chatter sender, String message, boolean mirror) {
+ if (mirror) {
+ DiscordBot.withBot(bot -> bot.getServerTeamChat().sendMessage(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..f362099b
--- /dev/null
+++ b/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java
@@ -0,0 +1,85 @@
+/*
+ * 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 net.kyori.adventure.text.Component;
+
+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
+ protected Message icon(Chatter receiver) {
+ return new Message("CHAT_ICON_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 Component 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..23e5ea69
--- /dev/null
+++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.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.commands;
+
+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.sql.UserPerm;
+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) {
+ ChatManager.notifyDefault(sender, ChatManager.getDefault(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 : ChatManager.currentLocal(sender.getPlayer());
+ case "team", "tc" -> sender.user().getTeam() != 0 ? ChatManager.team(sender.user().getTeam()) : null;
+ case "serverteam", "stc" -> sender.user().hasPerm(UserPerm.TEAM) ? ChatManager.getServerTeam() : null;
+ default -> arena(sender, s);
+ };
+ }
+
+ @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)) {
+ ChatManager.fightServers().map(server -> server.getServerInfo().getName()).forEach(channels::add);
+ }
+ return channels;
+ }
+ };
+ }
+
+ /**
+ * Moderators can address the fight chat of every fight server by its server name.
+ */
+ private static ChatChannel arena(Chatter sender, String name) {
+ if (!sender.user().hasPerm(UserPerm.MODERATION)) return null;
+
+ return ChatManager.fightServers()
+ .filter(server -> server.getServerInfo().getName().equalsIgnoreCase(name))
+ .findFirst()
+ .map(ChatManager::local)
+ .orElse(null);
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/ChattingEvent.java b/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java
similarity index 57%
rename from SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/ChattingEvent.java
rename to VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java
index b39a4943..489fa83b 100644
--- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/ChattingEvent.java
+++ b/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.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,23 @@
* along with this program. If not, see .
*/
-package de.steamwar.core.events;
-import de.steamwar.core.Core;
+package de.steamwar.velocitycore.commands;
+
+import de.steamwar.command.SWCommand;
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;
+import de.steamwar.messages.Chatter;
+import de.steamwar.velocitycore.chat.ChatManager;
@Linked
-public class ChattingEvent implements Listener {
+public class GlobalCommand extends SWCommand {
- @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
- private void onChat(AsyncPlayerChatEvent event) {
- event.setCancelled(true);
- Core.MESSAGE.broadcastPrefixless("LOCAL_CHAT", event.getPlayer().getDisplayName(), event.getMessage());
+ public GlobalCommand() {
+ super("global");
+ }
+
+ @Register(description = "GLOBAL_USAGE")
+ public void genericCommand(Chatter sender, String... message) {
+ ChatManager.sendOrSwitch(sender, ChatManager.getGlobal(), message, "GLOBAL_USAGE");
}
}
diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java
index 40f48fbf..b52a014f 100644
--- a/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java
+++ b/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java
@@ -22,7 +22,8 @@ package de.steamwar.velocitycore.commands;
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 +32,11 @@ 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 = ChatManager.currentLocal(sender.getPlayer());
+ if (channel == null) return;
+
+ ChatManager.sendOrSwitch(sender, channel, message, "CHAT_BC_USAGE");
}
}
diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java
index 622aa2dc..4db12170 100644
--- a/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java
+++ b/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java
@@ -22,12 +22,9 @@ 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.ChatManager;
import static de.steamwar.persistent.Storage.lastChats;
@@ -49,15 +46,7 @@ public class MsgCommand extends SWCommand {
return;
}
- if (IgnoreSystem.isIgnored(target.getUniqueId(), sender.user().getUUID())) {
- sender.system("MSG_IGNORED");
- return;
- }
-
- 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());
+ ChatManager.direct(sender.user().getUUID(), target.getUniqueId()).send(sender, String.join(" ", args));
}
public static void remove(Player player) {
diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java
index 0b04183e..024c118d 100644
--- a/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java
+++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java
@@ -23,7 +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.listeners.ChatListener;
+import de.steamwar.velocitycore.chat.ChatManager;
@Linked
public class ServerTeamchatCommand extends SWCommand {
@@ -33,7 +33,7 @@ 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) {
+ ChatManager.sendOrSwitch(sender, ChatManager.getServerTeam(), message, "STC_USAGE");
}
}
diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java
index e9b1d4d2..39de5457 100644
--- a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java
+++ b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java
@@ -22,9 +22,7 @@ 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.velocitycore.chat.ChatManager;
@Linked
public class TeamchatCommand extends SWCommand {
@@ -34,14 +32,13 @@ 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));
+ ChatManager.sendOrSwitch(sender, ChatManager.team(team), args, "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..b7ad0d19 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;
@@ -50,7 +51,8 @@ public class FightInfoHandler extends PacketHandler {
FightInfoPacket lobbyPacket = packet.withServerName(server.getServerInfo().getName());
- TablistManager.newFightInfo(server, packet);
+ FightInfoPacket previous = TablistManager.newFightInfo(server, packet);
+ ChatManager.updateFightInfo(server, previous, packet);
lobbys.removeIf(s -> s.getPlayersConnected().isEmpty());
lobbys.forEach(lobby -> NetworkSender.send(lobby, lobbyPacket));
diff --git a/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java
index 7d2fc7ae..9631720d 100644
--- a/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java
+++ b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java
@@ -45,9 +45,17 @@ public class TablistManager extends BasicListener implements Disable {
private static final Map fightInfos = new HashMap<>();
- public static synchronized void newFightInfo(RegisteredServer info, FightInfoPacket packet) {
- fightInfos.put(info, packet);
+ /**
+ * @return the previous fight info of the server, null if there was none
+ */
+ public static synchronized FightInfoPacket newFightInfo(RegisteredServer info, FightInfoPacket packet) {
+ FightInfoPacket previous = fightInfos.put(info, packet);
fightInfos.keySet().removeIf(serverInfo -> serverInfo.getPlayersConnected().isEmpty());
+ return previous;
+ }
+
+ public static synchronized FightInfoPacket getFightInfo(RegisteredServer server) {
+ return fightInfos.get(server);
}
private final Map tablists = new HashMap<>();