Merge pull request 'Chat channels' (#506) from feature/chat-channels into main

Reviewed-on: SteamWar/SteamWar#506
This commit is contained in:
2026-10-16 09:19:54 +02:00
39 changed files with 1786 additions and 377 deletions
@@ -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)!
@@ -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)!
@@ -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))
@@ -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<Integer, String> spectatePorts = new HashMap<>();
public static void addSpectateServer(int port, String command) {
private static final Set<String> 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() {
@@ -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]));
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Chatter> 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)));
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Chatter> 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<Integer, Chatter> 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");
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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();
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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 <format>_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(" "));
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<RegisteredServer, LocalChannel> locals = new ConcurrentHashMap<>();
private static final Map<RegisteredServer, FightTeamChannel[]> fightTeams = new ConcurrentHashMap<>();
private static final Map<Integer, TeamChannel> teams = new ConcurrentHashMap<>();
private static final Set<DirectChannel> directs = ConcurrentHashMap.newKeySet();
private static final Map<UUID, ChatChannel> defaults = new ConcurrentHashMap<>();
private static final Map<UUID, String> persisted = new ConcurrentHashMap<>();
private static final Set<UUID> globalOnArena = ConcurrentHashMap.newKeySet();
private static final Set<UUID> announced = ConcurrentHashMap.newKeySet();
private static final Map<UUID, Set<RegisteredServer>> spies = new ConcurrentHashMap<>();
private static final Map<UUID, Set<String>> hidden = new ConcurrentHashMap<>();
private static int refereeEvent = -1;
private static long refereesLoaded = 0;
private static Set<Integer> 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<RegisteredServer> 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<RegisteredServer> 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<Chatter> 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<Integer> 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<String> hidden(UUID uuid) {
return hidden.computeIfAbsent(uuid, u -> {
Set<String> 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<String> 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<RegisteredServer> 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<Integer, Subserver> 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<Integer> 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));
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Chatter> 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);
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Integer> 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<Chatter> readers() {
List<Integer> 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);
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Chatter> 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;
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Chatter> readers() {
Stream<Chatter> 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 <arena> 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);
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Chatter> 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);
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Chatter> 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);
}
}
@@ -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));
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<ChatChannel> 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<String> tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) {
List<String> 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);
}
}
@@ -0,0 +1,39 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.velocitycore.commands;
import de.steamwar.command.SWCommand;
import de.steamwar.linkage.Linked;
import de.steamwar.messages.Chatter;
import de.steamwar.velocitycore.chat.ChatManager;
@Linked
public class GlobalCommand extends SWCommand {
public GlobalCommand() {
super("global");
}
@Register(description = "GLOBAL_USAGE")
public void genericCommand(Chatter sender, String... message) {
ChatManager.sendOrSwitch(sender, ChatManager.getGlobal(), message, "GLOBAL_USAGE");
}
}
@@ -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");
}
}
@@ -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) {
@@ -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");
}
}
@@ -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");
}
}
@@ -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 {
@@ -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<ChatterGroup> target;
private final ChatChannel target;
public DiscordChatRoom(String channel, String format, Supplier<ChatterGroup> 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);
}
}
@@ -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<String> suggestions = e.getSuggestions();
@@ -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));
@@ -45,9 +45,17 @@ public class TablistManager extends BasicListener implements Disable {
private static final Map<RegisteredServer, FightInfoPacket> 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<Player, Tablist> tablists = new HashMap<>();