From 6330dbc2b1793065dab0f2e776a9f821ca7c8b68 Mon Sep 17 00:00:00 2001 From: killerkuh <54+12312@noreply.localhost> Date: Thu, 24 Sep 2026 21:43:20 +0200 Subject: [PATCH 01/43] VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java aktualisiert --- .../src/de/steamwar/velocitycore/commands/TeamCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java index 0e9b7aba..c3108a55 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java @@ -67,7 +67,7 @@ public class TeamCommand extends SWCommand { helpMessages(sender, "TEAM_HELP_CHAT", "TEAM_HELP_EVENT", "TEAM_HELP_LEAVE"); if (user.isLeader()) { - helpMessages(sender, "TEAM_HELP_INVITE", "TEAM_HELP_REMOVE", "TEAM_HELP_KUERZEL", "TEAM_HELP_NAME", "TEAM_HELP_COLOR", "TEAM_HELP_LEADER", "TEAM_HELP_STEP_BACK", "TEAM_HELP_SERVER"); + helpMessages(sender, "TEAM_HELP_INVITE", "TEAM_HELP_REMOVE", "TEAM_HELP_KUERZEL", "TEAM_HELP_NAME", "TEAM_HELP_COLOR", "TEAM_HELP_LEADER", "TEAM_HELP_STEP_BACK"); } } } From 5a224c1bcc53ccce421e8ceddd63408e4865554a Mon Sep 17 00:00:00 2001 From: killerkuh <54+12312@noreply.localhost> Date: Thu, 24 Sep 2026 21:46:18 +0200 Subject: [PATCH 02/43] Remove TEAM_HELP_SERVER English --- VelocityCore/src/de/steamwar/messages/BungeeCore.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index 6afbaf7e..798a2932 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -430,7 +430,6 @@ TEAM_HELP_NAME = §8/§7team changename §8- §7Change your team name. TEAM_HELP_COLOR = §8/§7team changecolor §8- §7Change your team color. TEAM_HELP_LEADER = §8/§7team promote §8- §7Promote someone to leader. TEAM_HELP_STEP_BACK = §8/§7team stepback §8- §7Demote yourself from leader. -TEAM_HELP_SERVER = §8/§7team server §8[§eIP/address§8] §8(§7port§8) §8- §7Set the address for your teamserver. #Team Create TEAM_CREATE_USAGE = §8/§7team create §8[§eteam shortcut§8] §8[§eteam name§8] From 1d43021853e7ed3d3c31d14c710643326f459e94 Mon Sep 17 00:00:00 2001 From: killerkuh <54+12312@noreply.localhost> Date: Thu, 24 Sep 2026 21:47:22 +0200 Subject: [PATCH 03/43] Remove TEAM_HELP_SERVER Deutsch --- VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 5921259a..3504acf2 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -399,7 +399,7 @@ TEAM_HELP_NAME = §8/§7team changename §8- §7Ändere deinen Teamnamen. TEAM_HELP_COLOR = §8/§7team changecolor §8- §7Ändere deine Teamfarbe. TEAM_HELP_LEADER = §8/§7team promote §8- §7Ernenne jemanden zum Teamleader. TEAM_HELP_STEP_BACK = §8/§7team stepback §8- §7Tritt als Leader zurück. -TEAM_HELP_SERVER = §8/§7team server §8[§eIP/Adresse§8] §8(§7Port§8) §8- §7Setzt Adresse des Teamservers. + #Team Create TEAM_CREATE_USAGE = §8/§7team create §8[§eTeamkürzel§8] §8[§eTeamname§8] From 3e3a209c80142b149288cf8f4dadcf7f97fe515b Mon Sep 17 00:00:00 2001 From: killerkuh <54+12312@noreply.localhost> Date: Thu, 24 Sep 2026 21:49:20 +0200 Subject: [PATCH 04/43] Removed Extra Line --- VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 3504acf2..d07b6c7b 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -400,7 +400,6 @@ TEAM_HELP_COLOR = §8/§7team changecolor §8- §7Ändere deine Teamfarbe. TEAM_HELP_LEADER = §8/§7team promote §8- §7Ernenne jemanden zum Teamleader. TEAM_HELP_STEP_BACK = §8/§7team stepback §8- §7Tritt als Leader zurück. - #Team Create TEAM_CREATE_USAGE = §8/§7team create §8[§eTeamkürzel§8] §8[§eTeamname§8] TEAM_CREATE_CREATED = §7Du hast das Team §e{0} §7gegründet! From 2302f9aafb4462a16f16cd15ea0584b2dec4ba9a Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Thu, 15 Oct 2026 18:13:53 +0200 Subject: [PATCH 05/43] Fix SmartPlace reversing debug stick --- .../bausystem/features/smartplace/SmartPlaceListener.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceListener.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceListener.java index f403e265..3554a7e5 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceListener.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceListener.java @@ -25,6 +25,7 @@ import de.steamwar.bausystem.Permission; import de.steamwar.bausystem.configplayer.Config; import de.steamwar.linkage.Linked; import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket; +import net.minecraft.world.InteractionHand; import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; @@ -88,6 +89,8 @@ public class SmartPlaceListener implements Listener { TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, (player, packet) -> { if (!Permission.BUILD.hasPermission(player)) return packet; if (!Config.STORE.get(player).isSmartPlace()) return packet; + ItemStack usedItem = packet.getHand() == InteractionHand.MAIN_HAND ? player.getInventory().getItemInMainHand() : player.getInventory().getItemInOffHand(); + if (usedItem.getType() == Material.DEBUG_STICK) return packet; RayTraceResult rayTraceResult = player.rayTraceBlocks(6); Block block = rayTraceResult != null ? rayTraceResult.getHitBlock() : null; BlockFace blockFace = rayTraceResult != null ? rayTraceResult.getHitBlockFace() : null; From 9fe05b59e43ad9c79d834a91fe3278b6513f697a Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Thu, 15 Oct 2026 19:55:02 +0200 Subject: [PATCH 06/43] Rework chat into Velocity-managed channels Chat is now handled entirely by VelocityCore through channels with separate readers and writers (Global, Local/Fight, Blue/Red fight team, Team, ServerTeam, Alert and 1:1 direct messages). Each player has a saved default channel; messages from other channels are tagged with the channel name, and every default change is announced. - /global, /local, /tc, /stc, /msg and /r switch the default channel when called without a message - /chat switches, /chat join|leave toggles reading (saved); moderators can join arenas by name and write with /chat msg - '+' sends from global to local and from local/fight to the fight team - Referees write yellow in the fight chat - Subservers no longer receive chat; arenas get a record-only ChatRecordPacket so replays keep their chat - Remove the chat listeners of SpigotCore, FightSystem, MissileWars, TowerRun and TNTLeague Closes #460, #129 --- .../packets/server/ChatRecordPacket.java | 33 +- .../fightsystem/FightSystem.properties | 1 - .../steamwar/fightsystem/fight/FightTeam.java | 4 - .../steamwar/fightsystem/listener/Chat.java | 47 +- .../misslewars/listener/ChatListener.java | 58 --- .../SpigotCore_Main/src/SpigotCore.properties | 1 - .../src/SpigotCore_de.properties | 1 - .../steamwar/tntleague/TNTLeague.properties | 3 - .../tntleague/events/GlobalListener.kt | 17 - TowerRun/src/TowerRun.properties | 3 - .../towerrun/listener/GlobalListener.java | 25 -- .../steamwar/messages/BungeeCore.properties | 32 +- .../messages/BungeeCore_de.properties | 32 +- .../src/de/steamwar/messages/Chatter.java | 9 - .../velocitycore/chat/AlertChannel.java | 70 +++ .../velocitycore/chat/ChatChannel.java | 105 +++++ .../velocitycore/chat/ChatFilter.java | 76 ++++ .../velocitycore/chat/ChatFormat.java | 94 ++++ .../velocitycore/chat/ChatManager.java | 400 ++++++++++++++++++ .../velocitycore/chat/DirectChannel.java | 105 +++++ .../velocitycore/chat/FightTeamChannel.java | 101 +++++ .../velocitycore/chat/GlobalChannel.java | 63 +++ .../velocitycore/chat/LocalChannel.java | 117 +++++ .../velocitycore/chat/ServerTeamChannel.java | 76 ++++ .../velocitycore/chat/TeamChannel.java | 79 ++++ .../velocitycore/commands/AlertCommand.java | 3 +- .../velocitycore/commands/ChatCommand.java | 123 ++++++ .../velocitycore/commands/GlobalCommand.java | 46 ++ .../velocitycore/commands/LocalCommand.java | 17 +- .../velocitycore/commands/MsgCommand.java | 21 +- .../velocitycore/commands/RCommand.java | 2 +- .../commands/ServerTeamchatCommand.java | 13 +- .../commands/TeamchatCommand.java | 22 +- .../velocitycore/discord/DiscordBot.java | 5 +- .../discord/channels/DiscordChatRoom.java | 13 +- .../velocitycore/listeners/ChatListener.java | 160 +------ .../network/handlers/FightInfoHandler.java | 2 + 37 files changed, 1607 insertions(+), 372 deletions(-) rename SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/ChattingEvent.java => CommonCore/Network/src/de/steamwar/network/packets/server/ChatRecordPacket.java (53%) delete mode 100644 MissileWars/src/de/steamwar/misslewars/listener/ChatListener.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/ChattingEvent.java b/CommonCore/Network/src/de/steamwar/network/packets/server/ChatRecordPacket.java similarity index 53% rename from SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/ChattingEvent.java rename to CommonCore/Network/src/de/steamwar/network/packets/server/ChatRecordPacket.java index b39a4943..642cf335 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/ChattingEvent.java +++ b/CommonCore/Network/src/de/steamwar/network/packets/server/ChatRecordPacket.java @@ -1,7 +1,7 @@ /* * This file is a part of the SteamWar software. * - * Copyright (C) 2025 SteamWar.de-Serverteam + * Copyright (C) 2026 SteamWar.de-Serverteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,21 +17,24 @@ * along with this program. If not, see . */ -package de.steamwar.core.events; -import de.steamwar.core.Core; -import de.steamwar.linkage.Linked; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.player.AsyncPlayerChatEvent; +package de.steamwar.network.packets.server; -@Linked -public class ChattingEvent implements Listener { +import de.steamwar.network.packets.NetworkPacket; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - private void onChat(AsyncPlayerChatEvent event) { - event.setCancelled(true); - Core.MESSAGE.broadcastPrefixless("LOCAL_CHAT", event.getPlayer().getDisplayName(), event.getMessage()); - } +/** + * A chat message written on an arena. The arena only records it for the replay and never displays it. + */ +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Getter +public class ChatRecordPacket extends NetworkPacket { + private static final long serialVersionUID = -4718360932583127458L; + private int playerId; + private String message; } diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.properties b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.properties index c546644f..1354f2e1 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.properties +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.properties @@ -109,7 +109,6 @@ TEAM_READY = §aTeam ready TEAM_NOT_READY = §c§mTeam ready SKIP_READY = §aSkipping to next event SKIP_NOT_READY = §c§mSkipping to next event -TEAM_CHAT = {0}{1}§8» {0}{2} CHOOSE_KIT = §eChoose kit RESPAWN = §eRespawn MANAGE_PLAYERS = §cManage players diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightTeam.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightTeam.java index 0119bf40..b7820066 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightTeam.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightTeam.java @@ -256,10 +256,6 @@ public class FightTeam { broadcast(player -> FightSystem.getMessage().send(message, player, params)); } - public void broadcastChat(Player sender, String message) { - broadcast(player -> FightSystem.getMessage().sendPrefixless("TEAM_CHAT", player, ChatMessageType.CHAT, prefix, sender.getName(), message)); - } - private void broadcast(Consumer f) { players.forEach((uuid, fightPlayer) -> { Player player = Bukkit.getPlayer(uuid); diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/listener/Chat.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/listener/Chat.java index 617684d8..d7d19338 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/listener/Chat.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/listener/Chat.java @@ -19,50 +19,41 @@ package de.steamwar.fightsystem.listener; -import de.steamwar.fightsystem.ArenaMode; import de.steamwar.fightsystem.Config; import de.steamwar.fightsystem.FightSystem; import de.steamwar.fightsystem.fight.Fight; import de.steamwar.fightsystem.fight.FightTeam; import de.steamwar.fightsystem.record.GlobalRecorder; -import de.steamwar.fightsystem.states.FightState; -import de.steamwar.fightsystem.states.StateDependentListener; import de.steamwar.linkage.Linked; +import de.steamwar.network.packets.PacketHandler; +import de.steamwar.network.packets.server.ChatRecordPacket; +import de.steamwar.sql.SteamwarUser; +import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.AsyncPlayerChatEvent; import java.util.logging.Level; +/** + * Chat is handled by Velocity. Messages written on this server are only sent here to be recorded for the replay. + */ @Linked -public class Chat implements Listener { +public class Chat extends PacketHandler { - public Chat() { - new StateDependentListener(ArenaMode.All, FightState.All, this); - } + @Handler + public void handleChatRecord(ChatRecordPacket packet) { + SteamwarUser user = SteamwarUser.byId(packet.getPlayerId()); + String message = packet.getMessage(); + FightSystem.getPlugin().getLogger().log(Level.INFO, user.getUserName() + "» " + message); - @EventHandler - public void handlePlayerChat(AsyncPlayerChatEvent event) { - Player player = event.getPlayer(); - String message = event.getMessage(); - - FightSystem.getPlugin().getLogger().log(Level.INFO, player.getName() + "» " + message); - FightTeam fightTeam = Fight.getPlayerTeam(player); + Player player = Bukkit.getPlayer(user.getUUID()); + FightTeam fightTeam = player != null ? Fight.getPlayerTeam(player) : null; if (fightTeam != null) { - String teamName = fightTeam.getColoredName(); - if (message.startsWith(Config.GameModeConfig.TeamChatPrefix)) { - fightTeam.broadcastChat(player, message.substring(1)); - } else { - broadcastChat("PARTICIPANT_CHAT", teamName, player.getName(), message); - } - } else if (Config.isReferee(player)) { - broadcastChat("FIGHTLEADER_CHAT", player.getName(), message); + GlobalRecorder.getInstance().chat("PARTICIPANT_CHAT", fightTeam.getColoredName(), user.getUserName(), message); + } else if (player != null && Config.isReferee(player)) { + GlobalRecorder.getInstance().chat("FIGHTLEADER_CHAT", user.getUserName(), message); } else { - broadcastChat("SPECTATOR_CHAT", player.getName(), message); + GlobalRecorder.getInstance().chat("SPECTATOR_CHAT", user.getUserName(), message); } - - event.setCancelled(true); } public static void broadcastChat(String message, Object... params) { diff --git a/MissileWars/src/de/steamwar/misslewars/listener/ChatListener.java b/MissileWars/src/de/steamwar/misslewars/listener/ChatListener.java deleted file mode 100644 index cf7a991c..00000000 --- a/MissileWars/src/de/steamwar/misslewars/listener/ChatListener.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * This file is a part of the SteamWar software. - * - * Copyright (C) 2025 SteamWar.de-Serverteam - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package de.steamwar.misslewars.listener; - -import de.steamwar.linkage.Linked; -import de.steamwar.misslewars.FightState; -import de.steamwar.misslewars.MWTeam; -import de.steamwar.misslewars.MissileWars; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.player.AsyncPlayerChatEvent; - -import java.util.EnumSet; - -@Linked -public class ChatListener extends BasicListener { - - public ChatListener() { - super(EnumSet.allOf(FightState.class)); - } - - @EventHandler - public void onChat(AsyncPlayerChatEvent e) { - e.setCancelled(true); - - Player p = e.getPlayer(); - MWTeam team = MissileWars.getTeam(p); - String message = e.getPlayer().getDisplayName() + "§8» §7" + e.getMessage(); - if (team == null) { - Bukkit.broadcastMessage(message); - } else { - if (e.getMessage().startsWith("+")) { - String teamMessage = "§8[" + team.getColorCode() + "TEAM§8] " + e.getPlayer().getDisplayName() + "§8» §7" + e.getMessage().substring(1); - team.getPlayers().forEach(player -> player.sendMessage(teamMessage)); - } else { - Bukkit.broadcastMessage(message); - } - } - } -} diff --git a/SpigotCore/SpigotCore_Main/src/SpigotCore.properties b/SpigotCore/SpigotCore_Main/src/SpigotCore.properties index e7e13eeb..56622660 100644 --- a/SpigotCore/SpigotCore_Main/src/SpigotCore.properties +++ b/SpigotCore/SpigotCore_Main/src/SpigotCore.properties @@ -16,7 +16,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # -LOCAL_CHAT = §eLocal §r{0}§8»§7 {1} COMMAND_SYSTEM_ERROR = §cError executing the command! diff --git a/SpigotCore/SpigotCore_Main/src/SpigotCore_de.properties b/SpigotCore/SpigotCore_Main/src/SpigotCore_de.properties index b7cba3c4..c1cf8e75 100644 --- a/SpigotCore/SpigotCore_Main/src/SpigotCore_de.properties +++ b/SpigotCore/SpigotCore_Main/src/SpigotCore_de.properties @@ -16,7 +16,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # -LOCAL_CHAT = §eLokal §r{0}§8»§7 {1} COMMAND_SYSTEM_ERROR = §cFehler beim Ausführen des Befehls! diff --git a/TNTLeague/src/de/steamwar/tntleague/TNTLeague.properties b/TNTLeague/src/de/steamwar/tntleague/TNTLeague.properties index 311e5263..fc06646a 100644 --- a/TNTLeague/src/de/steamwar/tntleague/TNTLeague.properties +++ b/TNTLeague/src/de/steamwar/tntleague/TNTLeague.properties @@ -53,6 +53,3 @@ DEALER_REDSTONE = §eRedstone DEALER_BLOCKS = §eBlocks DEALER_TOOLS = §eTools DEALER_ANGLES = §eAngle Blocks - -PARTICIPANT_CHAT = {0} {1}§8» §7{2} -SPECTATOR_CHAT = §7{0}§8» §7{1} \ No newline at end of file diff --git a/TNTLeague/src/de/steamwar/tntleague/events/GlobalListener.kt b/TNTLeague/src/de/steamwar/tntleague/events/GlobalListener.kt index b5f361ac..c0353812 100644 --- a/TNTLeague/src/de/steamwar/tntleague/events/GlobalListener.kt +++ b/TNTLeague/src/de/steamwar/tntleague/events/GlobalListener.kt @@ -22,7 +22,6 @@ package de.steamwar.tntleague.events import de.steamwar.tntleague.config.TNTLeagueWorldConfig import de.steamwar.tntleague.game.TNTLeagueGame import de.steamwar.tntleague.game.TNTLeagueTeam -import de.steamwar.tntleague.message import de.steamwar.tntleague.plugin import org.bukkit.GameMode import org.bukkit.Material @@ -90,20 +89,4 @@ object GlobalListener : Listener { null -> e.respawnLocation = TNTLeagueWorldConfig.lobby } } - - - @EventHandler - fun handlePlayerChat(event: AsyncPlayerChatEvent) { - val player = event.player - val msg = event.message - - val fightTeam = TNTLeagueGame.getTeam(player) - if (fightTeam != null) { - message.broadcastPrefixless("PARTICIPANT_CHAT", fightTeam.name, player.name, msg) - } else { - message.broadcastPrefixless("SPECTATOR_CHAT", player.name, msg) - } - - event.isCancelled = true - } } \ No newline at end of file diff --git a/TowerRun/src/TowerRun.properties b/TowerRun/src/TowerRun.properties index f008ae6c..55fe89eb 100644 --- a/TowerRun/src/TowerRun.properties +++ b/TowerRun/src/TowerRun.properties @@ -34,6 +34,3 @@ GAME_TIME = §a{0}:{1} CATCH_UP_WARNING = §4!! §cYou should catch up §4!! COMMAND_START = §aThe game will start soon§8! - -PARTICIPANT_CHAT = {0} {1}§8» §7{2} -SPECTATOR_CHAT = §7{0}§8» §7{1} \ No newline at end of file diff --git a/TowerRun/src/de/steamwar/towerrun/listener/GlobalListener.java b/TowerRun/src/de/steamwar/towerrun/listener/GlobalListener.java index 96248e40..324fddca 100644 --- a/TowerRun/src/de/steamwar/towerrun/listener/GlobalListener.java +++ b/TowerRun/src/de/steamwar/towerrun/listener/GlobalListener.java @@ -20,19 +20,15 @@ package de.steamwar.towerrun.listener; import de.steamwar.linkage.Linked; -import de.steamwar.towerrun.TowerRun; import de.steamwar.towerrun.config.WorldConfig; import de.steamwar.towerrun.game.TowerRunGame; import de.steamwar.towerrun.game.TowerRunPlayer; -import de.steamwar.towerrun.state.GameState; import de.steamwar.towerrun.state.GameStateBukkitListener; import de.steamwar.towerrun.state.GameStates; import org.bukkit.Bukkit; -import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerRespawnEvent; @@ -71,25 +67,4 @@ public class GlobalListener extends GameStateBukkitListener implements Listener public void onPlayerRespawn(PlayerRespawnEvent event) { event.setRespawnLocation(WorldConfig.SPAWN); } - - @EventHandler - public void onAsyncPlayerChat(AsyncPlayerChatEvent event) { - TowerRunPlayer player = TowerRunPlayer.get(event.getPlayer()); - if (GameState.getCurrentState() == GameStates.RUNNING && (TowerRunGame.isAlive(player) || TowerRunGame.isEscaped(player))) { - String prefix; - if (TowerRunGame.isAlive(player)) { - if (event.getPlayer().getGameMode() == GameMode.SPECTATOR) { - prefix = "§c☠"; - } else { - prefix = "§6❤"; - } - } else { - prefix = "§a✔"; - } - TowerRun.getMessage().broadcastPrefixless("PARTICIPANT_CHAT", prefix, event.getPlayer().getName(), event.getMessage()); - } else { - TowerRun.getMessage().broadcastPrefixless("SPECTATOR_CHAT", event.getPlayer().getName(), event.getMessage()); - } - event.setCancelled(true); - } } diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index 6afbaf7e..bff90140 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -574,12 +574,36 @@ CHAT_BC_USAGE = §8/§7bc §8[§emessage§8] CHAT_NO_RECEIVER = §cNobody receives your message CHAT_EMPTY = §cDon\'t write meaningless empty messages. -CHAT_SERVERTEAM = §8STC §e{0}§8» §f{2} -CHAT_DISCORD_SERVERTEAM = §8STC §e{0}§8» §f{2} +CHAT_SERVERTEAM = §e{0}§8» §f{2} CHAT_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_DISCORD_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_TEAM = §8TC §e{0}§8» §f{2} +CHAT_LOCAL = {3}{4}{5}{6}{0}§8» {7}{2} +CHAT_TEAM = §e{0}§8» §f{2} CHAT_MSG = §e{0}§8»§e{1} §7{2} +CHAT_FIGHT_PARTICIPANT = {0} {1}§8» §7{2} +CHAT_FIGHT_SPECTATOR = §7{0}§8» §7{1} +CHAT_FIGHT_REFEREE = §e{0}§8» §e{1} +CHAT_FIGHT_TEAM = {0} {1}§8» §f{2} +CHAT_TAGGED = §8[{0}§8] §r{1} +CHAT_CHANNEL_GLOBAL = §eGlobal +CHAT_CHANNEL_ARENA = §e{0} +CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} +CHAT_CHANNEL_TEAM = §eTC +CHAT_CHANNEL_SERVERTEAM = §eSTC +CHAT_CHANNEL_MSG = §eMSG §7{0} +CHAT_CHANNEL_ALERT = §cAlert +CHAT_CHANNEL_LOCAL = §eLocal +CHAT_CHANNEL_CURRENT = §7You are writing into §e{0}§7. +CHAT_CHANNEL_SWITCHED = §7You are now writing into §e{0}§7. +CHAT_CHANNEL_CLOSED = §e{0} §7was closed. +CHAT_CHANNEL_NOT_SWITCHABLE = §cYou cannot write into this channel by default. +CHAT_NO_WRITE = §cYou cannot write into this channel. +CHAT_FIGHT_NO_TEAM = §cYou are not in a fight team. +GLOBAL_USAGE = §8/§7global §8[§emessage§8] +CHAT_USAGE = §8/§7chat §8[§echannel§8] [§ejoin§8|§eleave§8|§emsg §8<§etext§8>] +CHAT_CHANNEL_UNKNOWN = §cUnknown chat channel. +CHAT_CHANNEL_JOINED = §7You are reading §e{0} §7again. +CHAT_CHANNEL_LEFT = §7You no longer read §e{0}§7. +CHAT_CHANNEL_LEAVE_DEFAULT = §cYou cannot leave the channel you are writing into. Switch to another channel first. #CheckListner CHECK_UNCHECKED = §7You still have §e{0} §7unchecked schematic§8(§7s§8)! diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 5921259a..3f34d888 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -539,12 +539,36 @@ CHAT_BC_USAGE = §8/§7bc §8[§eNachricht§8] CHAT_NO_RECEIVER = §cNiemand empfängt deine Nachricht CHAT_EMPTY = §cSchreibe keine inhaltslosen Nachrichten. -CHAT_SERVERTEAM = §8STC §e{0}§8» §f{2} -CHAT_DISCORD_SERVERTEAM = §8STC §e{0}§8» §f{2} +CHAT_SERVERTEAM = §e{0}§8» §f{2} CHAT_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_DISCORD_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_TEAM = §8TC §e{0}§8» §f{2} +CHAT_LOCAL = {3}{4}{5}{6}{0}§8» {7}{2} +CHAT_TEAM = §e{0}§8» §f{2} CHAT_MSG = §e{0}§8»§e{1} §7{2} +CHAT_FIGHT_PARTICIPANT = {0} {1}§8» §7{2} +CHAT_FIGHT_SPECTATOR = §7{0}§8» §7{1} +CHAT_FIGHT_REFEREE = §e{0}§8» §e{1} +CHAT_FIGHT_TEAM = {0} {1}§8» §f{2} +CHAT_TAGGED = §8[{0}§8] §r{1} +CHAT_CHANNEL_GLOBAL = §eGlobal +CHAT_CHANNEL_ARENA = §e{0} +CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} +CHAT_CHANNEL_TEAM = §eTC +CHAT_CHANNEL_SERVERTEAM = §eSTC +CHAT_CHANNEL_MSG = §eMSG §7{0} +CHAT_CHANNEL_ALERT = §cAlert +CHAT_CHANNEL_LOCAL = §eLokal +CHAT_CHANNEL_CURRENT = §7Du schreibst in §e{0}§7. +CHAT_CHANNEL_SWITCHED = §7Du schreibst jetzt in §e{0}§7. +CHAT_CHANNEL_CLOSED = §e{0} §7wurde geschlossen. +CHAT_CHANNEL_NOT_SWITCHABLE = §cIn diesen Kanal kannst du nicht standardmäßig schreiben. +CHAT_NO_WRITE = §cDu kannst nicht in diesen Kanal schreiben. +CHAT_FIGHT_NO_TEAM = §cDu bist in keinem Kampfteam. +GLOBAL_USAGE = §8/§7global §8[§eNachricht§8] +CHAT_USAGE = §8/§7chat §8[§eKanal§8] [§ejoin§8|§eleave§8|§emsg §8<§eText§8>] +CHAT_CHANNEL_UNKNOWN = §cUnbekannter Chatkanal. +CHAT_CHANNEL_JOINED = §7Du liest §e{0} §7wieder mit. +CHAT_CHANNEL_LEFT = §7Du liest §e{0} §7nicht mehr mit. +CHAT_CHANNEL_LEAVE_DEFAULT = §cDu kannst den Kanal, in den du schreibst, nicht verlassen. Wechsle zuerst in einen anderen Kanal. #CheckListner CHECK_UNCHECKED = §7Du hast noch §e{0} §7ungeprüfte Schematic§8(§7s§8)! diff --git a/VelocityCore/src/de/steamwar/messages/Chatter.java b/VelocityCore/src/de/steamwar/messages/Chatter.java index 5cc74f23..fc34a4f7 100644 --- a/VelocityCore/src/de/steamwar/messages/Chatter.java +++ b/VelocityCore/src/de/steamwar/messages/Chatter.java @@ -23,8 +23,6 @@ import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.event.ResultedEvent; import com.velocitypowered.api.event.connection.LoginEvent; import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.ServerConnection; -import de.steamwar.persistent.Subserver; import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.UserPerm; import de.steamwar.velocitycore.VelocityCore; @@ -61,13 +59,6 @@ public interface Chatter { return new ChatterGroup(allStream()); } - static ChatterGroup globalChat() { - return new ChatterGroup(Stream.concat(Stream.of(Chatter.console()), allPlayers().filter(player -> { - Subserver subserver = Subserver.getSubserver(player); - return !(Subserver.isArena(subserver) && subserver.getServer() == player.getCurrentServer().map(ServerConnection::getServerInfo).orElse(null)); - }).map(Chatter::of))); - } - static ChatterGroup serverteam() { return new ChatterGroup(allStream() .filter(player -> player.user().hasPerm(UserPerm.TEAM)) diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java new file mode 100644 index 00000000..2c194f88 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java @@ -0,0 +1,70 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.UserPerm; + +import java.util.Arrays; +import java.util.stream.Stream; + +public class AlertChannel extends ChatChannel { + + @Override + public boolean switchable() { + return false; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_ALERT"); + } + + @Override + public Stream readers() { + return Arrays.stream(Chatter.broadcast().getChatters()); + } + + @Override + public boolean canWrite(Chatter sender) { + if (!sender.user().hasPerm(UserPerm.MODERATION)) { + sender.system("CHAT_NO_WRITE"); + return false; + } + return true; + } + + @Override + protected Message format(Chatter receiver, Chatter sender, String message) { + return new Message("ALERT", message.replace('&', '§')); + } + + /** + * Alerts are system messages: they are shown even with hidden chat and bypass the chat filter. + */ + @Override + public void send(Chatter sender, String message, boolean mirror) { + if (!canWrite(sender)) return; + + readers().forEach(reader -> reader.send(true, null, null, format(reader, sender, message))); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java new file mode 100644 index 00000000..8c3be5f9 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java @@ -0,0 +1,105 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Stream; + +public abstract class ChatChannel { + + /** + * @return the id under which this channel is stored as default channel of a player, null if it cannot be stored + */ + public String persistId() { + return null; + } + + public boolean switchable() { + return true; + } + + /** + * @return the channel name shown in front of messages of this channel if it is not the default channel of the receiver + */ + public abstract Message tag(Chatter receiver); + + public abstract Stream readers(); + + /** + * Checks if the sender is allowed to write into this channel and informs the sender otherwise. + */ + public abstract boolean canWrite(Chatter sender); + + protected abstract Message format(Chatter receiver, Chatter sender, String message); + + /** + * Called after a message was delivered to all readers. + * + * @param mirror false if the message originates from the mirror of this channel and should not be sent back + * @return true if the message was delivered to a receiver outside the game + */ + protected boolean delivered(Chatter sender, String message, boolean mirror) { + return false; + } + + public void audit(SteamwarUser sender, String message) { + AuditLog.createChat(AuditLog.SERVER_NAME_VELOCITY, null, sender, message); + } + + public void send(Chatter sender, String message) { + send(sender, message, true); + } + + public void send(Chatter sender, String message, boolean mirror) { + if (!canWrite(sender)) return; + + SteamwarUser user = sender.user(); + String coloredMessage = user.hasPerm(UserPerm.COLOR_CHAT) ? message.replace('&', '§') : message; + if (ChatFilter.filtered(sender, coloredMessage)) return; + + Map receivers = new LinkedHashMap<>(); + readers().forEach(reader -> receivers.putIfAbsent(reader.user().getId(), reader)); + receivers.putIfAbsent(user.getId(), sender); + + boolean noReceiver = true; + for (Chatter receiver : receivers.values()) { + if (!receiver.chatShown()) continue; + + Message msg = format(receiver, sender, coloredMessage); + if (ChatManager.getDefault(receiver) != this) { + msg = new Message("CHAT_TAGGED", tag(receiver), msg); + } + receiver.send(false, null, null, msg); + + if (receiver.user().getId() != user.getId()) noReceiver = false; + } + + if (!delivered(sender, coloredMessage, mirror) && noReceiver) { + sender.system("CHAT_NO_RECEIVER"); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java new file mode 100644 index 00000000..efe8ebb2 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java @@ -0,0 +1,76 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.Punishment; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.commands.PunishmentCommand; + +import java.util.concurrent.TimeUnit; + +public class ChatFilter { + + private ChatFilter() {} + + public static boolean filtered(Chatter sender, String message) { + if (!sender.chatShown()) { + sender.system("CHAT_RECEIVE"); + return true; + } + + if (message.replace("§[a-f0-9klmno]", "").trim().isEmpty()) { + sender.system("CHAT_EMPTY"); + return true; + } + + SteamwarUser user = sender.user(); + if (!user.hasPerm(UserPerm.TEAM) && (message.contains("http:") || message.contains("https:") || message.contains("www."))) { + sender.system("CHAT_NO_LINKS"); + return true; + } + + if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.Mute)) return true; + + if (message.contains("LIXFEL")) { + specialAlert(sender, "Lixfel", "CHAT_LIXFEL_", 3, 6, 11, 12, 15); + } + if (message.contains("YOYONOW")) { + specialAlert(sender, "YoyoNow", "CHAT_YOYONOW_", 3, 6, 11, 12); + } + if (message.contains("CHAOSCAOT")) { + specialAlert(sender, "Chaoscaot", "CHAT_CHAOSCAOT_", 3, 6, 11, 12, 15, 17); + } + + return false; + } + + private static void specialAlert(Chatter sender, String name, String baseMessage, int... delay) { + sender.system("CHAT_LIXFEL_ACTION_BAR"); + for (int i = 0; i < delay.length; i++) { + int finalI = i; + VelocityCore.schedule(() -> sender.prefixless("CHAT_MSG", name, sender.user(), new Message(baseMessage + (finalI + 1)))).delay(delay[i], TimeUnit.SECONDS).schedule(); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java new file mode 100644 index 00000000..28a114f2 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java @@ -0,0 +1,94 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.network.packets.server.PingPacket; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.Team; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.network.NetworkSender; +import de.steamwar.velocitycore.util.SteamwarPrefix; + +import java.util.Arrays; +import java.util.stream.Collectors; + +public class ChatFormat { + + private ChatFormat() {} + + /** + * Formats a message with the rank and team prefixes of the sender. + */ + public static Message player(String format, Chatter receiver, Chatter sender, Chatter msgReceiver, String message) { + SteamwarUser user = sender.user(); + UserPerm.Prefix prefix = user.prefix(); + + String teamPrefix = ""; + if (user.hasPerm(UserPerm.TEAM) && useSwPrefix(user)) { + teamPrefix = "§eS§8W "; + } else if (user.getTeam() != 0) { + Team team = Team.byId(user.getTeam()); + teamPrefix = "§" + team.getTeamColor() + team.getTeamKuerzel() + " "; + } + + String chatColorCode = user.hasPerm(UserPerm.TEAM) ? "§f" : "§7"; + return new Message(format, + user, + msgReceiver == null ? receiver : msgReceiver, + highlightMentions(message, chatColorCode, receiver), + teamPrefix, + "", + prefix == UserPerm.emptyPrefix ? "§f" : prefix.getColorCode(), + prefix.getChatPrefix().length() == 0 ? "§f" : prefix.getChatPrefix() + " ", + chatColorCode); + } + + private static boolean useSwPrefix(SteamwarUser user) { + if (!user.hasPerm(UserPerm.TEAM)) { + return false; + } + + UserPerm.Prefix prefix = user.prefix(); + + boolean hasTeamPrefix = user.getTeam() != 0; + boolean hasSwPrefix = prefix != UserPerm.emptyPrefix; + + if (!hasSwPrefix) return false; + if (!hasTeamPrefix) return true; + + return SteamwarPrefix.usesSWPrefix(user); + } + + public static String highlightMentions(String message, String returnColor, Chatter player) { + if (!message.contains("@")) return message; + + String mark = "@" + player.user().getUserName(); + return Arrays.stream(message.split(" ")).map(cur -> { + if (cur.equalsIgnoreCase(mark) && player.getPlayer() != null) { + NetworkSender.send(player.getPlayer(), new PingPacket(player.user().getId())); + return "§e" + cur + returnColor; + } + return cur; + }).collect(Collectors.joining(" ")); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java new file mode 100644 index 00000000..a437532a --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -0,0 +1,400 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.player.ServerPostConnectEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.linkage.Linked; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.persistent.Subserver; +import de.steamwar.sql.Event; +import de.steamwar.sql.Referee; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserConfig; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.EventStarter; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.listeners.BasicListener; +import lombok.Getter; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +/** + * Keeps track of all chat channels and the default channel every player writes into. + */ +@Linked +public class ChatManager extends BasicListener { + + private static final String CONFIG_KEY = "chat.channel"; + private static final String HIDDEN_KEY = "chat.hidden"; + private static final long REFEREE_CACHE_MILLIS = 60_000; + + @Getter + private static final GlobalChannel global = new GlobalChannel(); + @Getter + private static final ServerTeamChannel serverTeam = new ServerTeamChannel(); + @Getter + private static final AlertChannel alert = new AlertChannel(); + + private static final Map locals = new ConcurrentHashMap<>(); + private static final Map fightTeams = new ConcurrentHashMap<>(); + private static final Map fightInfos = new ConcurrentHashMap<>(); + private static final Map teams = new ConcurrentHashMap<>(); + private static final Set directs = ConcurrentHashMap.newKeySet(); + + private static final Map defaults = new ConcurrentHashMap<>(); + private static final Map persisted = new ConcurrentHashMap<>(); + private static final Set globalOnArena = ConcurrentHashMap.newKeySet(); + private static final Set announced = ConcurrentHashMap.newKeySet(); + private static final Map> spies = new ConcurrentHashMap<>(); + private static final Map> hidden = new ConcurrentHashMap<>(); + + private static int refereeEvent = -1; + private static long refereesLoaded = 0; + private static Set referees = Collections.emptySet(); + + public static LocalChannel local(RegisteredServer server) { + locals.keySet().removeIf(ChatManager::unregistered); + return locals.computeIfAbsent(server, LocalChannel::new); + } + + public static FightTeamChannel fightTeam(RegisteredServer server, boolean blue) { + fightTeams.keySet().removeIf(ChatManager::unregistered); + return fightTeams.computeIfAbsent(server, s -> new FightTeamChannel[]{new FightTeamChannel(s, true), new FightTeamChannel(s, false)})[blue ? 0 : 1]; + } + + public static TeamChannel team(int team) { + return teams.computeIfAbsent(team, TeamChannel::new); + } + + public static DirectChannel direct(UUID first, UUID second) { + for (DirectChannel channel : directs) { + if (channel.contains(first) && channel.partner(first).equals(second)) { + return channel; + } + } + + DirectChannel channel = new DirectChannel(first, second); + directs.add(channel); + return channel; + } + + private static boolean unregistered(RegisteredServer server) { + return VelocityCore.getProxy().getServer(server.getServerInfo().getName()).orElse(null) != server; + } + + public static boolean isArena(RegisteredServer server) { + return Subserver.isArena(Subserver.getSubserver(server.getServerInfo())); + } + + private static Optional currentServer(Player player) { + return player.getCurrentServer().map(ServerConnection::getServer); + } + + static boolean readsGlobal(Player player) { + return !currentServer(player).map(ChatManager::isArena).orElse(false) || globalOnArena.contains(player.getUniqueId()); + } + + static Stream spies(RegisteredServer server) { + return spies.entrySet().stream() + .filter(entry -> entry.getValue().contains(server)) + .map(entry -> VelocityCore.getProxy().getPlayer(entry.getKey()).orElse(null)) + .filter(Objects::nonNull) + .map(Chatter::of) + .filter(chatter -> chatter.user().hasPerm(UserPerm.MODERATION)) + .map(Chatter.class::cast); + } + + /** + * @return true if the user is a referee of the event fight running on the server + */ + static boolean isReferee(RegisteredServer server, int userId) { + Subserver subserver = Subserver.getSubserver(server.getServerInfo()); + if (subserver == null || !EventStarter.getEventServer().containsValue(subserver)) return false; + + Event event = Event.get(); + return event != null && referees(event.getEventID()).contains(userId); + } + + private static synchronized Set referees(int eventId) { + if (eventId != refereeEvent || System.currentTimeMillis() - refereesLoaded > REFEREE_CACHE_MILLIS) { + referees = Referee.get(eventId); + refereeEvent = eventId; + refereesLoaded = System.currentTimeMillis(); + } + return referees; + } + + private static Set hidden(UUID uuid) { + return hidden.computeIfAbsent(uuid, u -> { + Set channels = ConcurrentHashMap.newKeySet(); + String config = UserConfig.getConfig(u, HIDDEN_KEY); + if (config != null && !config.isEmpty()) { + channels.addAll(Arrays.asList(config.split(","))); + } + return channels; + }); + } + + /** + * @return false if the player left the channel with the given persist id as reader + */ + static boolean reads(Chatter chatter, String persistId) { + Player player = chatter.getPlayer(); + return player == null || !hidden(player.getUniqueId()).contains(persistId); + } + + private static void saveHidden(UUID uuid) { + Set channels = hidden(uuid); + UserConfig.updatePlayerConfig(uuid, HIDDEN_KEY, channels.isEmpty() ? null : String.join(",", channels)); + } + + /** + * @return true if the channel is the fight chat of an arena the player is not on, which moderators can read without joining the arena + */ + private static boolean isForeignArena(PlayerChatter player, ChatChannel channel) { + return channel instanceof LocalChannel local && local.isArena() && !local.getServer().getPlayersConnected().contains(player.getPlayer()); + } + + public static void join(PlayerChatter player, ChatChannel channel) { + UUID uuid = player.user().getUUID(); + if (isForeignArena(player, channel)) { + spies.computeIfAbsent(uuid, u -> ConcurrentHashMap.newKeySet()).add(((LocalChannel) channel).getServer()); + player.system("CHAT_CHANNEL_JOINED", channel.tag(player)); + return; + } + + if (hidden(uuid).remove(channel.persistId())) { + saveHidden(uuid); + } + player.system("CHAT_CHANNEL_JOINED", channel.tag(player)); + } + + public static void leave(PlayerChatter player, ChatChannel channel) { + UUID uuid = player.user().getUUID(); + if (isForeignArena(player, channel)) { + Set servers = spies.get(uuid); + if (servers != null) servers.remove(((LocalChannel) channel).getServer()); + player.system("CHAT_CHANNEL_LEFT", channel.tag(player)); + return; + } + + String id = channel.persistId(); + if (id.equals(getDefault(player).persistId())) { + player.system("CHAT_CHANNEL_LEAVE_DEFAULT"); + return; + } + + if (hidden(uuid).add(id)) { + saveHidden(uuid); + } + player.system("CHAT_CHANNEL_LEFT", channel.tag(player)); + } + + public static FightInfoPacket getFightInfo(RegisteredServer server) { + return fightInfos.get(server); + } + + /** + * Updates the fight teams of an arena. Players who just joined a fight team write into the fight chat by default. + */ + public static void updateFightInfo(RegisteredServer server, FightInfoPacket packet) { + fightInfos.keySet().removeIf(s -> s.getPlayersConnected().isEmpty()); + FightInfoPacket previous = fightInfos.put(server, packet); + + Set joined = new HashSet<>(packet.getBluePlayers()); + joined.addAll(packet.getRedPlayers()); + if (previous != null) { + previous.getBluePlayers().forEach(joined::remove); + previous.getRedPlayers().forEach(joined::remove); + } + + for (Player player : server.getPlayersConnected()) { + if (joined.contains(SteamwarUser.get(player.getUniqueId()).getId())) { + setDefault(Chatter.of(player), local(server)); + } + } + } + + /** + * @return the channel the receiver writes into by default; non-player chatters (console, Discord) use the global chat + */ + public static ChatChannel getDefault(Chatter chatter) { + Player player = chatter.getPlayer(); + if (player == null) return global; + + return defaults.computeIfAbsent(player.getUniqueId(), uuid -> initialDefault(player, currentServer(player).orElse(null))); + } + + private static ChatChannel initialDefault(Player player, RegisteredServer server) { + if (server != null && isArena(server)) return local(server); + return resolvePersisted(player, server); + } + + private static ChatChannel resolvePersisted(Player player, RegisteredServer server) { + String id = persisted.computeIfAbsent(player.getUniqueId(), uuid -> Optional.ofNullable(UserConfig.getConfig(uuid, CONFIG_KEY)).orElse(global.persistId())); + SteamwarUser user = SteamwarUser.get(player.getUniqueId()); + return switch (id) { + case "local" -> server != null && !isArena(server) ? local(server) : global; + case "team" -> user.getTeam() != 0 ? team(user.getTeam()) : global; + case "serverteam" -> user.hasPerm(UserPerm.TEAM) ? serverTeam : global; + default -> global; + }; + } + + /** + * Makes the channel the default channel of the player because the player chose it. + */ + public static void switchTo(PlayerChatter player, ChatChannel channel) { + if (!channel.switchable() || isForeignArena(player, channel)) { + player.system("CHAT_CHANNEL_NOT_SWITCHABLE"); + return; + } + if (!channel.canWrite(player)) return; + + UUID uuid = player.user().getUUID(); + if (channel == global && currentServer(player.getPlayer()).map(ChatManager::isArena).orElse(false)) { + globalOnArena.add(uuid); + } else if (channel != global) { + globalOnArena.remove(uuid); + } + + String id = channel.persistId(); + if (id != null && !id.equals(persisted.get(uuid))) { + persisted.put(uuid, id); + UserConfig.updatePlayerConfig(uuid, CONFIG_KEY, id); + } + if (id != null && hidden(uuid).remove(id)) { + saveHidden(uuid); + } + + defaults.put(uuid, channel); + player.system("CHAT_CHANNEL_SWITCHED", channel.tag(player)); + } + + private static void setDefault(PlayerChatter player, ChatChannel channel) { + UUID uuid = player.user().getUUID(); + if (channel != global) globalOnArena.remove(uuid); + + ChatChannel previous = defaults.put(uuid, channel); + if (announced.add(uuid)) { + player.system("CHAT_CHANNEL_CURRENT", channel.tag(player)); + } else if (previous != channel && !player.parseToPlain(previous.tag(player)).equals(player.parseToPlain(channel.tag(player)))) { + player.system("CHAT_CHANNEL_SWITCHED", channel.tag(player)); + } + } + + /** + * Handles a chat message the player typed without a command. + */ + public static void chat(PlayerChatter sender, String message) { + ChatChannel channel = getDefault(sender); + if (channel instanceof TeamChannel team && team.getTeam() != sender.user().getTeam()) { + channel = resolvePersisted(sender.getPlayer(), currentServer(sender.getPlayer()).orElse(null)); + setDefault(sender, channel); + } + + if (message.startsWith("+")) { + channel = quickChannel(sender, channel); + message = message.substring(1); + } + + channel.send(sender, message); + channel.audit(sender.user(), message); + } + + /** + * The channel a message prefixed with '+' is sent to: the local chat when writing into the global chat, + * the fight team chat when writing into the local/fight chat. + */ + private static ChatChannel quickChannel(PlayerChatter sender, ChatChannel current) { + RegisteredServer server = currentServer(sender.getPlayer()).orElse(null); + if (server == null) return current; + + if (current instanceof LocalChannel) { + FightTeamChannel blue = fightTeam(server, true); + if (blue.isMember(sender)) return blue; + + FightTeamChannel red = fightTeam(server, false); + if (red.isMember(sender)) return red; + + return global; + } + + return local(server); + } + + @Subscribe + public void onServerPostConnect(ServerPostConnectEvent event) { + Player player = event.getPlayer(); + RegisteredServer server = currentServer(player).orElse(null); + if (server == null) return; + + UUID uuid = player.getUniqueId(); + globalOnArena.remove(uuid); + + ChatChannel current = defaults.get(uuid); + ChatChannel next; + if (isArena(server)) { + next = local(server); + } else if (!announced.contains(uuid) || current instanceof LocalChannel || (event.getPreviousServer() != null && isArena(event.getPreviousServer()))) { + next = resolvePersisted(player, server); + } else { + next = current; + } + + setDefault(Chatter.of(player), next); + } + + @Subscribe + public void onDisconnect(DisconnectEvent event) { + UUID uuid = event.getPlayer().getUniqueId(); + defaults.remove(uuid); + persisted.remove(uuid); + globalOnArena.remove(uuid); + announced.remove(uuid); + spies.remove(uuid); + hidden.remove(uuid); + + Iterator it = directs.iterator(); + while (it.hasNext()) { + DirectChannel channel = it.next(); + if (!channel.contains(uuid)) continue; + it.remove(); + + Player partner = VelocityCore.getProxy().getPlayer(channel.partner(uuid)).orElse(null); + if (partner != null && defaults.get(partner.getUniqueId()) == channel) { + PlayerChatter chatter = Chatter.of(partner); + chatter.system("CHAT_CHANNEL_CLOSED", channel.tag(chatter)); + defaults.put(partner.getUniqueId(), resolvePersisted(partner, currentServer(partner).orElse(null))); + chatter.system("CHAT_CHANNEL_SWITCHED", defaults.get(partner.getUniqueId()).tag(chatter)); + } + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java new file mode 100644 index 00000000..b369bafd --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java @@ -0,0 +1,105 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.IgnoreSystem; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.velocitycore.VelocityCore; + +import java.util.UUID; +import java.util.stream.Stream; + +import static de.steamwar.persistent.Storage.lastChats; + +/** + * A temporary channel between two players, closed as soon as one of them goes offline. + */ +public class DirectChannel extends ChatChannel { + + private final UUID first; + private final UUID second; + + DirectChannel(UUID first, UUID second) { + this.first = first; + this.second = second; + } + + public boolean contains(UUID uuid) { + return first.equals(uuid) || second.equals(uuid); + } + + public UUID partner(UUID uuid) { + return first.equals(uuid) ? second : first; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_MSG", SteamwarUser.get(partner(receiver.user().getUUID()))); + } + + @Override + public Stream readers() { + return Stream.of(first, second) + .map(uuid -> VelocityCore.getProxy().getPlayer(uuid).orElse(null)) + .filter(player -> player != null) + .map(Chatter::of); + } + + @Override + public boolean canWrite(Chatter sender) { + UUID partner = partner(sender.user().getUUID()); + if (VelocityCore.getProxy().getPlayer(partner).isEmpty()) { + sender.system("MSG_OFFLINE"); + return false; + } + + if (IgnoreSystem.isIgnored(partner, sender.user().getUUID())) { + sender.system("MSG_IGNORED"); + return false; + } + return true; + } + + @Override + protected Message format(Chatter receiver, Chatter sender, String message) { + return ChatFormat.player("CHAT_MSG", receiver, sender, Chatter.of(partner(sender.user().getUUID())), message); + } + + @Override + protected boolean delivered(Chatter sender, String message, boolean mirror) { + Player senderPlayer = VelocityCore.getProxy().getPlayer(sender.user().getUUID()).orElse(null); + Player partnerPlayer = VelocityCore.getProxy().getPlayer(partner(sender.user().getUUID())).orElse(null); + if (senderPlayer != null && partnerPlayer != null) { + lastChats.put(senderPlayer, partnerPlayer); + lastChats.put(partnerPlayer, senderPlayer); + } + return false; + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createSensitiveCommand(AuditLog.SERVER_NAME_VELOCITY, null, sender, "/msg " + SteamwarUser.get(partner(sender.getUUID())).getUserName() + " " + message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java new file mode 100644 index 00000000..93b80e60 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java @@ -0,0 +1,101 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import lombok.Getter; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +/** + * The team chat of the blue or red team of a fight. + */ +public class FightTeamChannel extends ChatChannel { + + @Getter + private final RegisteredServer server; + private final boolean blue; + + FightTeamChannel(RegisteredServer server, boolean blue) { + this.server = server; + this.blue = blue; + } + + @Override + public boolean switchable() { + return false; + } + + private List members() { + FightInfoPacket info = ChatManager.getFightInfo(server); + if (info == null) return Collections.emptyList(); + return blue ? info.getBluePlayers() : info.getRedPlayers(); + } + + private String teamName() { + FightInfoPacket info = ChatManager.getFightInfo(server); + if (info == null) return ""; + return blue ? info.getBlueName() : info.getRedName(); + } + + public boolean isMember(Chatter chatter) { + return members().contains(chatter.user().getId()); + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_FIGHT_TEAM", server.getServerInfo().getName(), teamName()); + } + + @Override + public Stream readers() { + List members = members(); + return Stream.concat( + server.getPlayersConnected().stream().map(Chatter::of).filter(chatter -> members.contains(chatter.user().getId())), + ChatManager.spies(server)); + } + + @Override + public boolean canWrite(Chatter sender) { + if (!isMember(sender) || sender.getPlayer() == null || !server.getPlayersConnected().contains(sender.getPlayer())) { + sender.system("CHAT_FIGHT_NO_TEAM"); + return false; + } + return true; + } + + @Override + protected Message format(Chatter receiver, Chatter sender, String message) { + return new Message("CHAT_FIGHT_TEAM", teamName(), sender.user(), ChatFormat.highlightMentions(message, "§f", receiver)); + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createChat(server.getServerInfo().getName(), null, sender, "+" + message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java new file mode 100644 index 00000000..a4146c1d --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java @@ -0,0 +1,63 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.velocitycore.discord.DiscordBot; + +import java.util.stream.Stream; + +public class GlobalChannel extends ChatChannel { + + @Override + public String persistId() { + return "global"; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_GLOBAL"); + } + + @Override + public Stream readers() { + return Stream.concat(Stream.of(Chatter.console()), Chatter.allPlayers().filter(ChatManager::readsGlobal).map(Chatter::of).filter(chatter -> ChatManager.reads(chatter, persistId()))); + } + + @Override + public boolean canWrite(Chatter sender) { + return true; + } + + @Override + protected Message format(Chatter receiver, Chatter sender, String message) { + return ChatFormat.player("CHAT_GLOBAL", receiver, sender, null, message); + } + + @Override + protected boolean delivered(Chatter sender, String message, boolean mirror) { + if (mirror) { + DiscordBot.withBot(bot -> bot.getIngameChat().send(false, null, null, format(bot.getIngameChat(), sender, message))); + } + return true; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java new file mode 100644 index 00000000..5eaf90aa --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java @@ -0,0 +1,117 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.network.packets.server.ChatRecordPacket; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.network.NetworkSender; +import lombok.Getter; + +import java.util.stream.Stream; + +/** + * The chat of all players on one server. On arenas this is the fight chat. + */ +public class LocalChannel extends ChatChannel { + + @Getter + private final RegisteredServer server; + + LocalChannel(RegisteredServer server) { + this.server = server; + } + + public boolean isArena() { + return ChatManager.isArena(server); + } + + @Override + public String persistId() { + return "local"; + } + + @Override + public Message tag(Chatter receiver) { + if (isArena()) { + return new Message("CHAT_CHANNEL_ARENA", server.getServerInfo().getName()); + } + return new Message("CHAT_CHANNEL_LOCAL"); + } + + @Override + public Stream readers() { + Stream players = server.getPlayersConnected().stream().map(Chatter::of).filter(chatter -> ChatManager.reads(chatter, persistId())).map(Chatter.class::cast); + if (isArena()) { + players = Stream.concat(players, ChatManager.spies(server)); + } + return players; + } + + @Override + public boolean canWrite(Chatter sender) { + boolean onServer = sender.getPlayer() != null && server.getPlayersConnected().contains(sender.getPlayer()); + // Moderators can write into fight chats they are not on (/chatspy) + if (!onServer && !(isArena() && sender.user().hasPerm(UserPerm.MODERATION))) { + sender.system("CHAT_NO_WRITE"); + return false; + } + return true; + } + + @Override + protected Message format(Chatter receiver, Chatter sender, String message) { + if (!isArena()) { + return ChatFormat.player("CHAT_LOCAL", receiver, sender, null, message); + } + + String highlighted = ChatFormat.highlightMentions(message, "§7", receiver); + FightInfoPacket info = ChatManager.getFightInfo(server); + int id = sender.user().getId(); + if (info != null && info.getBluePlayers().contains(id)) { + return new Message("CHAT_FIGHT_PARTICIPANT", info.getBlueName(), sender.user(), highlighted); + } else if (info != null && info.getRedPlayers().contains(id)) { + return new Message("CHAT_FIGHT_PARTICIPANT", info.getRedName(), sender.user(), highlighted); + } else if (ChatManager.isReferee(server, id)) { + return new Message("CHAT_FIGHT_REFEREE", sender.user(), highlighted); + } + return new Message("CHAT_FIGHT_SPECTATOR", sender.user(), highlighted); + } + + @Override + protected boolean delivered(Chatter sender, String message, boolean mirror) { + if (isArena()) { + // Arenas never display chat from Velocity, they only record it for replays + NetworkSender.send(server, new ChatRecordPacket(sender.user().getId(), message)); + } + return false; + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createChat(server.getServerInfo().getName(), null, sender, message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java new file mode 100644 index 00000000..46950b1c --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java @@ -0,0 +1,76 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.discord.DiscordBot; + +import java.util.Arrays; +import java.util.stream.Stream; + +public class ServerTeamChannel extends ChatChannel { + + @Override + public String persistId() { + return "serverteam"; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_SERVERTEAM"); + } + + @Override + public Stream readers() { + return Arrays.stream(Chatter.serverteam().getChatters()).filter(chatter -> ChatManager.reads(chatter, persistId())); + } + + @Override + public boolean canWrite(Chatter sender) { + if (!sender.user().hasPerm(UserPerm.TEAM)) { + sender.system("CHAT_NO_WRITE"); + return false; + } + return true; + } + + @Override + protected Message format(Chatter receiver, Chatter sender, String message) { + return ChatFormat.player("CHAT_SERVERTEAM", receiver, sender, null, message); + } + + @Override + protected boolean delivered(Chatter sender, String message, boolean mirror) { + if (mirror) { + DiscordBot.withBot(bot -> bot.getServerTeamChat().send(false, null, null, ChatFormat.player("CHAT_GLOBAL", bot.getServerTeamChat(), sender, null, message))); + } + return true; + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createCommand(AuditLog.SERVER_NAME_VELOCITY, null, sender, "/stc " + message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java new file mode 100644 index 00000000..f29a80f4 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java @@ -0,0 +1,79 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.velocitycore.commands.StreamingCommand; +import lombok.Getter; + +import java.util.stream.Stream; + +public class TeamChannel extends ChatChannel { + + @Getter + private final int team; + + TeamChannel(int team) { + this.team = team; + } + + @Override + public String persistId() { + return "team"; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_TEAM"); + } + + @Override + public Stream readers() { + return Chatter.allPlayers() + .map(Chatter::of) + .filter(chatter -> chatter.user().getTeam() == team) + .filter(StreamingCommand::isNotStreaming) + .filter(chatter -> ChatManager.reads(chatter, persistId())) + .map(Chatter.class::cast); + } + + @Override + public boolean canWrite(Chatter sender) { + if (sender.user().getTeam() != team) { + sender.system("TC_NO_TEAM"); + return false; + } + return true; + } + + @Override + protected Message format(Chatter receiver, Chatter sender, String message) { + return ChatFormat.player("CHAT_TEAM", receiver, sender, null, message); + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createSensitiveCommand(AuditLog.SERVER_NAME_VELOCITY, null, sender, "/tc " + message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java index ea846789..552691d2 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java @@ -23,6 +23,7 @@ import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.chat.ChatManager; import de.steamwar.velocitycore.discord.DiscordBot; @Linked @@ -40,7 +41,7 @@ public class AlertCommand extends SWCommand { } String s = String.join(" ", message); - Chatter.broadcast().system("ALERT", s.replace('&', '§')); + ChatManager.getAlert().send(sender, s); if ("-discord".equals(sendToDiscord)) { DiscordBot.withBot(bot -> bot.getAnnouncementChannel().send(s)); diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java new file mode 100644 index 00000000..cd563a81 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java @@ -0,0 +1,123 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.commands; + +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.command.PreviousArguments; +import de.steamwar.command.SWCommand; +import de.steamwar.command.TypeMapper; +import de.steamwar.linkage.Linked; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.persistent.Subserver; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.chat.ChatChannel; +import de.steamwar.velocitycore.chat.ChatManager; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@Linked +public class ChatCommand extends SWCommand { + + public ChatCommand() { + super("chat"); + } + + @Register(description = "CHAT_USAGE") + public void current(PlayerChatter sender) { + sender.system("CHAT_CHANNEL_CURRENT", ChatManager.getDefault(sender).tag(sender)); + } + + @Register + public void switchTo(PlayerChatter sender, ChatChannel channel) { + ChatManager.switchTo(sender, channel); + } + + @Register + public void joinOrLeave(PlayerChatter sender, ChatChannel channel, @StaticValue(value = {"leave", "join"}, allowISE = true) boolean join) { + if (join) { + ChatManager.join(sender, channel); + } else { + ChatManager.leave(sender, channel); + } + } + + @Register + public void msg(PlayerChatter sender, ChatChannel channel, @StaticValue("msg") String msg, @ErrorMessage(value = "CHAT_USAGE", allowEAs = false) String... message) { + if (!sender.user().hasPerm(UserPerm.MODERATION)) { + sender.system("CHAT_NO_WRITE"); + return; + } + + channel.send(sender, String.join(" ", message)); + } + + @ClassMapper(value = ChatChannel.class, local = true) + public TypeMapper channelTypeMapper() { + return new TypeMapper<>() { + @Override + public ChatChannel map(Chatter sender, PreviousArguments previousArguments, String s) { + return switch (s.toLowerCase()) { + case "global" -> ChatManager.getGlobal(); + case "local" -> sender.getPlayer() == null ? null : sender.getPlayer().getCurrentServer().map(ServerConnection::getServer).map(ChatManager::local).orElse(null); + case "team", "tc" -> sender.user().getTeam() != 0 ? ChatManager.team(sender.user().getTeam()) : null; + case "serverteam", "stc" -> sender.user().hasPerm(UserPerm.TEAM) ? ChatManager.getServerTeam() : null; + default -> arena(sender, s); + }; + } + + @Override + public boolean validate(Chatter sender, ChatChannel value, MessageSender messageSender) { + return !messageSender.send(value == null, "CHAT_CHANNEL_UNKNOWN"); + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + List channels = new ArrayList<>(List.of("global", "local")); + if (sender.user().getTeam() != 0) channels.add("team"); + if (sender.user().hasPerm(UserPerm.TEAM)) channels.add("serverteam"); + if (sender.user().hasPerm(UserPerm.MODERATION)) { + List subserverList = Subserver.getServerList(); + synchronized (subserverList) { + subserverList.stream().filter(Subserver::isArena).map(subserver -> subserver.getServer().getName()).forEach(channels::add); + } + } + return channels; + } + }; + } + + /** + * Moderators can address the fight chat of every arena by its server name. + */ + private static ChatChannel arena(Chatter sender, String name) { + if (!sender.user().hasPerm(UserPerm.MODERATION)) return null; + + Subserver subserver = Subserver.getSubserver(VelocityCore.getProxy().getServer(name).map(RegisteredServer::getServerInfo).orElse(null)); + if (!Subserver.isArena(subserver)) return null; + + return ChatManager.local(subserver.getRegisteredServer()); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java new file mode 100644 index 00000000..6ce66d43 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java @@ -0,0 +1,46 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.commands; + +import de.steamwar.command.SWCommand; +import de.steamwar.linkage.Linked; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.velocitycore.chat.ChatManager; + +@Linked +public class GlobalCommand extends SWCommand { + + public GlobalCommand() { + super("global"); + } + + @Register(description = "GLOBAL_USAGE") + public void genericCommand(Chatter sender, String... message) { + if (message.length > 0) { + ChatManager.getGlobal().send(sender, String.join(" ", message)); + } else if (sender instanceof PlayerChatter player) { + ChatManager.switchTo(player, ChatManager.getGlobal()); + } else { + sender.system("GLOBAL_USAGE"); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java index 40f48fbf..ab52364d 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java @@ -19,10 +19,12 @@ package de.steamwar.velocitycore.commands; +import com.velocitypowered.api.proxy.ServerConnection; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.PlayerChatter; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatChannel; +import de.steamwar.velocitycore.chat.ChatManager; @Linked public class LocalCommand extends SWCommand { @@ -31,8 +33,15 @@ public class LocalCommand extends SWCommand { super("local", "bc", "bauchat"); } - @Register - public void genericCommand(PlayerChatter player, String... message) { - ChatListener.localChat(player, String.join(" ", message)); + @Register(description = "CHAT_BC_USAGE") + public void genericCommand(PlayerChatter sender, String... message) { + ChatChannel channel = sender.getPlayer().getCurrentServer().map(ServerConnection::getServer).map(ChatManager::local).orElse(null); + if (channel == null) return; + + if (message.length == 0) { + ChatManager.switchTo(sender, channel); + } else { + channel.send(sender, String.join(" ", message)); + } } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java index 622aa2dc..67cbd21a 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java @@ -22,12 +22,10 @@ package de.steamwar.velocitycore.commands; import com.velocitypowered.api.proxy.Player; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; -import de.steamwar.messages.Chatter; -import de.steamwar.messages.ChatterGroup; import de.steamwar.messages.PlayerChatter; -import de.steamwar.sql.IgnoreSystem; import de.steamwar.velocitycore.VelocityCore; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatChannel; +import de.steamwar.velocitycore.chat.ChatManager; import static de.steamwar.persistent.Storage.lastChats; @@ -39,7 +37,7 @@ public class MsgCommand extends SWCommand { } @Register(description = "MSG_USAGE") - public void genericCommand(PlayerChatter sender, @ErrorMessage(value = "MSG_OFFLINE") Player target, @ErrorMessage(value = "MSG_USAGE", allowEAs = false) String... message) { + public void genericCommand(PlayerChatter sender, @ErrorMessage(value = "MSG_OFFLINE") Player target, String... message) { msg(sender, target, message); } @@ -49,15 +47,12 @@ public class MsgCommand extends SWCommand { return; } - if (IgnoreSystem.isIgnored(target.getUniqueId(), sender.user().getUUID())) { - sender.system("MSG_IGNORED"); - return; + ChatChannel channel = ChatManager.direct(sender.user().getUUID(), target.getUniqueId()); + if (args.length == 0) { + ChatManager.switchTo(sender, channel); + } else { + channel.send(sender, String.join(" ", args)); } - - Chatter receiver = Chatter.of(target); - ChatListener.sendChat(sender, new ChatterGroup(sender, receiver), "CHAT_MSG", receiver, String.join(" ", args)); - lastChats.put(sender.getPlayer(), target); - lastChats.put(target, sender.getPlayer()); } public static void remove(Player player) { diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java index ae86f249..65582e85 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java @@ -33,7 +33,7 @@ public class RCommand extends SWCommand { } @Register(description = "R_USAGE") - public void genericCommand(PlayerChatter sender, @ErrorMessage(value = "R_USAGE", allowEAs = false) String... message) { + public void genericCommand(PlayerChatter sender, String... message) { MsgCommand.msg(sender, lastChats.get(sender.getPlayer()), message); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java index 0b04183e..ea3932b4 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java @@ -22,8 +22,9 @@ package de.steamwar.velocitycore.commands; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; import de.steamwar.sql.UserPerm; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatManager; @Linked public class ServerTeamchatCommand extends SWCommand { @@ -33,7 +34,13 @@ public class ServerTeamchatCommand extends SWCommand { } @Register(description = "STC_USAGE") - public void genericCommand(Chatter sender, @ErrorMessage(value = "STC_USAGE", allowEAs = false) String... message) { - ChatListener.sendChat(sender, Chatter.serverteam(), "CHAT_SERVERTEAM", null, String.join(" ", message)); + public void genericCommand(Chatter sender, String... message) { + if (message.length > 0) { + ChatManager.getServerTeam().send(sender, String.join(" ", message)); + } else if (sender instanceof PlayerChatter player) { + ChatManager.switchTo(player, ChatManager.getServerTeam()); + } else { + sender.system("STC_USAGE"); + } } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java index e9b1d4d2..1f6d8458 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java @@ -22,9 +22,9 @@ package de.steamwar.velocitycore.commands; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; -import de.steamwar.messages.ChatterGroup; -import de.steamwar.sql.SteamwarUser; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.velocitycore.chat.ChatChannel; +import de.steamwar.velocitycore.chat.ChatManager; @Linked public class TeamchatCommand extends SWCommand { @@ -34,14 +34,20 @@ public class TeamchatCommand extends SWCommand { } @Register(description = "TC_USAGE") - public void genericCommand(Chatter sender, @ErrorMessage(value = "TC_USAGE", allowEAs = false) String... args) { - SteamwarUser user = sender.user(); - - if (user.getTeam() == 0) { + public void genericCommand(Chatter sender, String... args) { + int team = sender.user().getTeam(); + if (team == 0) { sender.system("TC_NO_TEAM"); return; } - ChatListener.sendChat(sender, new ChatterGroup(Chatter.allStream().filter(p -> p.user().getTeam() == user.getTeam()).filter(StreamingCommand::isNotStreaming)), "CHAT_TEAM", null, String.join(" ", args)); + ChatChannel channel = ChatManager.team(team); + if (args.length > 0) { + channel.send(sender, String.join(" ", args)); + } else if (sender instanceof PlayerChatter player) { + ChatManager.switchTo(player, channel); + } else { + sender.system("TC_USAGE"); + } } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java b/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java index 7e7dba1e..4b557bb7 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java @@ -21,6 +21,7 @@ package de.steamwar.velocitycore.discord; import de.steamwar.command.SWCommand; import de.steamwar.messages.Chatter; +import de.steamwar.velocitycore.chat.ChatManager; import de.steamwar.sql.Event; import de.steamwar.velocitycore.VelocityCore; import de.steamwar.velocitycore.discord.channels.*; @@ -181,9 +182,9 @@ public class DiscordBot { // There is a hard limit of 30 messages per minute to send as a webhook, thus with 8 webhooks we can send // 240 messages per minute. Which means 4 every second. I looked at the WGS fights and there were around // ~70 in a short burst and then rather long no new message. - ingameChat = new DiscordChatRoom(config.channel("ingame"), "CHAT_DISCORD_GLOBAL", Chatter::globalChat, 8); + ingameChat = new DiscordChatRoom(config.channel("ingame"), ChatManager.getGlobal(), 8); // 60 messages per minute should be enough for the server team! - serverTeamChat = new DiscordChatRoom(config.channel("serverteam"), "CHAT_DISCORD_SERVERTEAM", Chatter::serverteam, 2); + serverTeamChat = new DiscordChatRoom(config.channel("serverteam"), ChatManager.getServerTeam(), 2); VelocityCore.schedule(() -> { try { diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java index ef02ce11..6c69a516 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java @@ -20,22 +20,17 @@ package de.steamwar.velocitycore.discord.channels; import de.steamwar.messages.Chatter; -import de.steamwar.messages.ChatterGroup; import de.steamwar.sql.Punishment; import de.steamwar.sql.SteamwarUser; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatChannel; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import java.util.function.Supplier; - public class DiscordChatRoom extends DiscordChannel { - private final String format; - private final Supplier target; + private final ChatChannel target; - public DiscordChatRoom(String channel, String format, Supplier target, int maxNumberOfWebhooks) { + public DiscordChatRoom(String channel, ChatChannel target, int maxNumberOfWebhooks) { super(channel, maxNumberOfWebhooks); - this.format = format; this.target = target; } @@ -47,6 +42,6 @@ public class DiscordChatRoom extends DiscordChannel { return; } - ChatListener.sendChat(Chatter.of(user.getUUID(), true), target.get(), format, null, event.getMessage().getContentDisplay().replace('§', '&').replace('\n', ' ')); + target.send(Chatter.of(user.getUUID(), true), event.getMessage().getContentDisplay().replace('§', '&').replace('\n', ' '), false); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java index ad522fee..1da67ed6 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java @@ -27,30 +27,18 @@ import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.event.player.TabCompleteEvent; import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.ServerConnection; -import com.velocitypowered.api.proxy.server.ServerInfo; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; -import de.steamwar.messages.ChatterGroup; -import de.steamwar.messages.Message; -import de.steamwar.messages.PlayerChatter; -import de.steamwar.network.packets.server.PingPacket; -import de.steamwar.persistent.Subserver; import de.steamwar.sql.*; import de.steamwar.velocitycore.ArenaMode; import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.chat.ChatManager; import de.steamwar.velocitycore.commands.PunishmentCommand; -import de.steamwar.velocitycore.discord.DiscordBot; -import de.steamwar.velocitycore.network.NetworkSender; -import de.steamwar.velocitycore.util.SteamwarPrefix; -import java.util.Arrays; import java.util.List; import java.util.Set; -import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Collectors; @Linked public class ChatListener extends BasicListener { @@ -138,21 +126,7 @@ public class ChatListener extends BasicListener { return; } - Subserver subserver = Subserver.getSubserver(player); - String serverName = AuditLog.SERVER_NAME_VELOCITY; - if (Subserver.isArena(subserver) && subserver.getServer() == player.getCurrentServer().orElseThrow().getServerInfo()) { - serverName = subserver.getServer().getName(); - localChat(Chatter.of(player), message); - } else if (message.startsWith("+")) { - serverName = player.getCurrentServer() - .map(ServerConnection::getServerInfo) - .map(ServerInfo::getName) - .orElse(serverName); - localChat(Chatter.of(player), message.substring(1)); - } else { - sendChat(Chatter.of(player), Chatter.globalChat(), "CHAT_GLOBAL", null, message); - } - AuditLog.createChat(serverName, null, user, message); + ChatManager.chat(Chatter.of(player), message); } private static boolean isMistypedCommand(Player player, String message) { @@ -172,115 +146,6 @@ public class ChatListener extends BasicListener { return isCommand; } - public static void sendChat(Chatter sender, ChatterGroup receivers, String format, Chatter msgReceiver, String message) { - SteamwarUser user = sender.user(); - final String coloredMessage = user.hasPerm(UserPerm.COLOR_CHAT) ? message.replace('&', '§') : message; - if (chatFilter(sender, coloredMessage)) return; - - boolean useSwPrefix = useSwPrefix(user); - - boolean noReceiver = true; - for (Chatter player : receivers.getChatters()) { - if (player.chatShown()) { - chatToReciever(player, msgReceiver, user, format, coloredMessage, useSwPrefix); - if (sender.user().getId() != player.user().getId()) noReceiver = false; - } - - } - - if (format.equals("CHAT_GLOBAL")) { - DiscordBot.withBot(bot -> chatToReciever(bot.getIngameChat(), msgReceiver, user, format, coloredMessage, useSwPrefix)); - } else if (format.equals("CHAT_SERVERTEAM")) { - DiscordBot.withBot(bot -> chatToReciever(bot.getServerTeamChat(), msgReceiver, user, "CHAT_GLOBAL", coloredMessage, useSwPrefix)); - } else if (noReceiver) { - sender.system("CHAT_NO_RECEIVER"); - } - } - - public static void localChat(PlayerChatter sender, String message) { - if (message.isEmpty()) { - sender.system("CHAT_BC_USAGE"); - return; - } - - if (ChatListener.filteredCommand(sender, message)) return; - - if (!message.startsWith("/") && chatFilter(sender, message)) return; - - sender.getPlayer().spoofChatInput(message); - } - - private static boolean chatFilter(Chatter sender, String message) { - if (!sender.chatShown()) { - sender.system("CHAT_RECEIVE"); - return true; - } - - if (message.replace("§[a-f0-9klmno]", "").trim().isEmpty()) { - sender.system("CHAT_EMPTY"); - return true; - } - - SteamwarUser user = sender.user(); - if (!user.hasPerm(UserPerm.TEAM) && (message.contains("http:") || message.contains("https:") || message.contains("www."))) { - sender.system("CHAT_NO_LINKS"); - return true; - } - - if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.Mute)) return true; - - if (message.contains("LIXFEL")) { - specialAlert(sender, "Lixfel", "CHAT_LIXFEL_", 3, 6, 11, 12, 15); - } - if (message.contains("YOYONOW")) { - specialAlert(sender, "YoyoNow", "CHAT_YOYONOW_", 3, 6, 11, 12); - } - if (message.contains("CHAOSCAOT")) { - specialAlert(sender, "Chaoscaot", "CHAT_CHAOSCAOT_", 3, 6, 11, 12, 15, 17); - } - - return false; - } - - private static void chatToReciever(Chatter receiver, Chatter msgReceiver, SteamwarUser sender, String format, String message, boolean useSwPrefix) { - UserPerm.Prefix prefix = sender.prefix(); - - String teamPrefix = ""; - if (sender.hasPerm(UserPerm.TEAM) && useSwPrefix) { - teamPrefix = "§eS§8W "; - } else if (sender.getTeam() != 0) { - Team team = Team.byId(sender.getTeam()); - teamPrefix = "§" + team.getTeamColor() + team.getTeamKuerzel() + " "; - } - - String chatColorCode = sender.hasPerm(UserPerm.TEAM) ? "§f" : "§7"; - receiver.prefixless(format, - sender, - msgReceiver == null ? receiver : msgReceiver, - highlightMentions(message, chatColorCode, receiver), - teamPrefix, - "", - prefix == UserPerm.emptyPrefix ? "§f" : prefix.getColorCode(), - prefix.getChatPrefix().length() == 0 ? "§f" : prefix.getChatPrefix() + " ", - chatColorCode); - } - - private static boolean useSwPrefix(SteamwarUser user) { - if (!user.hasPerm(UserPerm.TEAM)) { - return false; - } - - UserPerm.Prefix prefix = user.prefix(); - - boolean hasTeamPrefix = user.getTeam() != 0; - boolean hasSwPrefix = prefix != UserPerm.emptyPrefix; - - if (!hasSwPrefix) return false; - if (!hasTeamPrefix) return true; - - return SteamwarPrefix.usesSWPrefix(user); - } - private static boolean filteredCommand(Chatter sender, String message) { String command = message.split(" ", 2)[0]; if (command.startsWith("/") && command.contains(":")) { @@ -290,27 +155,6 @@ public class ChatListener extends BasicListener { return false; } - private static void specialAlert(Chatter sender, String name, String baseMessage, int... delay) { - sender.system("CHAT_LIXFEL_ACTION_BAR"); - for (int i = 0; i < delay.length; i++) { - int finalI = i; - VelocityCore.schedule(() -> sender.prefixless("CHAT_MSG", name, sender.user(), new Message(baseMessage + (finalI + 1)))).delay(delay[i], TimeUnit.SECONDS).schedule(); - } - } - - private static String highlightMentions(String message, String returnColor, Chatter player) { - if (!message.contains("@")) return message; - - String mark = "@" + player.user().getUserName(); - return Arrays.stream(message.split(" ")).map(cur -> { - if (cur.equalsIgnoreCase(mark) && player.getPlayer() != null) { - NetworkSender.send(player.getPlayer(), new PingPacket(player.user().getId())); - return "§e" + cur + returnColor; - } - return cur; - }).collect(Collectors.joining(" ")); - } - @Subscribe public void onTabCompleteResponseEvent(TabCompleteEvent e) { List suggestions = e.getSuggestions(); diff --git a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java index 704acfec..e14f10c2 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java +++ b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java @@ -24,6 +24,7 @@ import com.velocitypowered.api.proxy.server.RegisteredServer; import de.steamwar.linkage.Linked; import de.steamwar.network.packets.PacketHandler; import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.velocitycore.chat.ChatManager; import de.steamwar.velocitycore.network.NetworkSender; import de.steamwar.velocitycore.network.ServerMetaInfo; import de.steamwar.velocitycore.tablist.TablistManager; @@ -51,6 +52,7 @@ public class FightInfoHandler extends PacketHandler { FightInfoPacket lobbyPacket = packet.withServerName(server.getServerInfo().getName()); TablistManager.newFightInfo(server, packet); + ChatManager.updateFightInfo(server, packet); lobbys.removeIf(s -> s.getPlayersConnected().isEmpty()); lobbys.forEach(lobby -> NetworkSender.send(lobby, lobbyPacket)); From b21ff415a88df8db45bc7b963707914c8f96b6c5 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Thu, 15 Oct 2026 20:06:49 +0200 Subject: [PATCH 07/43] Show channel tags as plain gray prefix Messages from a channel that is not the receiver's default are now prefixed with the plain channel name in dark gray instead of brackets. Direct messages get no tag, as their format already shows sender and receiver. --- .../src/de/steamwar/messages/BungeeCore.properties | 2 +- .../src/de/steamwar/messages/BungeeCore_de.properties | 2 +- .../de/steamwar/velocitycore/chat/ChatChannel.java | 11 +++++++++-- .../de/steamwar/velocitycore/chat/DirectChannel.java | 5 +++++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index bff90140..b617fd32 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -583,7 +583,7 @@ CHAT_FIGHT_PARTICIPANT = {0} {1}§8» §7{2} CHAT_FIGHT_SPECTATOR = §7{0}§8» §7{1} CHAT_FIGHT_REFEREE = §e{0}§8» §e{1} CHAT_FIGHT_TEAM = {0} {1}§8» §f{2} -CHAT_TAGGED = §8[{0}§8] §r{1} +CHAT_TAGGED = §8{0} §r{1} CHAT_CHANNEL_GLOBAL = §eGlobal CHAT_CHANNEL_ARENA = §e{0} CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 3f34d888..e253ce72 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -548,7 +548,7 @@ CHAT_FIGHT_PARTICIPANT = {0} {1}§8» §7{2} CHAT_FIGHT_SPECTATOR = §7{0}§8» §7{1} CHAT_FIGHT_REFEREE = §e{0}§8» §e{1} CHAT_FIGHT_TEAM = {0} {1}§8» §f{2} -CHAT_TAGGED = §8[{0}§8] §r{1} +CHAT_TAGGED = §8{0} §r{1} CHAT_CHANNEL_GLOBAL = §eGlobal CHAT_CHANNEL_ARENA = §e{0} CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java index 8c3be5f9..8d25acb1 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java @@ -47,6 +47,13 @@ public abstract class ChatChannel { */ public abstract Message tag(Chatter receiver); + /** + * @return false if the message format already shows the channel, so no tag is put in front + */ + protected boolean tagged() { + return true; + } + public abstract Stream readers(); /** @@ -90,8 +97,8 @@ public abstract class ChatChannel { if (!receiver.chatShown()) continue; Message msg = format(receiver, sender, coloredMessage); - if (ChatManager.getDefault(receiver) != this) { - msg = new Message("CHAT_TAGGED", tag(receiver), msg); + if (tagged() && ChatManager.getDefault(receiver) != this) { + msg = new Message("CHAT_TAGGED", receiver.parseToPlain(tag(receiver)), msg); } receiver.send(false, null, null, msg); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java index b369bafd..0871357b 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java @@ -59,6 +59,11 @@ public class DirectChannel extends ChatChannel { return new Message("CHAT_CHANNEL_MSG", SteamwarUser.get(partner(receiver.user().getUUID()))); } + @Override + protected boolean tagged() { + return false; + } + @Override public Stream readers() { return Stream.of(first, second) From 317cb0adf87694ddc2041b2f0b3fe0fd1a93b2f3 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 08:48:10 +0200 Subject: [PATCH 08/43] Add channel icons and hovers to chat messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every chat message now starts with a fixed icon of its channel (🌍 Global, ⌂ Local, ⚔ fight, ⚑ fight team, 👥 TC, ⚙ STC, ✉ DM), highlighted yellow if it is the receiver's default channel. Hovering the icon shows the channel name, hovering the sender shows their team name and shorthand. --- .../steamwar/messages/BungeeCore.properties | 28 +++++++++++++++---- .../messages/BungeeCore_de.properties | 28 +++++++++++++++---- .../velocitycore/chat/AlertChannel.java | 7 +++-- .../velocitycore/chat/ChatChannel.java | 24 ++++++++++------ .../velocitycore/chat/ChatFilter.java | 2 +- .../velocitycore/chat/ChatFormat.java | 18 +++++++++--- .../velocitycore/chat/DirectChannel.java | 7 +++-- .../velocitycore/chat/FightTeamChannel.java | 15 ++++++++-- .../velocitycore/chat/GlobalChannel.java | 10 +++++-- .../velocitycore/chat/LocalChannel.java | 22 +++++++++++---- .../velocitycore/chat/ServerTeamChannel.java | 10 +++++-- .../velocitycore/chat/TeamChannel.java | 8 +++++- 12 files changed, 135 insertions(+), 44 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index b617fd32..74454123 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -574,16 +574,32 @@ CHAT_BC_USAGE = §8/§7bc §8[§emessage§8] CHAT_NO_RECEIVER = §cNobody receives your message CHAT_EMPTY = §cDon\'t write meaningless empty messages. -CHAT_SERVERTEAM = §e{0}§8» §f{2} -CHAT_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_LOCAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_TEAM = §e{0}§8» §f{2} -CHAT_MSG = §e{0}§8»§e{1} §7{2} +CHAT_SERVERTEAM_SENDER = §e{0} +CHAT_SERVERTEAM = §8» §f{2} +CHAT_GLOBAL_SENDER = {3}{4}{5}{6}{0} +CHAT_GLOBAL = §8» {7}{2} +CHAT_LOCAL_SENDER = {3}{4}{5}{6}{0} +CHAT_LOCAL = §8» {7}{2} +CHAT_TEAM_SENDER = §e{0} +CHAT_TEAM = §8» §f{2} +CHAT_MSG_SENDER = §e{0} +CHAT_MSG = §8»§e{1} §7{2} +CHAT_SPECIAL_ALERT = §e{0}§8»§e{1} §7{2} CHAT_FIGHT_PARTICIPANT = {0} {1}§8» §7{2} CHAT_FIGHT_SPECTATOR = §7{0}§8» §7{1} CHAT_FIGHT_REFEREE = §e{0}§8» §e{1} CHAT_FIGHT_TEAM = {0} {1}§8» §f{2} -CHAT_TAGGED = §8{0} §r{1} +CHAT_ICON = {0}{1} +CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Prefix: §r{1}{2} +CHAT_ICON_GLOBAL = 🌍 +CHAT_ICON_LOCAL = ⌂ +CHAT_ICON_FIGHT = ⚔ +CHAT_ICON_FIGHT_FOREIGN = ⚔ §8{0} +CHAT_ICON_FIGHT_TEAM = {0}⚑ +CHAT_ICON_FIGHT_TEAM_FOREIGN = {0}⚑ §8{1} +CHAT_ICON_TEAM = 👥 +CHAT_ICON_SERVERTEAM = ⚙ +CHAT_ICON_MSG = ✉ CHAT_CHANNEL_GLOBAL = §eGlobal CHAT_CHANNEL_ARENA = §e{0} CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index e253ce72..437a5366 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -539,16 +539,32 @@ CHAT_BC_USAGE = §8/§7bc §8[§eNachricht§8] CHAT_NO_RECEIVER = §cNiemand empfängt deine Nachricht CHAT_EMPTY = §cSchreibe keine inhaltslosen Nachrichten. -CHAT_SERVERTEAM = §e{0}§8» §f{2} -CHAT_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_LOCAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_TEAM = §e{0}§8» §f{2} -CHAT_MSG = §e{0}§8»§e{1} §7{2} +CHAT_SERVERTEAM_SENDER = §e{0} +CHAT_SERVERTEAM = §8» §f{2} +CHAT_GLOBAL_SENDER = {3}{4}{5}{6}{0} +CHAT_GLOBAL = §8» {7}{2} +CHAT_LOCAL_SENDER = {3}{4}{5}{6}{0} +CHAT_LOCAL = §8» {7}{2} +CHAT_TEAM_SENDER = §e{0} +CHAT_TEAM = §8» §f{2} +CHAT_MSG_SENDER = §e{0} +CHAT_MSG = §8»§e{1} §7{2} +CHAT_SPECIAL_ALERT = §e{0}§8»§e{1} §7{2} CHAT_FIGHT_PARTICIPANT = {0} {1}§8» §7{2} CHAT_FIGHT_SPECTATOR = §7{0}§8» §7{1} CHAT_FIGHT_REFEREE = §e{0}§8» §e{1} CHAT_FIGHT_TEAM = {0} {1}§8» §f{2} -CHAT_TAGGED = §8{0} §r{1} +CHAT_ICON = {0}{1} +CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Kürzel: §r{1}{2} +CHAT_ICON_GLOBAL = 🌍 +CHAT_ICON_LOCAL = ⌂ +CHAT_ICON_FIGHT = ⚔ +CHAT_ICON_FIGHT_FOREIGN = ⚔ §8{0} +CHAT_ICON_FIGHT_TEAM = {0}⚑ +CHAT_ICON_FIGHT_TEAM_FOREIGN = {0}⚑ §8{1} +CHAT_ICON_TEAM = 👥 +CHAT_ICON_SERVERTEAM = ⚙ +CHAT_ICON_MSG = ✉ CHAT_CHANNEL_GLOBAL = §eGlobal CHAT_CHANNEL_ARENA = §e{0} CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java index 2c194f88..a86964fb 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java @@ -23,6 +23,7 @@ package de.steamwar.velocitycore.chat; import de.steamwar.messages.Chatter; import de.steamwar.messages.Message; import de.steamwar.sql.UserPerm; +import net.kyori.adventure.text.Component; import java.util.Arrays; import java.util.stream.Stream; @@ -54,8 +55,8 @@ public class AlertChannel extends ChatChannel { } @Override - protected Message format(Chatter receiver, Chatter sender, String message) { - return new Message("ALERT", message.replace('&', '§')); + protected Component format(Chatter receiver, Chatter sender, String message) { + return receiver.parse(true, new Message("ALERT", message.replace('&', '§'))); } /** @@ -65,6 +66,6 @@ public class AlertChannel extends ChatChannel { public void send(Chatter sender, String message, boolean mirror) { if (!canWrite(sender)) return; - readers().forEach(reader -> reader.send(true, null, null, format(reader, sender, message))); + readers().forEach(reader -> reader.sendMessage(format(reader, sender, message))); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java index 8d25acb1..9e8e5861 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java @@ -24,6 +24,8 @@ import de.steamwar.messages.Message; import de.steamwar.sql.AuditLog; import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.UserPerm; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; import java.util.LinkedHashMap; import java.util.Map; @@ -43,15 +45,16 @@ public abstract class ChatChannel { } /** - * @return the channel name shown in front of messages of this channel if it is not the default channel of the receiver + * @return the channel name shown in channel related system messages */ public abstract Message tag(Chatter receiver); /** - * @return false if the message format already shows the channel, so no tag is put in front + * @return the icon always shown in front of messages of this channel, null if the message format already shows the channel. + * The icon is highlighted if this is the default channel of the receiver. */ - protected boolean tagged() { - return true; + protected Message icon(Chatter receiver) { + return null; } public abstract Stream readers(); @@ -61,7 +64,7 @@ public abstract class ChatChannel { */ public abstract boolean canWrite(Chatter sender); - protected abstract Message format(Chatter receiver, Chatter sender, String message); + protected abstract Component format(Chatter receiver, Chatter sender, String message); /** * Called after a message was delivered to all readers. @@ -96,11 +99,14 @@ public abstract class ChatChannel { for (Chatter receiver : receivers.values()) { if (!receiver.chatShown()) continue; - Message msg = format(receiver, sender, coloredMessage); - if (tagged() && ChatManager.getDefault(receiver) != this) { - msg = new Message("CHAT_TAGGED", receiver.parseToPlain(tag(receiver)), msg); + Component msg = format(receiver, sender, coloredMessage); + Message icon = icon(receiver); + if (icon != null) { + Component iconComponent = receiver.parse(new Message("CHAT_ICON", ChatManager.getDefault(receiver) == this ? "§e" : "§7", icon)) + .hoverEvent(HoverEvent.showText(receiver.parse(tag(receiver)))); + msg = Component.text().append(iconComponent).append(Component.text(" ")).append(msg).build(); } - receiver.send(false, null, null, msg); + receiver.sendMessage(msg); if (receiver.user().getId() != user.getId()) noReceiver = false; } diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java index efe8ebb2..51c2841c 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java @@ -70,7 +70,7 @@ public class ChatFilter { sender.system("CHAT_LIXFEL_ACTION_BAR"); for (int i = 0; i < delay.length; i++) { int finalI = i; - VelocityCore.schedule(() -> sender.prefixless("CHAT_MSG", name, sender.user(), new Message(baseMessage + (finalI + 1)))).delay(delay[i], TimeUnit.SECONDS).schedule(); + VelocityCore.schedule(() -> sender.prefixless("CHAT_SPECIAL_ALERT", name, sender.user(), new Message(baseMessage + (finalI + 1)))).delay(delay[i], TimeUnit.SECONDS).schedule(); } } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java index 28a114f2..aeb8064e 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java @@ -28,6 +28,8 @@ import de.steamwar.sql.Team; import de.steamwar.sql.UserPerm; import de.steamwar.velocitycore.network.NetworkSender; import de.steamwar.velocitycore.util.SteamwarPrefix; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; import java.util.Arrays; import java.util.stream.Collectors; @@ -37,9 +39,10 @@ public class ChatFormat { private ChatFormat() {} /** - * Formats a message with the rank and team prefixes of the sender. + * Formats a message with the rank and team prefixes of the sender. The sender part ({@code _SENDER}) + * shows the team of the sender on hover. */ - public static Message player(String format, Chatter receiver, Chatter sender, Chatter msgReceiver, String message) { + public static Component player(String format, Chatter receiver, Chatter sender, Chatter msgReceiver, String message) { SteamwarUser user = sender.user(); UserPerm.Prefix prefix = user.prefix(); @@ -52,7 +55,7 @@ public class ChatFormat { } String chatColorCode = user.hasPerm(UserPerm.TEAM) ? "§f" : "§7"; - return new Message(format, + Object[] params = { user, msgReceiver == null ? receiver : msgReceiver, highlightMentions(message, chatColorCode, receiver), @@ -60,7 +63,14 @@ public class ChatFormat { "", prefix == UserPerm.emptyPrefix ? "§f" : prefix.getColorCode(), prefix.getChatPrefix().length() == 0 ? "§f" : prefix.getChatPrefix() + " ", - chatColorCode); + chatColorCode}; + + Component senderPart = receiver.parse(new Message(format + "_SENDER", params)); + if (user.getTeam() != 0) { + Team team = Team.byId(user.getTeam()); + senderPart = senderPart.hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM", team.getTeamName(), "§" + team.getTeamColor(), team.getTeamKuerzel())))); + } + return Component.text().append(senderPart).append(receiver.parse(new Message(format, params))).build(); } private static boolean useSwPrefix(SteamwarUser user) { diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java index 0871357b..3695c701 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java @@ -27,6 +27,7 @@ import de.steamwar.sql.AuditLog; import de.steamwar.sql.IgnoreSystem; import de.steamwar.sql.SteamwarUser; import de.steamwar.velocitycore.VelocityCore; +import net.kyori.adventure.text.Component; import java.util.UUID; import java.util.stream.Stream; @@ -60,8 +61,8 @@ public class DirectChannel extends ChatChannel { } @Override - protected boolean tagged() { - return false; + protected Message icon(Chatter receiver) { + return new Message("CHAT_ICON_MSG"); } @Override @@ -88,7 +89,7 @@ public class DirectChannel extends ChatChannel { } @Override - protected Message format(Chatter receiver, Chatter sender, String message) { + protected Component format(Chatter receiver, Chatter sender, String message) { return ChatFormat.player("CHAT_MSG", receiver, sender, Chatter.of(partner(sender.user().getUUID())), message); } diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java index 93b80e60..3768fa75 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java @@ -27,6 +27,7 @@ import de.steamwar.network.packets.common.FightInfoPacket; import de.steamwar.sql.AuditLog; import de.steamwar.sql.SteamwarUser; import lombok.Getter; +import net.kyori.adventure.text.Component; import java.util.Collections; import java.util.List; @@ -72,6 +73,16 @@ public class FightTeamChannel extends ChatChannel { return new Message("CHAT_CHANNEL_FIGHT_TEAM", server.getServerInfo().getName(), teamName()); } + @Override + protected Message icon(Chatter receiver) { + String teamName = teamName(); + String color = teamName.length() >= 2 && teamName.charAt(0) == '§' ? teamName.substring(0, 2) : "§7"; + if (receiver.getPlayer() != null && server.getPlayersConnected().contains(receiver.getPlayer())) { + return new Message("CHAT_ICON_FIGHT_TEAM", color); + } + return new Message("CHAT_ICON_FIGHT_TEAM_FOREIGN", color, server.getServerInfo().getName()); + } + @Override public Stream readers() { List members = members(); @@ -90,8 +101,8 @@ public class FightTeamChannel extends ChatChannel { } @Override - protected Message format(Chatter receiver, Chatter sender, String message) { - return new Message("CHAT_FIGHT_TEAM", teamName(), sender.user(), ChatFormat.highlightMentions(message, "§f", receiver)); + protected Component format(Chatter receiver, Chatter sender, String message) { + return receiver.parse(new Message("CHAT_FIGHT_TEAM", teamName(), sender.user(), ChatFormat.highlightMentions(message, "§f", receiver))); } @Override diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java index a4146c1d..cc2219cd 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java @@ -23,6 +23,7 @@ package de.steamwar.velocitycore.chat; import de.steamwar.messages.Chatter; import de.steamwar.messages.Message; import de.steamwar.velocitycore.discord.DiscordBot; +import net.kyori.adventure.text.Component; import java.util.stream.Stream; @@ -38,6 +39,11 @@ public class GlobalChannel extends ChatChannel { return new Message("CHAT_CHANNEL_GLOBAL"); } + @Override + protected Message icon(Chatter receiver) { + return new Message("CHAT_ICON_GLOBAL"); + } + @Override public Stream readers() { return Stream.concat(Stream.of(Chatter.console()), Chatter.allPlayers().filter(ChatManager::readsGlobal).map(Chatter::of).filter(chatter -> ChatManager.reads(chatter, persistId()))); @@ -49,14 +55,14 @@ public class GlobalChannel extends ChatChannel { } @Override - protected Message format(Chatter receiver, Chatter sender, String message) { + protected Component format(Chatter receiver, Chatter sender, String message) { return ChatFormat.player("CHAT_GLOBAL", receiver, sender, null, message); } @Override protected boolean delivered(Chatter sender, String message, boolean mirror) { if (mirror) { - DiscordBot.withBot(bot -> bot.getIngameChat().send(false, null, null, format(bot.getIngameChat(), sender, message))); + DiscordBot.withBot(bot -> bot.getIngameChat().sendMessage(format(bot.getIngameChat(), sender, message))); } return true; } diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java index 5eaf90aa..27f7b636 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java @@ -30,6 +30,7 @@ import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.UserPerm; import de.steamwar.velocitycore.network.NetworkSender; import lombok.Getter; +import net.kyori.adventure.text.Component; import java.util.stream.Stream; @@ -62,6 +63,17 @@ public class LocalChannel extends ChatChannel { return new Message("CHAT_CHANNEL_LOCAL"); } + @Override + protected Message icon(Chatter receiver) { + if (!isArena()) return new Message("CHAT_ICON_LOCAL"); + + // Moderators reading an arena they are not on need to know which arena it is + if (receiver.getPlayer() != null && server.getPlayersConnected().contains(receiver.getPlayer())) { + return new Message("CHAT_ICON_FIGHT"); + } + return new Message("CHAT_ICON_FIGHT_FOREIGN", server.getServerInfo().getName()); + } + @Override public Stream readers() { Stream players = server.getPlayersConnected().stream().map(Chatter::of).filter(chatter -> ChatManager.reads(chatter, persistId())).map(Chatter.class::cast); @@ -83,7 +95,7 @@ public class LocalChannel extends ChatChannel { } @Override - protected Message format(Chatter receiver, Chatter sender, String message) { + protected Component format(Chatter receiver, Chatter sender, String message) { if (!isArena()) { return ChatFormat.player("CHAT_LOCAL", receiver, sender, null, message); } @@ -92,13 +104,13 @@ public class LocalChannel extends ChatChannel { FightInfoPacket info = ChatManager.getFightInfo(server); int id = sender.user().getId(); if (info != null && info.getBluePlayers().contains(id)) { - return new Message("CHAT_FIGHT_PARTICIPANT", info.getBlueName(), sender.user(), highlighted); + return receiver.parse(new Message("CHAT_FIGHT_PARTICIPANT", info.getBlueName(), sender.user(), highlighted)); } else if (info != null && info.getRedPlayers().contains(id)) { - return new Message("CHAT_FIGHT_PARTICIPANT", info.getRedName(), sender.user(), highlighted); + return receiver.parse(new Message("CHAT_FIGHT_PARTICIPANT", info.getRedName(), sender.user(), highlighted)); } else if (ChatManager.isReferee(server, id)) { - return new Message("CHAT_FIGHT_REFEREE", sender.user(), highlighted); + return receiver.parse(new Message("CHAT_FIGHT_REFEREE", sender.user(), highlighted)); } - return new Message("CHAT_FIGHT_SPECTATOR", sender.user(), highlighted); + return receiver.parse(new Message("CHAT_FIGHT_SPECTATOR", sender.user(), highlighted)); } @Override diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java index 46950b1c..1860633d 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java @@ -26,6 +26,7 @@ import de.steamwar.sql.AuditLog; import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.UserPerm; import de.steamwar.velocitycore.discord.DiscordBot; +import net.kyori.adventure.text.Component; import java.util.Arrays; import java.util.stream.Stream; @@ -42,6 +43,11 @@ public class ServerTeamChannel extends ChatChannel { return new Message("CHAT_CHANNEL_SERVERTEAM"); } + @Override + protected Message icon(Chatter receiver) { + return new Message("CHAT_ICON_SERVERTEAM"); + } + @Override public Stream readers() { return Arrays.stream(Chatter.serverteam().getChatters()).filter(chatter -> ChatManager.reads(chatter, persistId())); @@ -57,14 +63,14 @@ public class ServerTeamChannel extends ChatChannel { } @Override - protected Message format(Chatter receiver, Chatter sender, String message) { + protected Component format(Chatter receiver, Chatter sender, String message) { return ChatFormat.player("CHAT_SERVERTEAM", receiver, sender, null, message); } @Override protected boolean delivered(Chatter sender, String message, boolean mirror) { if (mirror) { - DiscordBot.withBot(bot -> bot.getServerTeamChat().send(false, null, null, ChatFormat.player("CHAT_GLOBAL", bot.getServerTeamChat(), sender, null, message))); + DiscordBot.withBot(bot -> bot.getServerTeamChat().sendMessage(ChatFormat.player("CHAT_GLOBAL", bot.getServerTeamChat(), sender, null, message))); } return true; } diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java index f29a80f4..f362099b 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java @@ -26,6 +26,7 @@ import de.steamwar.sql.AuditLog; import de.steamwar.sql.SteamwarUser; import de.steamwar.velocitycore.commands.StreamingCommand; import lombok.Getter; +import net.kyori.adventure.text.Component; import java.util.stream.Stream; @@ -48,6 +49,11 @@ public class TeamChannel extends ChatChannel { return new Message("CHAT_CHANNEL_TEAM"); } + @Override + protected Message icon(Chatter receiver) { + return new Message("CHAT_ICON_TEAM"); + } + @Override public Stream readers() { return Chatter.allPlayers() @@ -68,7 +74,7 @@ public class TeamChannel extends ChatChannel { } @Override - protected Message format(Chatter receiver, Chatter sender, String message) { + protected Component format(Chatter receiver, Chatter sender, String message) { return ChatFormat.player("CHAT_TEAM", receiver, sender, null, message); } From 975d42c17f26012578a12f6adb68cfbcd735443d Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 08:57:45 +0200 Subject: [PATCH 09/43] Add team hovers to fight chat and make DMs non-default - Names in the fight chat show the sender's team on hover like everywhere else; the Blue/Red team prefix is only hoverable in event fights, where it is the actual team - Server team members using the SW prefix show the SteamWar team - Direct messages can no longer be the default channel; /msg and /r require a message again --- .../steamwar/messages/BungeeCore.properties | 13 +++-- .../messages/BungeeCore_de.properties | 13 +++-- .../velocitycore/chat/ChatFormat.java | 47 +++++++++++++++++-- .../velocitycore/chat/ChatManager.java | 33 +++++++------ .../velocitycore/chat/DirectChannel.java | 5 ++ .../velocitycore/chat/FightTeamChannel.java | 2 +- .../velocitycore/chat/LocalChannel.java | 8 ++-- .../velocitycore/commands/MsgCommand.java | 10 +--- .../velocitycore/commands/RCommand.java | 2 +- 9 files changed, 90 insertions(+), 43 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index 74454123..c9db69bb 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -585,10 +585,14 @@ CHAT_TEAM = §8» §f{2} CHAT_MSG_SENDER = §e{0} CHAT_MSG = §8»§e{1} §7{2} CHAT_SPECIAL_ALERT = §e{0}§8»§e{1} §7{2} -CHAT_FIGHT_PARTICIPANT = {0} {1}§8» §7{2} -CHAT_FIGHT_SPECTATOR = §7{0}§8» §7{1} -CHAT_FIGHT_REFEREE = §e{0}§8» §e{1} -CHAT_FIGHT_TEAM = {0} {1}§8» §f{2} +CHAT_FIGHT_PARTICIPANT_SENDER = {0}{1} +CHAT_FIGHT_PARTICIPANT = §8» §7{0} +CHAT_FIGHT_SPECTATOR_SENDER = §7{1} +CHAT_FIGHT_SPECTATOR = §8» §7{0} +CHAT_FIGHT_REFEREE_SENDER = §e{1} +CHAT_FIGHT_REFEREE = §8» §e{0} +CHAT_FIGHT_TEAM_SENDER = {0}{1} +CHAT_FIGHT_TEAM = §8» §f{0} CHAT_ICON = {0}{1} CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Prefix: §r{1}{2} CHAT_ICON_GLOBAL = 🌍 @@ -610,7 +614,6 @@ CHAT_CHANNEL_ALERT = §cAlert CHAT_CHANNEL_LOCAL = §eLocal CHAT_CHANNEL_CURRENT = §7You are writing into §e{0}§7. CHAT_CHANNEL_SWITCHED = §7You are now writing into §e{0}§7. -CHAT_CHANNEL_CLOSED = §e{0} §7was closed. CHAT_CHANNEL_NOT_SWITCHABLE = §cYou cannot write into this channel by default. CHAT_NO_WRITE = §cYou cannot write into this channel. CHAT_FIGHT_NO_TEAM = §cYou are not in a fight team. diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 437a5366..09199878 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -550,10 +550,14 @@ CHAT_TEAM = §8» §f{2} CHAT_MSG_SENDER = §e{0} CHAT_MSG = §8»§e{1} §7{2} CHAT_SPECIAL_ALERT = §e{0}§8»§e{1} §7{2} -CHAT_FIGHT_PARTICIPANT = {0} {1}§8» §7{2} -CHAT_FIGHT_SPECTATOR = §7{0}§8» §7{1} -CHAT_FIGHT_REFEREE = §e{0}§8» §e{1} -CHAT_FIGHT_TEAM = {0} {1}§8» §f{2} +CHAT_FIGHT_PARTICIPANT_SENDER = {0}{1} +CHAT_FIGHT_PARTICIPANT = §8» §7{0} +CHAT_FIGHT_SPECTATOR_SENDER = §7{1} +CHAT_FIGHT_SPECTATOR = §8» §7{0} +CHAT_FIGHT_REFEREE_SENDER = §e{1} +CHAT_FIGHT_REFEREE = §8» §e{0} +CHAT_FIGHT_TEAM_SENDER = {0}{1} +CHAT_FIGHT_TEAM = §8» §f{0} CHAT_ICON = {0}{1} CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Kürzel: §r{1}{2} CHAT_ICON_GLOBAL = 🌍 @@ -575,7 +579,6 @@ CHAT_CHANNEL_ALERT = §cAlert CHAT_CHANNEL_LOCAL = §eLokal CHAT_CHANNEL_CURRENT = §7Du schreibst in §e{0}§7. CHAT_CHANNEL_SWITCHED = §7Du schreibst jetzt in §e{0}§7. -CHAT_CHANNEL_CLOSED = §e{0} §7wurde geschlossen. CHAT_CHANNEL_NOT_SWITCHABLE = §cIn diesen Kanal kannst du nicht standardmäßig schreiben. CHAT_NO_WRITE = §cDu kannst nicht in diesen Kanal schreiben. CHAT_FIGHT_NO_TEAM = §cDu bist in keinem Kampfteam. diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java index aeb8064e..ad38e020 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java @@ -20,6 +20,7 @@ package de.steamwar.velocitycore.chat; +import com.velocitypowered.api.proxy.server.RegisteredServer; import de.steamwar.messages.Chatter; import de.steamwar.messages.Message; import de.steamwar.network.packets.server.PingPacket; @@ -29,6 +30,7 @@ import de.steamwar.sql.UserPerm; import de.steamwar.velocitycore.network.NetworkSender; import de.steamwar.velocitycore.util.SteamwarPrefix; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.event.HoverEvent; import java.util.Arrays; @@ -65,14 +67,49 @@ public class ChatFormat { prefix.getChatPrefix().length() == 0 ? "§f" : prefix.getChatPrefix() + " ", chatColorCode}; - Component senderPart = receiver.parse(new Message(format + "_SENDER", params)); - if (user.getTeam() != 0) { - Team team = Team.byId(user.getTeam()); - senderPart = senderPart.hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM", team.getTeamName(), "§" + team.getTeamColor(), team.getTeamKuerzel())))); - } + Component senderPart = withTeamHover(receiver, user, receiver.parse(new Message(format + "_SENDER", params))); return Component.text().append(senderPart).append(receiver.parse(new Message(format, params))).build(); } + /** + * Formats a fight chat message. The fight team name is only hoverable in event fights, where it is the actual team. + * + * @param coloredTeamName the fight team of the sender, null if the sender is not in a fight team + */ + public static Component fight(Chatter receiver, RegisteredServer server, Chatter sender, String coloredTeamName, String format, String message) { + TextComponent.Builder line = Component.text(); + String nameColor = ""; + if (coloredTeamName != null) { + Component team = Chatter.SERIALIZER.deserialize(coloredTeamName); + Team eventTeam = ChatManager.eventTeam(server, coloredTeamName); + if (eventTeam != null) { + team = team.hoverEvent(HoverEvent.showText(teamHover(receiver, eventTeam))); + } + line.append(team).append(Component.text(" ")); + nameColor = coloredTeamName.length() >= 2 && coloredTeamName.charAt(0) == '§' ? coloredTeamName.substring(0, 2) : ""; + } + + line.append(withTeamHover(receiver, sender.user(), receiver.parse(new Message(format + "_SENDER", nameColor, sender.user())))); + line.append(receiver.parse(new Message(format, message))); + return line.build(); + } + + /** + * Shows the team of the user when hovering the component, the SteamWar team for server team members using the SW prefix. + */ + public static Component withTeamHover(Chatter receiver, SteamwarUser user, Component component) { + if (useSwPrefix(user)) { + return component.hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM", "SteamWar", "§e", "S§8W")))); + } + if (user.getTeam() == 0) return component; + + return component.hoverEvent(HoverEvent.showText(teamHover(receiver, Team.byId(user.getTeam())))); + } + + private static Component teamHover(Chatter receiver, Team team) { + return receiver.parse(new Message("CHAT_HOVER_TEAM", team.getTeamName(), "§" + team.getTeamColor(), team.getTeamKuerzel())); + } + private static boolean useSwPrefix(SteamwarUser user) { if (!user.hasPerm(UserPerm.TEAM)) { return false; diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java index a437532a..c8565b8e 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -33,6 +33,7 @@ import de.steamwar.network.packets.common.FightInfoPacket; import de.steamwar.persistent.Subserver; import de.steamwar.sql.Event; import de.steamwar.sql.Referee; +import de.steamwar.sql.Team; import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.UserConfig; import de.steamwar.sql.UserPerm; @@ -221,6 +222,23 @@ public class ChatManager extends BasicListener { return fightInfos.get(server); } + /** + * @return the team of an event fight on the server whose colored name is shown, null if the server runs no event fight + */ + static Team eventTeam(RegisteredServer server, String coloredTeamName) { + Subserver subserver = Subserver.getSubserver(server.getServerInfo()); + if (subserver == null) return null; + + String kuerzel = coloredTeamName.replaceAll("§.", ""); + for (Map.Entry entry : new ArrayList<>(EventStarter.getEventServer().entrySet())) { + if (entry.getValue() != subserver) continue; + + Team team = Team.byId(entry.getKey()); + if (team != null && team.getTeamKuerzel().equals(kuerzel)) return team; + } + return null; + } + /** * Updates the fight teams of an arena. Players who just joined a fight team write into the fight chat by default. */ @@ -382,19 +400,6 @@ public class ChatManager extends BasicListener { spies.remove(uuid); hidden.remove(uuid); - Iterator it = directs.iterator(); - while (it.hasNext()) { - DirectChannel channel = it.next(); - if (!channel.contains(uuid)) continue; - it.remove(); - - Player partner = VelocityCore.getProxy().getPlayer(channel.partner(uuid)).orElse(null); - if (partner != null && defaults.get(partner.getUniqueId()) == channel) { - PlayerChatter chatter = Chatter.of(partner); - chatter.system("CHAT_CHANNEL_CLOSED", channel.tag(chatter)); - defaults.put(partner.getUniqueId(), resolvePersisted(partner, currentServer(partner).orElse(null))); - chatter.system("CHAT_CHANNEL_SWITCHED", defaults.get(partner.getUniqueId()).tag(chatter)); - } - } + directs.removeIf(channel -> channel.contains(uuid)); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java index 3695c701..f0a79923 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java @@ -60,6 +60,11 @@ public class DirectChannel extends ChatChannel { return new Message("CHAT_CHANNEL_MSG", SteamwarUser.get(partner(receiver.user().getUUID()))); } + @Override + public boolean switchable() { + return false; + } + @Override protected Message icon(Chatter receiver) { return new Message("CHAT_ICON_MSG"); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java index 3768fa75..869d7c24 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java @@ -102,7 +102,7 @@ public class FightTeamChannel extends ChatChannel { @Override protected Component format(Chatter receiver, Chatter sender, String message) { - return receiver.parse(new Message("CHAT_FIGHT_TEAM", teamName(), sender.user(), ChatFormat.highlightMentions(message, "§f", receiver))); + return ChatFormat.fight(receiver, server, sender, teamName(), "CHAT_FIGHT_TEAM", ChatFormat.highlightMentions(message, "§f", receiver)); } @Override diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java index 27f7b636..12aece2a 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java @@ -104,13 +104,13 @@ public class LocalChannel extends ChatChannel { FightInfoPacket info = ChatManager.getFightInfo(server); int id = sender.user().getId(); if (info != null && info.getBluePlayers().contains(id)) { - return receiver.parse(new Message("CHAT_FIGHT_PARTICIPANT", info.getBlueName(), sender.user(), highlighted)); + return ChatFormat.fight(receiver, server, sender, info.getBlueName(), "CHAT_FIGHT_PARTICIPANT", highlighted); } else if (info != null && info.getRedPlayers().contains(id)) { - return receiver.parse(new Message("CHAT_FIGHT_PARTICIPANT", info.getRedName(), sender.user(), highlighted)); + return ChatFormat.fight(receiver, server, sender, info.getRedName(), "CHAT_FIGHT_PARTICIPANT", highlighted); } else if (ChatManager.isReferee(server, id)) { - return receiver.parse(new Message("CHAT_FIGHT_REFEREE", sender.user(), highlighted)); + return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_REFEREE", highlighted); } - return receiver.parse(new Message("CHAT_FIGHT_SPECTATOR", sender.user(), highlighted)); + return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_SPECTATOR", highlighted); } @Override diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java index 67cbd21a..4db12170 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java @@ -24,7 +24,6 @@ import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.PlayerChatter; import de.steamwar.velocitycore.VelocityCore; -import de.steamwar.velocitycore.chat.ChatChannel; import de.steamwar.velocitycore.chat.ChatManager; import static de.steamwar.persistent.Storage.lastChats; @@ -37,7 +36,7 @@ public class MsgCommand extends SWCommand { } @Register(description = "MSG_USAGE") - public void genericCommand(PlayerChatter sender, @ErrorMessage(value = "MSG_OFFLINE") Player target, String... message) { + public void genericCommand(PlayerChatter sender, @ErrorMessage(value = "MSG_OFFLINE") Player target, @ErrorMessage(value = "MSG_USAGE", allowEAs = false) String... message) { msg(sender, target, message); } @@ -47,12 +46,7 @@ public class MsgCommand extends SWCommand { return; } - ChatChannel channel = ChatManager.direct(sender.user().getUUID(), target.getUniqueId()); - if (args.length == 0) { - ChatManager.switchTo(sender, channel); - } else { - channel.send(sender, String.join(" ", args)); - } + ChatManager.direct(sender.user().getUUID(), target.getUniqueId()).send(sender, String.join(" ", args)); } public static void remove(Player player) { diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java index 65582e85..ae86f249 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java @@ -33,7 +33,7 @@ public class RCommand extends SWCommand { } @Register(description = "R_USAGE") - public void genericCommand(PlayerChatter sender, String... message) { + public void genericCommand(PlayerChatter sender, @ErrorMessage(value = "R_USAGE", allowEAs = false) String... message) { MsgCommand.msg(sender, lastChats.get(sender.getPlayer()), message); } } From 65cc6119b04cecb929d2fef9c4ed9d122ce4e0c0 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 09:00:28 +0200 Subject: [PATCH 10/43] Show default channel notice with its icon The notice on join, on every default change and for /chat is now " is your default channel" in green without the SteamWar prefix; hovering the icon shows the channel name. --- .../steamwar/messages/BungeeCore.properties | 3 +-- .../messages/BungeeCore_de.properties | 3 +-- .../velocitycore/chat/ChatManager.java | 22 ++++++++++++++++--- .../velocitycore/commands/ChatCommand.java | 2 +- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index c9db69bb..36fdc284 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -612,8 +612,7 @@ CHAT_CHANNEL_SERVERTEAM = §eSTC CHAT_CHANNEL_MSG = §eMSG §7{0} CHAT_CHANNEL_ALERT = §cAlert CHAT_CHANNEL_LOCAL = §eLocal -CHAT_CHANNEL_CURRENT = §7You are writing into §e{0}§7. -CHAT_CHANNEL_SWITCHED = §7You are now writing into §e{0}§7. +CHAT_CHANNEL_DEFAULT = §ais your default channel CHAT_CHANNEL_NOT_SWITCHABLE = §cYou cannot write into this channel by default. CHAT_NO_WRITE = §cYou cannot write into this channel. CHAT_FIGHT_NO_TEAM = §cYou are not in a fight team. diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 09199878..0c54dccc 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -577,8 +577,7 @@ CHAT_CHANNEL_SERVERTEAM = §eSTC CHAT_CHANNEL_MSG = §eMSG §7{0} CHAT_CHANNEL_ALERT = §cAlert CHAT_CHANNEL_LOCAL = §eLokal -CHAT_CHANNEL_CURRENT = §7Du schreibst in §e{0}§7. -CHAT_CHANNEL_SWITCHED = §7Du schreibst jetzt in §e{0}§7. +CHAT_CHANNEL_DEFAULT = §aist dein Standardkanal CHAT_CHANNEL_NOT_SWITCHABLE = §cIn diesen Kanal kannst du nicht standardmäßig schreiben. CHAT_NO_WRITE = §cDu kannst nicht in diesen Kanal schreiben. CHAT_FIGHT_NO_TEAM = §cDu bist in keinem Kampfteam. diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java index c8565b8e..95359244 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -28,6 +28,7 @@ import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.server.RegisteredServer; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; import de.steamwar.messages.PlayerChatter; import de.steamwar.network.packets.common.FightInfoPacket; import de.steamwar.persistent.Subserver; @@ -41,6 +42,8 @@ import de.steamwar.velocitycore.EventStarter; import de.steamwar.velocitycore.VelocityCore; import de.steamwar.velocitycore.listeners.BasicListener; import lombok.Getter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -313,7 +316,7 @@ public class ChatManager extends BasicListener { } defaults.put(uuid, channel); - player.system("CHAT_CHANNEL_SWITCHED", channel.tag(player)); + notifyDefault(player, channel); } private static void setDefault(PlayerChatter player, ChatChannel channel) { @@ -322,12 +325,25 @@ public class ChatManager extends BasicListener { ChatChannel previous = defaults.put(uuid, channel); if (announced.add(uuid)) { - player.system("CHAT_CHANNEL_CURRENT", channel.tag(player)); + notifyDefault(player, channel); } else if (previous != channel && !player.parseToPlain(previous.tag(player)).equals(player.parseToPlain(channel.tag(player)))) { - player.system("CHAT_CHANNEL_SWITCHED", channel.tag(player)); + notifyDefault(player, channel); } } + /** + * Tells the player which channel they write into, showing the channel icon with its name on hover. + */ + public static void notifyDefault(PlayerChatter player, ChatChannel channel) { + Message icon = channel.icon(player); + Component iconComponent = icon == null ? player.parse(channel.tag(player)) : player.parse(new Message("CHAT_ICON", "§a", icon)); + player.sendMessage(Component.text() + .append(iconComponent.hoverEvent(HoverEvent.showText(player.parse(channel.tag(player))))) + .append(Component.text(" ")) + .append(player.parse(new Message("CHAT_CHANNEL_DEFAULT"))) + .build()); + } + /** * Handles a chat message the player typed without a command. */ diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java index cd563a81..b7a5bb05 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java @@ -47,7 +47,7 @@ public class ChatCommand extends SWCommand { @Register(description = "CHAT_USAGE") public void current(PlayerChatter sender) { - sender.system("CHAT_CHANNEL_CURRENT", ChatManager.getDefault(sender).tag(sender)); + ChatManager.notifyDefault(sender, ChatManager.getDefault(sender)); } @Register From 4c758799155b69303ffea31080bd0ee0f338d5c4 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 09:07:13 +0200 Subject: [PATCH 11/43] Keep spectators in fight chat and treat spectate servers as fight servers - '+' from a spectator on a fight server stays in the fight chat - Event spectate servers (static servers with a spectate port) use a local fight chat by default and hide the global chat like arenas --- .../src/de/steamwar/velocitycore/EventStarter.java | 11 ++++++++++- .../src/de/steamwar/velocitycore/VelocityCore.java | 2 +- .../de/steamwar/velocitycore/chat/ChatManager.java | 10 +++++++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java b/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java index 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/ChatManager.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java index 95359244..24082e65 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -113,8 +113,11 @@ public class ChatManager extends BasicListener { return VelocityCore.getProxy().getServer(server.getServerInfo().getName()).orElse(null) != server; } + /** + * @return true for arenas and event spectate servers, which use a fight chat instead of the global chat + */ public static boolean isArena(RegisteredServer server) { - return Subserver.isArena(Subserver.getSubserver(server.getServerInfo())); + return Subserver.isArena(Subserver.getSubserver(server.getServerInfo())) || EventStarter.isSpectateServer(server.getServerInfo().getName()); } private static Optional currentServer(Player player) { @@ -371,14 +374,15 @@ public class ChatManager extends BasicListener { RegisteredServer server = currentServer(sender.getPlayer()).orElse(null); if (server == null) return current; - if (current instanceof LocalChannel) { + if (current instanceof LocalChannel local) { FightTeamChannel blue = fightTeam(server, true); if (blue.isMember(sender)) return blue; FightTeamChannel red = fightTeam(server, false); if (red.isMember(sender)) return red; - return global; + // Spectators on fight servers stay in the fight chat + return local.isArena() ? local : global; } return local(server); From 904a850a3ff9070d07f7e216efebfbe29a37a84e Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 09:16:35 +0200 Subject: [PATCH 12/43] Remove duplicated chat channel logic - Shared send-or-switch helper for /global, /local, /tc and /stc - Shared helpers for "is on server" and the player's current local channel - Read fight infos from TablistManager instead of keeping a second map - /chat resolves fight servers (arenas and spectate servers) by name in one place - Reuse the global and participant formats instead of identical keys --- .../steamwar/messages/BungeeCore.properties | 3 -- .../messages/BungeeCore_de.properties | 3 -- .../velocitycore/chat/ChatFormat.java | 4 +- .../velocitycore/chat/ChatManager.java | 42 ++++++++++++++++--- .../velocitycore/chat/FightTeamChannel.java | 6 +-- .../velocitycore/chat/LocalChannel.java | 17 ++++---- .../velocitycore/commands/ChatCommand.java | 22 ++++------ .../velocitycore/commands/GlobalCommand.java | 9 +--- .../velocitycore/commands/LocalCommand.java | 9 +--- .../commands/ServerTeamchatCommand.java | 9 +--- .../commands/TeamchatCommand.java | 11 +---- .../network/handlers/FightInfoHandler.java | 4 +- .../velocitycore/tablist/TablistManager.java | 12 +++++- 13 files changed, 74 insertions(+), 77 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index 36fdc284..6dee34b2 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -578,8 +578,6 @@ CHAT_SERVERTEAM_SENDER = §e{0} CHAT_SERVERTEAM = §8» §f{2} CHAT_GLOBAL_SENDER = {3}{4}{5}{6}{0} CHAT_GLOBAL = §8» {7}{2} -CHAT_LOCAL_SENDER = {3}{4}{5}{6}{0} -CHAT_LOCAL = §8» {7}{2} CHAT_TEAM_SENDER = §e{0} CHAT_TEAM = §8» §f{2} CHAT_MSG_SENDER = §e{0} @@ -591,7 +589,6 @@ CHAT_FIGHT_SPECTATOR_SENDER = §7{1} CHAT_FIGHT_SPECTATOR = §8» §7{0} CHAT_FIGHT_REFEREE_SENDER = §e{1} CHAT_FIGHT_REFEREE = §8» §e{0} -CHAT_FIGHT_TEAM_SENDER = {0}{1} CHAT_FIGHT_TEAM = §8» §f{0} CHAT_ICON = {0}{1} CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Prefix: §r{1}{2} diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 0c54dccc..7f930eac 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -543,8 +543,6 @@ CHAT_SERVERTEAM_SENDER = §e{0} CHAT_SERVERTEAM = §8» §f{2} CHAT_GLOBAL_SENDER = {3}{4}{5}{6}{0} CHAT_GLOBAL = §8» {7}{2} -CHAT_LOCAL_SENDER = {3}{4}{5}{6}{0} -CHAT_LOCAL = §8» {7}{2} CHAT_TEAM_SENDER = §e{0} CHAT_TEAM = §8» §f{2} CHAT_MSG_SENDER = §e{0} @@ -556,7 +554,6 @@ CHAT_FIGHT_SPECTATOR_SENDER = §7{1} CHAT_FIGHT_SPECTATOR = §8» §7{0} CHAT_FIGHT_REFEREE_SENDER = §e{1} CHAT_FIGHT_REFEREE = §8» §e{0} -CHAT_FIGHT_TEAM_SENDER = {0}{1} CHAT_FIGHT_TEAM = §8» §f{0} CHAT_ICON = {0}{1} CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Kürzel: §r{1}{2} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java index ad38e020..f5b945dc 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java @@ -76,7 +76,7 @@ public class ChatFormat { * * @param coloredTeamName the fight team of the sender, null if the sender is not in a fight team */ - public static Component fight(Chatter receiver, RegisteredServer server, Chatter sender, String coloredTeamName, String format, String message) { + public static Component fight(Chatter receiver, RegisteredServer server, Chatter sender, String coloredTeamName, String senderFormat, String format, String message) { TextComponent.Builder line = Component.text(); String nameColor = ""; if (coloredTeamName != null) { @@ -89,7 +89,7 @@ public class ChatFormat { nameColor = coloredTeamName.length() >= 2 && coloredTeamName.charAt(0) == '§' ? coloredTeamName.substring(0, 2) : ""; } - line.append(withTeamHover(receiver, sender.user(), receiver.parse(new Message(format + "_SENDER", nameColor, sender.user())))); + line.append(withTeamHover(receiver, sender.user(), receiver.parse(new Message(senderFormat, nameColor, sender.user())))); line.append(receiver.parse(new Message(format, message))); return line.build(); } diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java index 24082e65..a88b55c3 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -41,6 +41,7 @@ import de.steamwar.sql.UserPerm; import de.steamwar.velocitycore.EventStarter; import de.steamwar.velocitycore.VelocityCore; import de.steamwar.velocitycore.listeners.BasicListener; +import de.steamwar.velocitycore.tablist.TablistManager; import lombok.Getter; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.HoverEvent; @@ -68,7 +69,6 @@ public class ChatManager extends BasicListener { private static final Map locals = new ConcurrentHashMap<>(); private static final Map fightTeams = new ConcurrentHashMap<>(); - private static final Map fightInfos = new ConcurrentHashMap<>(); private static final Map teams = new ConcurrentHashMap<>(); private static final Set directs = ConcurrentHashMap.newKeySet(); @@ -120,6 +120,38 @@ public class ChatManager extends BasicListener { return Subserver.isArena(Subserver.getSubserver(server.getServerInfo())) || EventStarter.isSpectateServer(server.getServerInfo().getName()); } + /** + * @return all servers using a fight chat (arenas and event spectate servers) + */ + public static Stream fightServers() { + return VelocityCore.getProxy().getAllServers().stream().filter(ChatManager::isArena); + } + + /** + * @return the local channel of the server the player is on, null while the player is on no server + */ + public static LocalChannel currentLocal(Player player) { + return currentServer(player).map(ChatManager::local).orElse(null); + } + + static boolean onServer(Chatter chatter, RegisteredServer server) { + Player player = chatter.getPlayer(); + return player != null && server.getPlayersConnected().contains(player); + } + + /** + * Sends the message into the channel or makes it the default channel of the player if there is no message. + */ + public static void sendOrSwitch(Chatter sender, ChatChannel channel, String[] message, String usage) { + if (message.length > 0) { + channel.send(sender, String.join(" ", message)); + } else if (sender instanceof PlayerChatter player) { + switchTo(player, channel); + } else { + sender.system(usage); + } + } + private static Optional currentServer(Player player) { return player.getCurrentServer().map(ServerConnection::getServer); } @@ -186,7 +218,7 @@ public class ChatManager extends BasicListener { * @return true if the channel is the fight chat of an arena the player is not on, which moderators can read without joining the arena */ private static boolean isForeignArena(PlayerChatter player, ChatChannel channel) { - return channel instanceof LocalChannel local && local.isArena() && !local.getServer().getPlayersConnected().contains(player.getPlayer()); + return channel instanceof LocalChannel local && local.isArena() && !onServer(player, local.getServer()); } public static void join(PlayerChatter player, ChatChannel channel) { @@ -225,7 +257,7 @@ public class ChatManager extends BasicListener { } public static FightInfoPacket getFightInfo(RegisteredServer server) { - return fightInfos.get(server); + return TablistManager.getFightInfo(server); } /** @@ -248,9 +280,7 @@ public class ChatManager extends BasicListener { /** * Updates the fight teams of an arena. Players who just joined a fight team write into the fight chat by default. */ - public static void updateFightInfo(RegisteredServer server, FightInfoPacket packet) { - fightInfos.keySet().removeIf(s -> s.getPlayersConnected().isEmpty()); - FightInfoPacket previous = fightInfos.put(server, packet); + public static void updateFightInfo(RegisteredServer server, FightInfoPacket previous, FightInfoPacket packet) { Set joined = new HashSet<>(packet.getBluePlayers()); joined.addAll(packet.getRedPlayers()); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java index 869d7c24..4e018ddd 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java @@ -77,7 +77,7 @@ public class FightTeamChannel extends ChatChannel { protected Message icon(Chatter receiver) { String teamName = teamName(); String color = teamName.length() >= 2 && teamName.charAt(0) == '§' ? teamName.substring(0, 2) : "§7"; - if (receiver.getPlayer() != null && server.getPlayersConnected().contains(receiver.getPlayer())) { + if (ChatManager.onServer(receiver, server)) { return new Message("CHAT_ICON_FIGHT_TEAM", color); } return new Message("CHAT_ICON_FIGHT_TEAM_FOREIGN", color, server.getServerInfo().getName()); @@ -93,7 +93,7 @@ public class FightTeamChannel extends ChatChannel { @Override public boolean canWrite(Chatter sender) { - if (!isMember(sender) || sender.getPlayer() == null || !server.getPlayersConnected().contains(sender.getPlayer())) { + if (!isMember(sender) || !ChatManager.onServer(sender, server)) { sender.system("CHAT_FIGHT_NO_TEAM"); return false; } @@ -102,7 +102,7 @@ public class FightTeamChannel extends ChatChannel { @Override protected Component format(Chatter receiver, Chatter sender, String message) { - return ChatFormat.fight(receiver, server, sender, teamName(), "CHAT_FIGHT_TEAM", ChatFormat.highlightMentions(message, "§f", receiver)); + return ChatFormat.fight(receiver, server, sender, teamName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_TEAM", ChatFormat.highlightMentions(message, "§f", receiver)); } @Override diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java index 12aece2a..b4e4892d 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java @@ -68,7 +68,7 @@ public class LocalChannel extends ChatChannel { if (!isArena()) return new Message("CHAT_ICON_LOCAL"); // Moderators reading an arena they are not on need to know which arena it is - if (receiver.getPlayer() != null && server.getPlayersConnected().contains(receiver.getPlayer())) { + if (ChatManager.onServer(receiver, server)) { return new Message("CHAT_ICON_FIGHT"); } return new Message("CHAT_ICON_FIGHT_FOREIGN", server.getServerInfo().getName()); @@ -85,9 +85,8 @@ public class LocalChannel extends ChatChannel { @Override public boolean canWrite(Chatter sender) { - boolean onServer = sender.getPlayer() != null && server.getPlayersConnected().contains(sender.getPlayer()); - // Moderators can write into fight chats they are not on (/chatspy) - if (!onServer && !(isArena() && sender.user().hasPerm(UserPerm.MODERATION))) { + // Moderators can write into fight chats they are not on (/chat msg) + if (!ChatManager.onServer(sender, server) && !(isArena() && sender.user().hasPerm(UserPerm.MODERATION))) { sender.system("CHAT_NO_WRITE"); return false; } @@ -97,20 +96,20 @@ public class LocalChannel extends ChatChannel { @Override protected Component format(Chatter receiver, Chatter sender, String message) { if (!isArena()) { - return ChatFormat.player("CHAT_LOCAL", receiver, sender, null, message); + return ChatFormat.player("CHAT_GLOBAL", receiver, sender, null, message); } String highlighted = ChatFormat.highlightMentions(message, "§7", receiver); FightInfoPacket info = ChatManager.getFightInfo(server); int id = sender.user().getId(); if (info != null && info.getBluePlayers().contains(id)) { - return ChatFormat.fight(receiver, server, sender, info.getBlueName(), "CHAT_FIGHT_PARTICIPANT", highlighted); + return ChatFormat.fight(receiver, server, sender, info.getBlueName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_PARTICIPANT", highlighted); } else if (info != null && info.getRedPlayers().contains(id)) { - return ChatFormat.fight(receiver, server, sender, info.getRedName(), "CHAT_FIGHT_PARTICIPANT", highlighted); + return ChatFormat.fight(receiver, server, sender, info.getRedName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_PARTICIPANT", highlighted); } else if (ChatManager.isReferee(server, id)) { - return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_REFEREE", highlighted); + return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_REFEREE_SENDER", "CHAT_FIGHT_REFEREE", highlighted); } - return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_SPECTATOR", highlighted); + return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_SPECTATOR_SENDER", "CHAT_FIGHT_SPECTATOR", highlighted); } @Override diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java index b7a5bb05..23e5ea69 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java @@ -20,17 +20,13 @@ package de.steamwar.velocitycore.commands; -import com.velocitypowered.api.proxy.ServerConnection; -import com.velocitypowered.api.proxy.server.RegisteredServer; import de.steamwar.command.PreviousArguments; import de.steamwar.command.SWCommand; import de.steamwar.command.TypeMapper; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; import de.steamwar.messages.PlayerChatter; -import de.steamwar.persistent.Subserver; import de.steamwar.sql.UserPerm; -import de.steamwar.velocitycore.VelocityCore; import de.steamwar.velocitycore.chat.ChatChannel; import de.steamwar.velocitycore.chat.ChatManager; @@ -81,7 +77,7 @@ public class ChatCommand extends SWCommand { public ChatChannel map(Chatter sender, PreviousArguments previousArguments, String s) { return switch (s.toLowerCase()) { case "global" -> ChatManager.getGlobal(); - case "local" -> sender.getPlayer() == null ? null : sender.getPlayer().getCurrentServer().map(ServerConnection::getServer).map(ChatManager::local).orElse(null); + case "local" -> sender.getPlayer() == null ? null : ChatManager.currentLocal(sender.getPlayer()); case "team", "tc" -> sender.user().getTeam() != 0 ? ChatManager.team(sender.user().getTeam()) : null; case "serverteam", "stc" -> sender.user().hasPerm(UserPerm.TEAM) ? ChatManager.getServerTeam() : null; default -> arena(sender, s); @@ -99,10 +95,7 @@ public class ChatCommand extends SWCommand { if (sender.user().getTeam() != 0) channels.add("team"); if (sender.user().hasPerm(UserPerm.TEAM)) channels.add("serverteam"); if (sender.user().hasPerm(UserPerm.MODERATION)) { - List subserverList = Subserver.getServerList(); - synchronized (subserverList) { - subserverList.stream().filter(Subserver::isArena).map(subserver -> subserver.getServer().getName()).forEach(channels::add); - } + ChatManager.fightServers().map(server -> server.getServerInfo().getName()).forEach(channels::add); } return channels; } @@ -110,14 +103,15 @@ public class ChatCommand extends SWCommand { } /** - * Moderators can address the fight chat of every arena by its server name. + * Moderators can address the fight chat of every fight server by its server name. */ private static ChatChannel arena(Chatter sender, String name) { if (!sender.user().hasPerm(UserPerm.MODERATION)) return null; - Subserver subserver = Subserver.getSubserver(VelocityCore.getProxy().getServer(name).map(RegisteredServer::getServerInfo).orElse(null)); - if (!Subserver.isArena(subserver)) return null; - - return ChatManager.local(subserver.getRegisteredServer()); + return ChatManager.fightServers() + .filter(server -> server.getServerInfo().getName().equalsIgnoreCase(name)) + .findFirst() + .map(ChatManager::local) + .orElse(null); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java index 6ce66d43..489fa83b 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java @@ -23,7 +23,6 @@ package de.steamwar.velocitycore.commands; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; -import de.steamwar.messages.PlayerChatter; import de.steamwar.velocitycore.chat.ChatManager; @Linked @@ -35,12 +34,6 @@ public class GlobalCommand extends SWCommand { @Register(description = "GLOBAL_USAGE") public void genericCommand(Chatter sender, String... message) { - if (message.length > 0) { - ChatManager.getGlobal().send(sender, String.join(" ", message)); - } else if (sender instanceof PlayerChatter player) { - ChatManager.switchTo(player, ChatManager.getGlobal()); - } else { - sender.system("GLOBAL_USAGE"); - } + ChatManager.sendOrSwitch(sender, ChatManager.getGlobal(), message, "GLOBAL_USAGE"); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java index ab52364d..b52a014f 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java @@ -19,7 +19,6 @@ package de.steamwar.velocitycore.commands; -import com.velocitypowered.api.proxy.ServerConnection; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.PlayerChatter; @@ -35,13 +34,9 @@ public class LocalCommand extends SWCommand { @Register(description = "CHAT_BC_USAGE") public void genericCommand(PlayerChatter sender, String... message) { - ChatChannel channel = sender.getPlayer().getCurrentServer().map(ServerConnection::getServer).map(ChatManager::local).orElse(null); + ChatChannel channel = ChatManager.currentLocal(sender.getPlayer()); if (channel == null) return; - if (message.length == 0) { - ChatManager.switchTo(sender, channel); - } else { - channel.send(sender, String.join(" ", message)); - } + ChatManager.sendOrSwitch(sender, channel, message, "CHAT_BC_USAGE"); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java index ea3932b4..024c118d 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java @@ -22,7 +22,6 @@ package de.steamwar.velocitycore.commands; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; -import de.steamwar.messages.PlayerChatter; import de.steamwar.sql.UserPerm; import de.steamwar.velocitycore.chat.ChatManager; @@ -35,12 +34,6 @@ public class ServerTeamchatCommand extends SWCommand { @Register(description = "STC_USAGE") public void genericCommand(Chatter sender, String... message) { - if (message.length > 0) { - ChatManager.getServerTeam().send(sender, String.join(" ", message)); - } else if (sender instanceof PlayerChatter player) { - ChatManager.switchTo(player, ChatManager.getServerTeam()); - } else { - sender.system("STC_USAGE"); - } + ChatManager.sendOrSwitch(sender, ChatManager.getServerTeam(), message, "STC_USAGE"); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java index 1f6d8458..39de5457 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java @@ -22,8 +22,6 @@ package de.steamwar.velocitycore.commands; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; -import de.steamwar.messages.PlayerChatter; -import de.steamwar.velocitycore.chat.ChatChannel; import de.steamwar.velocitycore.chat.ChatManager; @Linked @@ -41,13 +39,6 @@ public class TeamchatCommand extends SWCommand { return; } - ChatChannel channel = ChatManager.team(team); - if (args.length > 0) { - channel.send(sender, String.join(" ", args)); - } else if (sender instanceof PlayerChatter player) { - ChatManager.switchTo(player, channel); - } else { - sender.system("TC_USAGE"); - } + ChatManager.sendOrSwitch(sender, ChatManager.team(team), args, "TC_USAGE"); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java index e14f10c2..b7ad0d19 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java +++ b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java @@ -51,8 +51,8 @@ public class FightInfoHandler extends PacketHandler { FightInfoPacket lobbyPacket = packet.withServerName(server.getServerInfo().getName()); - TablistManager.newFightInfo(server, packet); - ChatManager.updateFightInfo(server, packet); + FightInfoPacket previous = TablistManager.newFightInfo(server, packet); + ChatManager.updateFightInfo(server, previous, packet); lobbys.removeIf(s -> s.getPlayersConnected().isEmpty()); lobbys.forEach(lobby -> NetworkSender.send(lobby, lobbyPacket)); diff --git a/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java index 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<>(); From be915770b16e564866859197002f23b6cba0bc7b Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 09:45:37 +0200 Subject: [PATCH 13/43] Show server name in local channel hover --- .../src/de/steamwar/messages/BungeeCore.properties | 1 + .../src/de/steamwar/messages/BungeeCore_de.properties | 1 + .../src/de/steamwar/velocitycore/chat/ChatChannel.java | 9 ++++++++- .../src/de/steamwar/velocitycore/chat/ChatManager.java | 2 +- .../src/de/steamwar/velocitycore/chat/LocalChannel.java | 5 +++++ 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index 2f4f87a0..bb133b93 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -591,6 +591,7 @@ 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_HOVER_LOCAL = {0}\n§7Server: §e{1} CHAT_ICON_GLOBAL = 🌍 CHAT_ICON_LOCAL = ⌂ CHAT_ICON_FIGHT = ⚔ diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 2cedc5b1..e7f26c99 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -556,6 +556,7 @@ 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_HOVER_LOCAL = {0}\n§7Server: §e{1} CHAT_ICON_GLOBAL = 🌍 CHAT_ICON_LOCAL = ⌂ CHAT_ICON_FIGHT = ⚔ diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java index 9e8e5861..c560c12c 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java @@ -49,6 +49,13 @@ public abstract class ChatChannel { */ public abstract Message tag(Chatter receiver); + /** + * @return the text shown when hovering the channel icon + */ + public Message hover(Chatter receiver) { + return tag(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. @@ -103,7 +110,7 @@ public abstract class ChatChannel { 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)))); + .hoverEvent(HoverEvent.showText(receiver.parse(hover(receiver)))); msg = Component.text().append(iconComponent).append(Component.text(" ")).append(msg).build(); } receiver.sendMessage(msg); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java index a88b55c3..bcdabf7e 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -371,7 +371,7 @@ public class ChatManager extends BasicListener { 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(iconComponent.hoverEvent(HoverEvent.showText(player.parse(channel.hover(player))))) .append(Component.text(" ")) .append(player.parse(new Message("CHAT_CHANNEL_DEFAULT"))) .build()); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java index b4e4892d..a32d3f23 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java @@ -63,6 +63,11 @@ public class LocalChannel extends ChatChannel { return new Message("CHAT_CHANNEL_LOCAL"); } + @Override + public Message hover(Chatter receiver) { + return new Message("CHAT_HOVER_LOCAL", tag(receiver), server.getServerInfo().getName()); + } + @Override protected Message icon(Chatter receiver) { if (!isArena()) return new Message("CHAT_ICON_LOCAL"); From 8a86ebde83772982acca2866c49d3ff5969c9751 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 10:49:10 +0200 Subject: [PATCH 14/43] Make DM names hoverable and channel icons clickable - Both names in direct messages show their team on hover - Clicking a channel icon suggests the command to write a single message into that channel --- .../steamwar/messages/BungeeCore.properties | 4 ++- .../messages/BungeeCore_de.properties | 4 ++- .../velocitycore/chat/ChatChannel.java | 31 ++++++++++++++++--- .../velocitycore/chat/ChatManager.java | 5 +-- .../velocitycore/chat/DirectChannel.java | 14 ++++++++- .../velocitycore/chat/FightTeamChannel.java | 6 ++++ .../velocitycore/chat/GlobalChannel.java | 5 +++ .../velocitycore/chat/LocalChannel.java | 8 +++++ .../velocitycore/chat/ServerTeamChannel.java | 5 +++ .../velocitycore/chat/TeamChannel.java | 5 +++ 10 files changed, 75 insertions(+), 12 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index bb133b93..789ccced 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -580,7 +580,9 @@ 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_MSG_SEPARATOR = §8» +CHAT_MSG_RECEIVER = §e{0} +CHAT_MSG = §7{0} CHAT_SPECIAL_ALERT = §e{0}§8»§e{1} §7{2} CHAT_FIGHT_PARTICIPANT_SENDER = {0}{1} CHAT_FIGHT_PARTICIPANT = §8» §7{0} diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index e7f26c99..99bcd3d3 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -545,7 +545,9 @@ 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_MSG_SEPARATOR = §8» +CHAT_MSG_RECEIVER = §e{0} +CHAT_MSG = §7{0} CHAT_SPECIAL_ALERT = §e{0}§8»§e{1} §7{2} CHAT_FIGHT_PARTICIPANT_SENDER = {0}{1} CHAT_FIGHT_PARTICIPANT = §8» §7{0} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java index c560c12c..51ccc0d2 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java @@ -25,6 +25,7 @@ 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.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; import java.util.LinkedHashMap; @@ -64,6 +65,28 @@ public abstract class ChatChannel { return null; } + /** + * @return the text put into the chat input when clicking the channel icon to write a single message, null if there is none + */ + protected String suggestion(Chatter receiver) { + return null; + } + + /** + * @return the channel icon with its name on hover and its write command on click + */ + Component iconComponent(Chatter receiver, String color) { + Message icon = icon(receiver); + Component component = icon == null ? receiver.parse(tag(receiver)) : receiver.parse(new Message("CHAT_ICON", color, icon)); + component = component.hoverEvent(HoverEvent.showText(receiver.parse(hover(receiver)))); + + String suggestion = suggestion(receiver); + if (suggestion != null) { + component = component.clickEvent(ClickEvent.suggestCommand(suggestion)); + } + return component; + } + public abstract Stream readers(); /** @@ -107,11 +130,9 @@ public abstract class ChatChannel { 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(hover(receiver)))); - msg = Component.text().append(iconComponent).append(Component.text(" ")).append(msg).build(); + if (icon(receiver) != null) { + Component icon = iconComponent(receiver, ChatManager.getDefault(receiver) == this ? "§e" : "§7"); + msg = Component.text().append(icon).append(Component.text(" ")).append(msg).build(); } receiver.sendMessage(msg); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java index bcdabf7e..199cda66 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -44,7 +44,6 @@ 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; @@ -368,10 +367,8 @@ public class ChatManager extends BasicListener { * 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.hover(player))))) + .append(channel.iconComponent(player, "§a")) .append(Component.text(" ")) .append(player.parse(new Message("CHAT_CHANNEL_DEFAULT"))) .build()); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java index f0a79923..5b89b8ba 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java @@ -70,6 +70,11 @@ public class DirectChannel extends ChatChannel { return new Message("CHAT_ICON_MSG"); } + @Override + protected String suggestion(Chatter receiver) { + return "/msg " + SteamwarUser.get(partner(receiver.user().getUUID())).getUserName() + " "; + } + @Override public Stream readers() { return Stream.of(first, second) @@ -95,7 +100,14 @@ public class DirectChannel extends ChatChannel { @Override protected Component format(Chatter receiver, Chatter sender, String message) { - return ChatFormat.player("CHAT_MSG", receiver, sender, Chatter.of(partner(sender.user().getUUID())), message); + SteamwarUser partner = SteamwarUser.get(partner(sender.user().getUUID())); + return Component.text() + .append(ChatFormat.withTeamHover(receiver, sender.user(), receiver.parse(new Message("CHAT_MSG_SENDER", sender.user())))) + .append(receiver.parse(new Message("CHAT_MSG_SEPARATOR"))) + .append(ChatFormat.withTeamHover(receiver, partner, receiver.parse(new Message("CHAT_MSG_RECEIVER", partner)))) + .append(Component.text(" ")) + .append(receiver.parse(new Message("CHAT_MSG", ChatFormat.highlightMentions(message, "§7", receiver)))) + .build(); } @Override diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java index 4e018ddd..49b75cfa 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java @@ -83,6 +83,12 @@ public class FightTeamChannel extends ChatChannel { return new Message("CHAT_ICON_FIGHT_TEAM_FOREIGN", color, server.getServerInfo().getName()); } + @Override + protected String suggestion(Chatter receiver) { + // Messages starting with '+' go into the fight team chat while writing into the fight chat + return ChatManager.onServer(receiver, server) ? "+" : null; + } + @Override public Stream readers() { List members = members(); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java index cc2219cd..67730da7 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java @@ -44,6 +44,11 @@ public class GlobalChannel extends ChatChannel { return new Message("CHAT_ICON_GLOBAL"); } + @Override + protected String suggestion(Chatter receiver) { + return "/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()))); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java index a32d3f23..d3a3e41c 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java @@ -79,6 +79,14 @@ public class LocalChannel extends ChatChannel { return new Message("CHAT_ICON_FIGHT_FOREIGN", server.getServerInfo().getName()); } + @Override + protected String suggestion(Chatter receiver) { + if (isArena() && !ChatManager.onServer(receiver, server)) { + return "/chat " + server.getServerInfo().getName() + " msg "; + } + return "/local "; + } + @Override public Stream readers() { Stream players = server.getPlayersConnected().stream().map(Chatter::of).filter(chatter -> ChatManager.reads(chatter, persistId())).map(Chatter.class::cast); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java index 1860633d..1e23d787 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java @@ -48,6 +48,11 @@ public class ServerTeamChannel extends ChatChannel { return new Message("CHAT_ICON_SERVERTEAM"); } + @Override + protected String suggestion(Chatter receiver) { + return "/stc "; + } + @Override public Stream readers() { return Arrays.stream(Chatter.serverteam().getChatters()).filter(chatter -> ChatManager.reads(chatter, persistId())); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java index f362099b..4603c3d5 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java @@ -54,6 +54,11 @@ public class TeamChannel extends ChatChannel { return new Message("CHAT_ICON_TEAM"); } + @Override + protected String suggestion(Chatter receiver) { + return "/tc "; + } + @Override public Stream readers() { return Chatter.allPlayers() From 503261ce6d67df306c9b3764f8917b7a80f5b25e Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 10:51:29 +0200 Subject: [PATCH 15/43] Name team chat channels TeamChat and ServerTeamChat --- VelocityCore/src/de/steamwar/messages/BungeeCore.properties | 4 ++-- .../src/de/steamwar/messages/BungeeCore_de.properties | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index 789ccced..5ebb84b9 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -606,8 +606,8 @@ 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_TEAM = §eTeamChat +CHAT_CHANNEL_SERVERTEAM = §eServerTeamChat CHAT_CHANNEL_MSG = §eMSG §7{0} CHAT_CHANNEL_ALERT = §cAlert CHAT_CHANNEL_LOCAL = §eLocal diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 99bcd3d3..9c889212 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -571,8 +571,8 @@ 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_TEAM = §eTeamChat +CHAT_CHANNEL_SERVERTEAM = §eServerTeamChat CHAT_CHANNEL_MSG = §eMSG §7{0} CHAT_CHANNEL_ALERT = §cAlert CHAT_CHANNEL_LOCAL = §eLokal From 39cacbd701377984e9489fcbeb03ed38359a5a00 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 10:53:01 +0200 Subject: [PATCH 16/43] Show team on hover for @name mentions --- .../steamwar/velocitycore/chat/ChatChannel.java | 2 +- .../steamwar/velocitycore/chat/ChatFormat.java | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java index 51ccc0d2..4bc9d919 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java @@ -129,7 +129,7 @@ public abstract class ChatChannel { for (Chatter receiver : receivers.values()) { if (!receiver.chatShown()) continue; - Component msg = format(receiver, sender, coloredMessage); + Component msg = ChatFormat.withMentionHovers(receiver, format(receiver, sender, coloredMessage)); if (icon(receiver) != null) { Component icon = iconComponent(receiver, ChatManager.getDefault(receiver) == this ? "§e" : "§7"); msg = Component.text().append(icon).append(Component.text(" ")).append(msg).build(); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java index f5b945dc..8f4a477e 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java @@ -31,13 +31,17 @@ 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.TextReplacementConfig; import net.kyori.adventure.text.event.HoverEvent; import java.util.Arrays; +import java.util.regex.Pattern; import java.util.stream.Collectors; public class ChatFormat { + private static final Pattern MENTION = Pattern.compile("@(\\w{1,16})"); + private ChatFormat() {} /** @@ -126,6 +130,19 @@ public class ChatFormat { return SteamwarPrefix.usesSWPrefix(user); } + /** + * Shows the team of every mentioned player ({@code @name}) on hover, like at the sender name. + */ + public static Component withMentionHovers(Chatter receiver, Component component) { + return component.replaceText(TextReplacementConfig.builder() + .match(MENTION) + .replacement((match, builder) -> { + SteamwarUser user = SteamwarUser.get(match.group(1)); + return user == null ? builder : withTeamHover(receiver, user, builder.build()); + }) + .build()); + } + public static String highlightMentions(String message, String returnColor, Chatter player) { if (!message.contains("@")) return message; From 362e1cebfff8a6cb0bcb80f406d3146d2d86cb1f Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 10:55:01 +0200 Subject: [PATCH 17/43] Name the fight channel Fight The server name is already shown in the second line of the hover. --- VelocityCore/src/de/steamwar/messages/BungeeCore.properties | 2 +- VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties | 2 +- .../src/de/steamwar/velocitycore/chat/LocalChannel.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index 5ebb84b9..d3d2d88f 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -604,7 +604,7 @@ CHAT_ICON_TEAM = 👥 CHAT_ICON_SERVERTEAM = ⚙ CHAT_ICON_MSG = ✉ CHAT_CHANNEL_GLOBAL = §eGlobal -CHAT_CHANNEL_ARENA = §e{0} +CHAT_CHANNEL_ARENA = §eFight CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} CHAT_CHANNEL_TEAM = §eTeamChat CHAT_CHANNEL_SERVERTEAM = §eServerTeamChat diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 9c889212..e27d3265 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -569,7 +569,7 @@ CHAT_ICON_TEAM = 👥 CHAT_ICON_SERVERTEAM = ⚙ CHAT_ICON_MSG = ✉ CHAT_CHANNEL_GLOBAL = §eGlobal -CHAT_CHANNEL_ARENA = §e{0} +CHAT_CHANNEL_ARENA = §eFight CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} CHAT_CHANNEL_TEAM = §eTeamChat CHAT_CHANNEL_SERVERTEAM = §eServerTeamChat diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java index d3a3e41c..e4d6c131 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java @@ -58,7 +58,7 @@ public class LocalChannel extends ChatChannel { @Override public Message tag(Chatter receiver) { if (isArena()) { - return new Message("CHAT_CHANNEL_ARENA", server.getServerInfo().getName()); + return new Message("CHAT_CHANNEL_ARENA"); } return new Message("CHAT_CHANNEL_LOCAL"); } From 7f47d687d333100fea15f1aa7af53caead68c172 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 11:01:23 +0200 Subject: [PATCH 18/43] Hide /chat msg from players without moderation permission --- .../de/steamwar/messages/BungeeCore.properties | 3 ++- .../de/steamwar/messages/BungeeCore_de.properties | 3 ++- .../velocitycore/commands/ChatCommand.java | 15 ++++++++------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index d3d2d88f..0dfd9688 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -616,7 +616,8 @@ 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_USAGE = §8/§7chat §8[§echannel§8] [§ejoin§8|§eleave§8] +CHAT_MSG_USAGE = §8/§7chat §8[§echannel§8] §7msg §8[§emessage§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. diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index e27d3265..73c8273c 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -581,7 +581,8 @@ CHAT_CHANNEL_NOT_SWITCHABLE = §cIn diesen Kanal kannst du nicht standardmäßig 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_USAGE = §8/§7chat §8[§eKanal§8] [§ejoin§8|§eleave§8] +CHAT_MSG_USAGE = §8/§7chat §8[§eKanal§8] §7msg §8[§eNachricht§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. diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java index 23e5ea69..55fb5d9c 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java @@ -23,6 +23,7 @@ package de.steamwar.velocitycore.commands; import de.steamwar.command.PreviousArguments; import de.steamwar.command.SWCommand; import de.steamwar.command.TypeMapper; +import de.steamwar.command.TypeValidator; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; import de.steamwar.messages.PlayerChatter; @@ -60,16 +61,16 @@ public class ChatCommand extends SWCommand { } } - @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; - } - + @Register(description = "CHAT_MSG_USAGE") + public void msg(@Validator("moderator") PlayerChatter sender, ChatChannel channel, @StaticValue("msg") String msg, @ErrorMessage(value = "CHAT_MSG_USAGE", allowEAs = false) String... message) { channel.send(sender, String.join(" ", message)); } + @Validator(value = "moderator", local = true) + public TypeValidator moderatorValidator() { + return (sender, value, messageSender) -> !messageSender.send(!value.user().hasPerm(UserPerm.MODERATION), "CHAT_NO_WRITE"); + } + @ClassMapper(value = ChatChannel.class, local = true) public TypeMapper channelTypeMapper() { return new TypeMapper<>() { From 62850b2e11d37916d3bc5f88a1ee568b3d569fbf Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 11:07:05 +0200 Subject: [PATCH 19/43] Let moderators read all fight chats automatically /chat arenas join|leave lets moderators read the fight and team chats of every arena and event spectate server; the choice is saved across relogs. /chat leave excludes a single arena for the session. --- .../steamwar/messages/BungeeCore.properties | 3 ++ .../messages/BungeeCore_de.properties | 3 ++ .../velocitycore/chat/ChatManager.java | 48 ++++++++++++++++--- .../velocitycore/commands/ChatCommand.java | 5 ++ 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index 0dfd9688..dc41683f 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -618,6 +618,9 @@ 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] CHAT_MSG_USAGE = §8/§7chat §8[§echannel§8] §7msg §8[§emessage§8] +CHAT_ARENAS_USAGE = §8/§7chat §7arenas §8[§ejoin§8|§eleave§8] +CHAT_ARENAS_JOINED = §7You now read the chats of all fights. +CHAT_ARENAS_LEFT = §7You no longer read the chats of all fights. 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. diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 73c8273c..b0c70a21 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -583,6 +583,9 @@ 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] CHAT_MSG_USAGE = §8/§7chat §8[§eKanal§8] §7msg §8[§eNachricht§8] +CHAT_ARENAS_USAGE = §8/§7chat §7arenas §8[§ejoin§8|§eleave§8] +CHAT_ARENAS_JOINED = §7Du liest jetzt die Chats aller Fights mit. +CHAT_ARENAS_LEFT = §7Du liest die Chats aller Fights nicht mehr mit. CHAT_CHANNEL_UNKNOWN = §cUnbekannter Chatkanal. CHAT_CHANNEL_JOINED = §7Du liest §e{0} §7wieder mit. CHAT_CHANNEL_LEFT = §7Du liest §e{0} §7nicht mehr mit. diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java index 199cda66..76d75844 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -57,6 +57,7 @@ public class ChatManager extends BasicListener { private static final String CONFIG_KEY = "chat.channel"; private static final String HIDDEN_KEY = "chat.hidden"; + private static final String ARENAS_KEY = "chat.arenas"; private static final long REFEREE_CACHE_MILLIS = 60_000; @Getter @@ -76,6 +77,8 @@ public class ChatManager extends BasicListener { 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 allArenas = new ConcurrentHashMap<>(); + private static final Map> leftArenas = new ConcurrentHashMap<>(); private static final Map> hidden = new ConcurrentHashMap<>(); private static int refereeEvent = -1; @@ -159,16 +162,41 @@ public class ChatManager extends BasicListener { return !currentServer(player).map(ChatManager::isArena).orElse(false) || globalOnArena.contains(player.getUniqueId()); } + /** + * @return the moderators reading the fight chats of the server without being on it + */ 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) + return Chatter.allPlayers() .map(Chatter::of) + .filter(chatter -> !onServer(chatter, server)) .filter(chatter -> chatter.user().hasPerm(UserPerm.MODERATION)) + .filter(chatter -> readsArena(chatter.user().getUUID(), server)) .map(Chatter.class::cast); } + private static boolean readsArena(UUID uuid, RegisteredServer server) { + Set joined = spies.get(uuid); + if (joined != null && joined.contains(server)) return true; + + Set left = leftArenas.get(uuid); + return readsAllArenas(uuid) && (left == null || !left.contains(server)); + } + + private static boolean readsAllArenas(UUID uuid) { + return allArenas.computeIfAbsent(uuid, u -> "true".equals(UserConfig.getConfig(u, ARENAS_KEY))); + } + + /** + * Lets a moderator read the fight chats of every fight server automatically, saved across relogs. + */ + public static void setReadAllArenas(PlayerChatter player, boolean read) { + UUID uuid = player.user().getUUID(); + allArenas.put(uuid, read); + leftArenas.remove(uuid); + UserConfig.updatePlayerConfig(uuid, ARENAS_KEY, read ? "true" : null); + player.system(read ? "CHAT_ARENAS_JOINED" : "CHAT_ARENAS_LEFT"); + } + /** * @return true if the user is a referee of the event fight running on the server */ @@ -223,7 +251,10 @@ public class ChatManager extends BasicListener { 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()); + RegisteredServer server = ((LocalChannel) channel).getServer(); + spies.computeIfAbsent(uuid, u -> ConcurrentHashMap.newKeySet()).add(server); + Set left = leftArenas.get(uuid); + if (left != null) left.remove(server); player.system("CHAT_CHANNEL_JOINED", channel.tag(player)); return; } @@ -237,8 +268,11 @@ public class ChatManager extends BasicListener { public static void leave(PlayerChatter player, ChatChannel channel) { UUID uuid = player.user().getUUID(); if (isForeignArena(player, channel)) { + RegisteredServer server = ((LocalChannel) channel).getServer(); Set servers = spies.get(uuid); - if (servers != null) servers.remove(((LocalChannel) channel).getServer()); + if (servers != null) servers.remove(server); + // Excludes the arena for this session while reading all arenas + leftArenas.computeIfAbsent(uuid, u -> ConcurrentHashMap.newKeySet()).add(server); player.system("CHAT_CHANNEL_LEFT", channel.tag(player)); return; } @@ -445,6 +479,8 @@ public class ChatManager extends BasicListener { globalOnArena.remove(uuid); announced.remove(uuid); spies.remove(uuid); + allArenas.remove(uuid); + leftArenas.remove(uuid); hidden.remove(uuid); directs.removeIf(channel -> channel.contains(uuid)); diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java index 55fb5d9c..106042f4 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java @@ -61,6 +61,11 @@ public class ChatCommand extends SWCommand { } } + @Register(value = "arenas", description = "CHAT_ARENAS_USAGE") + public void arenas(@Validator("moderator") PlayerChatter sender, @StaticValue(value = {"leave", "join"}, allowISE = true) boolean join) { + ChatManager.setReadAllArenas(sender, join); + } + @Register(description = "CHAT_MSG_USAGE") public void msg(@Validator("moderator") PlayerChatter sender, ChatChannel channel, @StaticValue("msg") String msg, @ErrorMessage(value = "CHAT_MSG_USAGE", allowEAs = false) String... message) { channel.send(sender, String.join(" ", message)); From 47557d7bb812986d97d991b9178f53f06dcdef9d Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 11:17:10 +0200 Subject: [PATCH 20/43] Keep global chat readable after writing into it on fight servers Writing /global on a fight server shows the global chat until the player leaves the server or runs /chat global leave, without changing the default channel. --- .../steamwar/messages/BungeeCore.properties | 1 + .../messages/BungeeCore_de.properties | 1 + .../velocitycore/chat/ChatManager.java | 25 ++++++++++++++++++- .../velocitycore/commands/GlobalCommand.java | 4 +++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index dc41683f..4f988ef1 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -621,6 +621,7 @@ CHAT_MSG_USAGE = §8/§7chat §8[§echannel§8] §7msg §8[§emessage§8] CHAT_ARENAS_USAGE = §8/§7chat §7arenas §8[§ejoin§8|§eleave§8] CHAT_ARENAS_JOINED = §7You now read the chats of all fights. CHAT_ARENAS_LEFT = §7You no longer read the chats of all fights. +CHAT_GLOBAL_AFTER_WRITING = §7You read §e{0} §7on this server until you leave it. 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. diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index b0c70a21..d352cdf3 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -586,6 +586,7 @@ CHAT_MSG_USAGE = §8/§7chat §8[§eKanal§8] §7msg §8[§eNachricht§8] CHAT_ARENAS_USAGE = §8/§7chat §7arenas §8[§ejoin§8|§eleave§8] CHAT_ARENAS_JOINED = §7Du liest jetzt die Chats aller Fights mit. CHAT_ARENAS_LEFT = §7Du liest die Chats aller Fights nicht mehr mit. +CHAT_GLOBAL_AFTER_WRITING = §7Du liest §e{0} §7auf diesem Server mit, bis du ihn verlässt. CHAT_CHANNEL_UNKNOWN = §cUnbekannter Chatkanal. CHAT_CHANNEL_JOINED = §7Du liest §e{0} §7wieder mit. CHAT_CHANNEL_LEFT = §7Du liest §e{0} §7nicht mehr mit. diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java index 76d75844..753ef201 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -75,6 +75,8 @@ public class ChatManager extends BasicListener { private static final Map defaults = new ConcurrentHashMap<>(); private static final Map persisted = new ConcurrentHashMap<>(); private static final Set globalOnArena = ConcurrentHashMap.newKeySet(); + // Players who wrote into the global chat on their current fight server and read it until they leave the server + private static final Set globalAfterWriting = ConcurrentHashMap.newKeySet(); private static final Set announced = ConcurrentHashMap.newKeySet(); private static final Map> spies = new ConcurrentHashMap<>(); private static final Map allArenas = new ConcurrentHashMap<>(); @@ -159,7 +161,20 @@ public class ChatManager extends BasicListener { } static boolean readsGlobal(Player player) { - return !currentServer(player).map(ChatManager::isArena).orElse(false) || globalOnArena.contains(player.getUniqueId()); + UUID uuid = player.getUniqueId(); + return !currentServer(player).map(ChatManager::isArena).orElse(false) || globalOnArena.contains(uuid) || globalAfterWriting.contains(uuid); + } + + /** + * Shows the global chat to a player who wrote into it on a fight server until they leave the server. + */ + public static void readGlobalAfterWriting(PlayerChatter player) { + if (!currentServer(player.getPlayer()).map(ChatManager::isArena).orElse(false)) return; + if (!reads(player, global.persistId())) return; + + if (globalAfterWriting.add(player.user().getUUID())) { + player.system("CHAT_GLOBAL_AFTER_WRITING", global.tag(player)); + } } /** @@ -283,6 +298,12 @@ public class ChatManager extends BasicListener { return; } + // Leaving the global chat read after writing into it only ends that, it does not leave the global chat everywhere + if (channel == global && globalAfterWriting.remove(uuid)) { + player.system("CHAT_CHANNEL_LEFT", channel.tag(player)); + return; + } + if (hidden(uuid).add(id)) { saveHidden(uuid); } @@ -457,6 +478,7 @@ public class ChatManager extends BasicListener { UUID uuid = player.getUniqueId(); globalOnArena.remove(uuid); + globalAfterWriting.remove(uuid); ChatChannel current = defaults.get(uuid); ChatChannel next; @@ -477,6 +499,7 @@ public class ChatManager extends BasicListener { defaults.remove(uuid); persisted.remove(uuid); globalOnArena.remove(uuid); + globalAfterWriting.remove(uuid); announced.remove(uuid); spies.remove(uuid); allArenas.remove(uuid); diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java index 489fa83b..96f7eb2b 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java @@ -23,6 +23,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.PlayerChatter; import de.steamwar.velocitycore.chat.ChatManager; @Linked @@ -35,5 +36,8 @@ public class GlobalCommand extends SWCommand { @Register(description = "GLOBAL_USAGE") public void genericCommand(Chatter sender, String... message) { ChatManager.sendOrSwitch(sender, ChatManager.getGlobal(), message, "GLOBAL_USAGE"); + if (message.length > 0 && sender instanceof PlayerChatter player) { + ChatManager.readGlobalAfterWriting(player); + } } } From 3aa471c3ab992bd5682897f23245e8b5ae4bb389 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 16 Oct 2026 11:28:47 +0200 Subject: [PATCH 21/43] Read global chat on fight servers after /chat global join --- .../velocitycore/chat/ChatManager.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java index 753ef201..f65be1b3 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -75,8 +75,8 @@ public class ChatManager extends BasicListener { private static final Map defaults = new ConcurrentHashMap<>(); private static final Map persisted = new ConcurrentHashMap<>(); private static final Set globalOnArena = ConcurrentHashMap.newKeySet(); - // Players who wrote into the global chat on their current fight server and read it until they leave the server - private static final Set globalAfterWriting = ConcurrentHashMap.newKeySet(); + // Players reading the global chat on their current fight server until they leave it (after /global or /chat global join) + private static final Set globalOnServer = ConcurrentHashMap.newKeySet(); private static final Set announced = ConcurrentHashMap.newKeySet(); private static final Map> spies = new ConcurrentHashMap<>(); private static final Map allArenas = new ConcurrentHashMap<>(); @@ -162,7 +162,7 @@ public class ChatManager extends BasicListener { static boolean readsGlobal(Player player) { UUID uuid = player.getUniqueId(); - return !currentServer(player).map(ChatManager::isArena).orElse(false) || globalOnArena.contains(uuid) || globalAfterWriting.contains(uuid); + return !currentServer(player).map(ChatManager::isArena).orElse(false) || globalOnArena.contains(uuid) || globalOnServer.contains(uuid); } /** @@ -172,7 +172,7 @@ public class ChatManager extends BasicListener { if (!currentServer(player.getPlayer()).map(ChatManager::isArena).orElse(false)) return; if (!reads(player, global.persistId())) return; - if (globalAfterWriting.add(player.user().getUUID())) { + if (globalOnServer.add(player.user().getUUID())) { player.system("CHAT_GLOBAL_AFTER_WRITING", global.tag(player)); } } @@ -277,6 +277,9 @@ public class ChatManager extends BasicListener { if (hidden(uuid).remove(channel.persistId())) { saveHidden(uuid); } + if (channel == global && currentServer(player.getPlayer()).map(ChatManager::isArena).orElse(false)) { + globalOnServer.add(uuid); + } player.system("CHAT_CHANNEL_JOINED", channel.tag(player)); } @@ -298,8 +301,8 @@ public class ChatManager extends BasicListener { return; } - // Leaving the global chat read after writing into it only ends that, it does not leave the global chat everywhere - if (channel == global && globalAfterWriting.remove(uuid)) { + // On fight servers leaving only stops reading the global chat on this server, it does not leave the global chat everywhere + if (channel == global && globalOnServer.remove(uuid)) { player.system("CHAT_CHANNEL_LEFT", channel.tag(player)); return; } @@ -478,7 +481,7 @@ public class ChatManager extends BasicListener { UUID uuid = player.getUniqueId(); globalOnArena.remove(uuid); - globalAfterWriting.remove(uuid); + globalOnServer.remove(uuid); ChatChannel current = defaults.get(uuid); ChatChannel next; @@ -499,7 +502,7 @@ public class ChatManager extends BasicListener { defaults.remove(uuid); persisted.remove(uuid); globalOnArena.remove(uuid); - globalAfterWriting.remove(uuid); + globalOnServer.remove(uuid); announced.remove(uuid); spies.remove(uuid); allArenas.remove(uuid); From 2111d72387d65391382a7e972ef9f7d9bcc8e0b2 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sun, 18 Oct 2026 19:58:34 +0200 Subject: [PATCH 22/43] Suggest /team info when clicking a team prefix Clicking the team prefix in the global/local chat or an event team prefix in the fight chat puts /team info into the chat input; the hover explains the click. --- .../steamwar/messages/BungeeCore.properties | 3 +- .../messages/BungeeCore_de.properties | 3 +- .../velocitycore/chat/ChatFormat.java | 35 ++++++++++++++----- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index 4f988ef1..8e47001e 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -575,7 +575,7 @@ CHAT_EMPTY = §cDon\'t write meaningless empty messages. CHAT_SERVERTEAM_SENDER = §e{0} CHAT_SERVERTEAM = §8» §f{2} -CHAT_GLOBAL_SENDER = {3}{4}{5}{6}{0} +CHAT_GLOBAL_SENDER = {4}{5}{6}{0} CHAT_GLOBAL = §8» {7}{2} CHAT_TEAM_SENDER = §e{0} CHAT_TEAM = §8» §f{2} @@ -593,6 +593,7 @@ 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_HOVER_TEAM_CLICK = §7Team: §e{0}\n§7Prefix: §r{1}{2}\n§8Click to insert §7/team info CHAT_HOVER_LOCAL = {0}\n§7Server: §e{1} CHAT_ICON_GLOBAL = 🌍 CHAT_ICON_LOCAL = ⌂ diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index d352cdf3..6f23220f 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -540,7 +540,7 @@ CHAT_EMPTY = §cSchreibe keine inhaltslosen Nachrichten. CHAT_SERVERTEAM_SENDER = §e{0} CHAT_SERVERTEAM = §8» §f{2} -CHAT_GLOBAL_SENDER = {3}{4}{5}{6}{0} +CHAT_GLOBAL_SENDER = {4}{5}{6}{0} CHAT_GLOBAL = §8» {7}{2} CHAT_TEAM_SENDER = §e{0} CHAT_TEAM = §8» §f{2} @@ -558,6 +558,7 @@ 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_HOVER_TEAM_CLICK = §7Team: §e{0}\n§7Kürzel: §r{1}{2}\n§8Klicke, um §7/team info §8einzufügen CHAT_HOVER_LOCAL = {0}\n§7Server: §e{1} CHAT_ICON_GLOBAL = 🌍 CHAT_ICON_LOCAL = ⌂ diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java index 8f4a477e..91e5feda 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java @@ -32,9 +32,11 @@ import de.steamwar.velocitycore.util.SteamwarPrefix; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextReplacementConfig; +import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; import java.util.Arrays; +import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -42,6 +44,9 @@ public class ChatFormat { private static final Pattern MENTION = Pattern.compile("@(\\w{1,16})"); + // Formats that show the team prefix in front of the sender name + private static final Set TEAM_PREFIX_FORMATS = Set.of("CHAT_GLOBAL"); + private ChatFormat() {} /** @@ -52,12 +57,15 @@ public class ChatFormat { 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() + " "; + // The team prefix is its own component, so that clicking it suggests /team info + Component teamPrefix = Component.empty(); + if (TEAM_PREFIX_FORMATS.contains(format)) { + if (useSwPrefix(user)) { + teamPrefix = withTeamHover(receiver, user, Chatter.SERIALIZER.deserialize("§eS§8W")).append(Component.text(" ")); + } else if (user.getTeam() != 0) { + Team team = Team.byId(user.getTeam()); + teamPrefix = clickableTeam(receiver, team, Chatter.SERIALIZER.deserialize("§" + team.getTeamColor() + team.getTeamKuerzel())).append(Component.text(" ")); + } } String chatColorCode = user.hasPerm(UserPerm.TEAM) ? "§f" : "§7"; @@ -65,14 +73,14 @@ public class ChatFormat { 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(); + return Component.text().append(teamPrefix).append(senderPart).append(receiver.parse(new Message(format, params))).build(); } /** @@ -87,7 +95,7 @@ public class ChatFormat { Component team = Chatter.SERIALIZER.deserialize(coloredTeamName); Team eventTeam = ChatManager.eventTeam(server, coloredTeamName); if (eventTeam != null) { - team = team.hoverEvent(HoverEvent.showText(teamHover(receiver, eventTeam))); + team = clickableTeam(receiver, eventTeam, team); } line.append(team).append(Component.text(" ")); nameColor = coloredTeamName.length() >= 2 && coloredTeamName.charAt(0) == '§' ? coloredTeamName.substring(0, 2) : ""; @@ -110,6 +118,15 @@ public class ChatFormat { return component.hoverEvent(HoverEvent.showText(teamHover(receiver, Team.byId(user.getTeam())))); } + /** + * Shows the team on hover and suggests {@code /team info} for it on click. + */ + private static Component clickableTeam(Chatter receiver, Team team, Component component) { + return component + .hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM_CLICK", team.getTeamName(), "§" + team.getTeamColor(), team.getTeamKuerzel())))) + .clickEvent(ClickEvent.suggestCommand("/team info " + team.getTeamKuerzel())); + } + private static Component teamHover(Chatter receiver, Team team) { return receiver.parse(new Message("CHAT_HOVER_TEAM", team.getTeamName(), "§" + team.getTeamColor(), team.getTeamKuerzel())); } From 12dd46176b027564a2961566587201ec9e7ab179 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sun, 18 Oct 2026 19:59:40 +0200 Subject: [PATCH 23/43] Suggest /team info SW when clicking the SW prefix --- .../src/de/steamwar/velocitycore/chat/ChatFormat.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java index 91e5feda..28b4a725 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java @@ -61,7 +61,10 @@ public class ChatFormat { Component teamPrefix = Component.empty(); if (TEAM_PREFIX_FORMATS.contains(format)) { if (useSwPrefix(user)) { - teamPrefix = withTeamHover(receiver, user, Chatter.SERIALIZER.deserialize("§eS§8W")).append(Component.text(" ")); + teamPrefix = Chatter.SERIALIZER.deserialize("§eS§8W") + .hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM_CLICK", "SteamWar", "§e", "S§8W")))) + .clickEvent(ClickEvent.suggestCommand("/team info SW")) + .append(Component.text(" ")); } else if (user.getTeam() != 0) { Team team = Team.byId(user.getTeam()); teamPrefix = clickableTeam(receiver, team, Chatter.SERIALIZER.deserialize("§" + team.getTeamColor() + team.getTeamKuerzel())).append(Component.text(" ")); From 253a975c73103549d59e5a6f3cba729589f82d01 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sun, 18 Oct 2026 20:01:09 +0200 Subject: [PATCH 24/43] Show no player hover for players of the public team --- VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java index 28b4a725..1921bec8 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java @@ -113,10 +113,11 @@ public class ChatFormat { * 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) { + // Players of the public team (id 0) get no hover + if (user.getTeam() == 0) return 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())))); } From 230f8c521a1afa1b1d3518d1ad5fb28036378f04 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sun, 18 Oct 2026 18:08:45 +0000 Subject: [PATCH 25/43] Use PistonStructureResolver in PistonCalculator Numbering now comes directly from the vanilla resolver instead of a custom breadth-first search, so the order matches the real move order (including backtracking through slime/honey chains). The display of more than 12 blocks is removed; a blocked piston is shown in red. Refs: #158 --- .../features/util/PistonCalculator.java | 143 ++++-------------- 1 file changed, 27 insertions(+), 116 deletions(-) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/PistonCalculator.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/PistonCalculator.java index ec191942..59044202 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/PistonCalculator.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/PistonCalculator.java @@ -23,15 +23,17 @@ import de.steamwar.bausystem.BauSystem; import de.steamwar.bausystem.Permission; import de.steamwar.entity.*; import de.steamwar.linkage.Linked; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.piston.PistonStructureResolver; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; -import org.bukkit.block.PistonMoveReaction; -import org.bukkit.block.TileState; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.type.Piston; +import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.entity.Display; import org.bukkit.entity.Player; import org.bukkit.entity.TextDisplay; @@ -170,7 +172,7 @@ public class PistonCalculator implements Listener { private boolean pulling = false; private List movedBlocks = new ArrayList<>(); private List brokenBlocks = new ArrayList<>(); - private Set immovableBlocks = new HashSet<>(); + private boolean blocked = false; public PistOrder(Block piston) { this.piston = piston; @@ -179,7 +181,7 @@ public class PistonCalculator implements Listener { public void calculate() { movedBlocks.clear(); brokenBlocks.clear(); - immovableBlocks.clear(); + blocked = false; if (piston.isEmpty()) { Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { @@ -199,7 +201,7 @@ public class PistonCalculator implements Listener { pulling = piston.getType() == Material.STICKY_PISTON && (piston.getRelative(pistonData.getFacing()).getType() == Material.AIR || pistonData.isExtended()); - calculate(piston, pistonData.getFacing(), (pulling ? pistonData.getFacing().getOppositeFace() : pistonData.getFacing())); + resolve(pistonData.getFacing()); Collections.reverse(movedBlocks); Collections.reverse(brokenBlocks); @@ -207,86 +209,23 @@ public class PistonCalculator implements Listener { visualize(); } - private void calculate(Block origin, BlockFace facing, BlockFace direction) { - Block calcOrigin = origin; - if (facing != direction) calcOrigin = origin.getRelative(facing, 3); - - List toCalc = new LinkedList<>(); - calcDirection(origin, null, calcOrigin, facing != direction ? origin.getRelative(facing) : null, facing, direction, toCalc); - - while (!toCalc.isEmpty()) { - Block current = toCalc.remove(0); - if (!movedBlocks.contains(current.getLocation())) { - movedBlocks.add(current.getLocation()); - } - - Material type = current.getType(); - if (type != Material.SLIME_BLOCK && type != Material.HONEY_BLOCK) continue; - Material oppositeType = type == Material.SLIME_BLOCK ? Material.HONEY_BLOCK : Material.SLIME_BLOCK; - - for (BlockFace face : FACES) { - Block block = current.getRelative(face); - if (block.getType().isAir()) continue; - if (isImmovable(block) || block.getPistonMoveReaction() == PistonMoveReaction.BREAK) continue; - if (face != direction || face == direction.getOppositeFace()) { - switch (block.getType()) { - case BLACK_GLAZED_TERRACOTTA: - case GRAY_GLAZED_TERRACOTTA: - case BLUE_GLAZED_TERRACOTTA: - case BROWN_GLAZED_TERRACOTTA: - case CYAN_GLAZED_TERRACOTTA: - case GREEN_GLAZED_TERRACOTTA: - case LIGHT_BLUE_GLAZED_TERRACOTTA: - case LIGHT_GRAY_GLAZED_TERRACOTTA: - case LIME_GLAZED_TERRACOTTA: - case MAGENTA_GLAZED_TERRACOTTA: - case ORANGE_GLAZED_TERRACOTTA: - case PINK_GLAZED_TERRACOTTA: - case PURPLE_GLAZED_TERRACOTTA: - case RED_GLAZED_TERRACOTTA: - case WHITE_GLAZED_TERRACOTTA: - case YELLOW_GLAZED_TERRACOTTA: - continue; - default: - break; - } - } - if (block.getType() != oppositeType) { - if (!movedBlocks.contains(block.getLocation())) toCalc.add(block); - calcDirection(null, origin, block, null, facing, direction, toCalc); - } - } + private void resolve(BlockFace facing) { + PistonStructureResolver resolver = new PistonStructureResolver( + ((CraftWorld) piston.getWorld()).getHandle(), + new BlockPos(piston.getX(), piston.getY(), piston.getZ()), + Direction.valueOf(facing.name()), + !pulling + ); + if (!resolver.resolve()) { + blocked = true; + return; } - - movedBlocks.remove(origin.getLocation()); - if (facing != direction) movedBlocks.remove(origin.getRelative(facing, 1).getLocation()); - if (pulling) immovableBlocks.remove(origin.getRelative(facing).getLocation()); + resolver.getToPush().forEach(pos -> movedBlocks.add(toLocation(pos))); + resolver.getToDestroy().forEach(pos -> brokenBlocks.add(toLocation(pos))); } - private void calcDirection(Block origin, Block blockOrigin, Block calcOrigin, Block ignore, BlockFace facing, BlockFace direction, List toCalc) { - for (int i = 1; i < 24; i++) { - Block block = calcOrigin.getRelative(direction, i); - if (block.equals(ignore)) return; - if (block.getPistonMoveReaction() == PistonMoveReaction.BREAK) { - if (!brokenBlocks.contains(block.getLocation())) { - brokenBlocks.add(block.getLocation()); - } - return; - } - if (isImmovable(block)) { - immovableBlocks.add(block.getLocation()); - return; - } - if (block.getType().isAir()) return; - if (facing == direction && block.equals(blockOrigin)) { - immovableBlocks.add(block.getLocation()); - return; - } - if (facing != direction && (block.equals(origin) || block.getRelative(facing.getOppositeFace()).equals(origin))) { - return; - } - if (!movedBlocks.contains(block.getLocation())) toCalc.add(block); - } + private Location toLocation(BlockPos pos) { + return new Location(piston.getWorld(), pos.getX(), pos.getY(), pos.getZ()); } private void visualize() { @@ -321,21 +260,11 @@ public class PistonCalculator implements Listener { textDisplay.setBrightness(new Display.Brightness(15, 15)); } - for (Location location : immovableBlocks) { - CWireframe wireframe = new CWireframe(server); - wireframe.setPos1(location); - wireframe.setPos2(location); - wireframe.setWidth(1 / 32f); - wireframe.setBlock(Material.RED_CONCRETE.createBlockData()); - } - CWireframe wireframe = new CWireframe(server); wireframe.setPos1(piston.getLocation()); wireframe.setPos2(piston.getLocation()); - if (!immovableBlocks.isEmpty()) { + if (blocked) { wireframe.setBlock(Material.RED_CONCRETE.createBlockData()); - } else if (movedBlocks.size() > 12) { - wireframe.setBlock(Material.YELLOW_CONCRETE.createBlockData()); } else { wireframe.setBlock(Material.LIME_CONCRETE.createBlockData()); } @@ -348,7 +277,11 @@ public class PistonCalculator implements Listener { } else { text.append("§ePush\n"); } - text.append("§f").append(movedBlocks.size()).append(" §eBlocks"); + if (blocked) { + text.append("§cBlocked"); + } else { + text.append("§f").append(movedBlocks.size()).append(" §eBlocks"); + } textDisplay.setText(text.toString()); textDisplay.setBillboard(Display.Billboard.CENTER); textDisplay.setAlignment(TextDisplay.TextAlignment.CENTER); @@ -358,26 +291,4 @@ public class PistonCalculator implements Listener { textDisplay.setBrightness(new Display.Brightness(15, 15)); } } - - private boolean isImmovable(Block block) { - if (block.isEmpty() || block.isLiquid()) { - return false; - } - BlockData blockData = block.getBlockData(); - if (blockData instanceof Piston) { - return ((Piston) blockData).isExtended(); - } - if (block.getState() instanceof TileState) { - return true; - } - PistonMoveReaction reaction = block.getPistonMoveReaction(); - if (reaction == PistonMoveReaction.IGNORE) return true; - if (reaction == PistonMoveReaction.BLOCK) return true; - switch (block.getType()) { - case OBSIDIAN: - return true; - default: - return false; - } - } } From 097df865e52643f8301836f3d8c0b6c90693160d Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sun, 18 Oct 2026 18:28:37 +0000 Subject: [PATCH 26/43] Resend replay player tab entry when skin arrives RPlayer requests the skin asynchronously, so the first tab list packet is usually sent without textures and viewers already in range kept the default skin. Re-send despawn/delist/list/spawn to the current viewers once the skin response is received. --- .../src/de/steamwar/entity/REntityServer.java | 16 ++++++++++++++++ .../src/de/steamwar/entity/RPlayer.java | 10 ++++++++++ .../steamwar/network/CoreNetworkHandler.java | 18 ++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java index 7a48f553..bff64bec 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java @@ -141,6 +141,22 @@ public class REntityServer implements Listener { } } + /** + * Re-sends list and spawn packets of an already added entity to all current viewers, + * e.g. when information that is part of the tab list entry changed after the entity was added. + */ + void refreshEntity(REntity entity) { + if (!entityMap.containsKey(entity.entityId)) return; + + for (Player player : players.getOrDefault(entityToId(entity), emptyPlayers)) { + Consumer sink = packet -> TinyProtocol.instance.sendPacket(player, packet); + if (!entity.isHidden()) entity.despawn(sink); + entity.delist(sink); + entity.list(sink); + if (!entity.isHidden()) entity.spawn(sink); + } + } + void removeEntity(REntity entity) { entity.despawn(packet -> updateEntity(entity, packet)); removeEntityFromChunk(entity); diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RPlayer.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RPlayer.java index cfb6304d..b57e1ee3 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RPlayer.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RPlayer.java @@ -76,10 +76,20 @@ public class RPlayer extends REntity implements RInteractableEntity { } private GameProfile saved; + private boolean awaitingSkin = false; @Override void list(Consumer packetSink) { saved = getGameProfile(); + if (saved.getProperties().isEmpty() && !awaitingSkin) { + // The skin is requested asynchronously, so it is usually not available yet when the entity is created. + // Without a refresh, players that already see this entity would keep the default skin. + awaitingSkin = true; + CoreNetworkHandler.onSkinReceived(actualUUID, () -> { + awaitingSkin = false; + server.refreshEntity(this); + }); + } packetSink.accept(ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.ADD, saved, GameMode.CREATIVE)); } diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/network/CoreNetworkHandler.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/network/CoreNetworkHandler.java index bc5d471e..a5fe8cf5 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/network/CoreNetworkHandler.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/network/CoreNetworkHandler.java @@ -21,6 +21,7 @@ package de.steamwar.network; import com.mojang.authlib.properties.Property; import de.steamwar.core.BountifulWrapper; +import de.steamwar.core.Core; import de.steamwar.network.handlers.InventoryHandler; import de.steamwar.network.packets.PacketHandler; import de.steamwar.network.packets.common.PlayerSkinResponsePacket; @@ -31,8 +32,11 @@ import org.bukkit.Bukkit; import org.bukkit.entity.Player; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; public class CoreNetworkHandler extends PacketHandler { @@ -85,9 +89,23 @@ public class CoreNetworkHandler extends PacketHandler { } }; + private static final Map> SKIN_LISTENERS = new ConcurrentHashMap<>(); + + /** + * Runs the given callback on the main thread once the skin of the given player has been received. + */ + public static void onSkinReceived(UUID uuid, Runnable callback) { + SKIN_LISTENERS.computeIfAbsent(uuid, __ -> new CopyOnWriteArrayList<>()).add(callback); + } + @Handler public void handlePlayerSkinResponse(PlayerSkinResponsePacket packet) { if (!SKIN_DATA_PROMISES.containsKey(packet.getUuid())) return; SKIN_DATA_PROMISES.put(packet.getUuid(), new Property("textures", packet.getSkin(), packet.getSignature())); + + Bukkit.getScheduler().runTask(Core.getInstance(), () -> { + List listeners = SKIN_LISTENERS.remove(packet.getUuid()); + if (listeners != null) listeners.forEach(Runnable::run); + }); } } From 69aef9dfb6e8f49fd97c38b723477d89cc12c048 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 19 Oct 2026 06:38:20 +0000 Subject: [PATCH 27/43] Fix ClassCastException for replay player equipment PacketProcessor passes Bukkit EquipmentSlots to REntity#setItem, which cast them to the NMS EquipmentSlot. Take the Bukkit slot and map it to the NMS slot when building the equipment packet. --- .../src/de/steamwar/entity/REntity.java | 19 +++++++++++++++---- .../src/de/steamwar/entity/RPlayer.java | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java index a5be3c41..5035e30f 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java @@ -89,7 +89,7 @@ public class REntity { private final int objectData; @Getter private String displayName; - protected final Map itemSlots; + protected final Map itemSlots; public REntity(REntityServer server, EntityType entityType, Location location) { this(server, entityType, location, 0); @@ -220,7 +220,7 @@ public class REntity { .add(nameVisibleWatcher, displayName != null).build()); } - public void setItem(Object slot, ItemStack stack) { + public void setItem(org.bukkit.inventory.EquipmentSlot slot, ItemStack stack) { itemSlots.put(slot, stack); server.updateEntity(this, getEquipmentPacket(slot, stack)); } @@ -368,8 +368,19 @@ public class REntity { return packet; } - protected Object getEquipmentPacket(Object slot, ItemStack stack) { - return new ClientboundSetEquipmentPacket(entityId, Collections.singletonList(Pair.of((EquipmentSlot) slot, CraftItemStack.asNMSCopy(stack)))); + protected Object getEquipmentPacket(org.bukkit.inventory.EquipmentSlot slot, ItemStack stack) { + return new ClientboundSetEquipmentPacket(entityId, Collections.singletonList(Pair.of(toNmsSlot(slot), CraftItemStack.asNMSCopy(stack)))); + } + + private static EquipmentSlot toNmsSlot(org.bukkit.inventory.EquipmentSlot slot) { + return switch (slot) { + case HEAD -> EquipmentSlot.HEAD; + case CHEST -> EquipmentSlot.CHEST; + case LEGS -> EquipmentSlot.LEGS; + case FEET -> EquipmentSlot.FEET; + case OFF_HAND -> EquipmentSlot.OFFHAND; + default -> EquipmentSlot.MAINHAND; + }; } private byte rotToByte(float rot) { diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RPlayer.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RPlayer.java index b57e1ee3..e13343dc 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RPlayer.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RPlayer.java @@ -98,7 +98,7 @@ public class RPlayer extends REntity implements RInteractableEntity { packetSink.accept(getNamedSpawnPacket()); entityDataPacket().add(skinPartsDataWatcher, (byte) 0x7F).send(packetSink); - for (Map.Entry entry : itemSlots.entrySet()) { + for (Map.Entry entry : itemSlots.entrySet()) { packetSink.accept(getEquipmentPacket(entry.getKey(), entry.getValue())); } From 380693a2b74a9a3686ab1dafdaba7ff6c1a2117f Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 19 Oct 2026 08:56:43 +0200 Subject: [PATCH 28/43] Fix REntity position desync and velocity in 1.21 replays Send ClientboundEntityPositionSyncPacket instead of the teleport packet so relative moves keep working afterwards, and pass rotation in degrees. Pass the velocity unscaled to Vec3 instead of pre-multiplying by 8000. Fixes #247 --- .../src/de/steamwar/entity/REntity.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java index 5035e30f..ba849b29 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java @@ -180,7 +180,7 @@ public class REntity { } public void setVelocity(double dX, double dY, double dZ) { - server.updateEntity(this, new ClientboundSetEntityMotionPacket(entityId, new Vec3(calcVelocity(dX), calcVelocity(dY), calcVelocity(dZ)))); + server.updateEntity(this, new ClientboundSetEntityMotionPacket(entityId, new Vec3(dX, dY, dZ))); } public void showDamage() { @@ -340,8 +340,8 @@ public class REntity { } private Object getTeleportPacket() { - PositionMoveRotation rot = new PositionMoveRotation(new Vec3(x, y, z), Vec3.ZERO, pitch, yaw); - return new ClientboundTeleportEntityPacket(entityId, rot, Collections.emptySet(), false); + PositionMoveRotation rot = new PositionMoveRotation(new Vec3(x, y, z), Vec3.ZERO, pitch * 360F / 256F, yaw * 360F / 256F); + return new ClientboundEntityPositionSyncPacket(entityId, rot, false); } private Object getMoveLookPacket(double diffX, double diffY, double diffZ, boolean rotEq) { @@ -387,10 +387,6 @@ public class REntity { return (byte) ((int) (rot * 256.0F / 360.0F)); } - private int calcVelocity(double value) { - return (int) (Math.max(-3.9, Math.min(value, 3.9)) * 8000); - } - private static Optional formatDisplayName(String displayName) { return displayName != null ? Optional.of(MutableComponent.create(PlainTextContents.create(displayName))) : Optional.empty(); } From 5cc55295330af06231a136edf00664dd2133af8f Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 19 Oct 2026 09:07:08 +0200 Subject: [PATCH 29/43] Reenable ReplayCommand --- .../src/de/steamwar/velocitycore/commands/ReplayCommand.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ReplayCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ReplayCommand.java index ae276519..dc6af940 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/ReplayCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ReplayCommand.java @@ -57,9 +57,6 @@ public class ReplayCommand extends SWCommand { @Register public void genericCommand(PlayerChatter sender, @OptionalValue("") String optionalMap) { if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.NoFightServer)) return; - if (!sender.user().hasPerm(UserPerm.TEAM)) return; - - new SWStreamInv<>(sender, new Message("REPLAY_TITLE"), (click, fight) -> { startReplay(sender, click.isShiftClick(), optionalMap, fight); From d14ddc4fabd2189d5f63b49596539871032c74bc Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 19 Oct 2026 07:18:29 +0000 Subject: [PATCH 30/43] Fix lag from PistonCalculator at high TPS Bundle piston order recalculations into one run every 200ms (wall clock, independent of TPS) instead of one full recalculation and entity rebuild per piston/place/break event. Tracked pistons that get moved are removed from the visualization instead of being moved along. Fixes #227 --- .../features/util/PistonCalculator.java | 55 ++++++++++--------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/PistonCalculator.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/PistonCalculator.java index 59044202..8b9b4a62 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/PistonCalculator.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/PistonCalculator.java @@ -94,41 +94,48 @@ public class PistonCalculator implements Listener { @EventHandler public void onBlockPistonExtend(BlockPistonExtendEvent event) { if (pistOrders.isEmpty()) return; - Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { - movePistOrders(event.getDirection(), event.getBlocks()); - pistOrders.values().forEach(PistOrder::calculate); - }, 3); + removeMovedPistOrders(event.getBlocks()); + scheduleRecalculate(); } @EventHandler public void onBlockPistonRetract(BlockPistonRetractEvent event) { if (pistOrders.isEmpty()) return; - Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { - movePistOrders(event.getDirection(), event.getBlocks()); - pistOrders.values().forEach(PistOrder::calculate); - }, 3); + removeMovedPistOrders(event.getBlocks()); + scheduleRecalculate(); } - private void movePistOrders(BlockFace direction, List blocks) { - Set orders = new HashSet<>(); + private void removeMovedPistOrders(List blocks) { blocks.forEach(block -> { - PistOrder pistOrder = pistOrders.get(block.getLocation()); - if (pistOrder == null) return; - pistOrders.remove(block.getLocation()); - pistOrder.piston = pistOrder.piston.getRelative(direction); - orders.add(pistOrder); - }); - orders.forEach(pistOrder -> { - pistOrders.put(pistOrder.piston.getLocation(), pistOrder); + PistOrder pistOrder = pistOrders.remove(block.getLocation()); + if (pistOrder != null) pistOrder.server.close(); }); } + private static final long RECALCULATE_INTERVAL_MS = 200; + private boolean recalculationScheduled = false; + private long lastRecalculation = 0; + + private void scheduleRecalculate() { + if (recalculationScheduled) return; + recalculationScheduled = true; + Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), this::recalculateIfDue, 1); + } + + private void recalculateIfDue() { + if (System.currentTimeMillis() - lastRecalculation < RECALCULATE_INTERVAL_MS) { + Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), this::recalculateIfDue, 1); + return; + } + recalculationScheduled = false; + lastRecalculation = System.currentTimeMillis(); + pistOrders.values().forEach(PistOrder::calculate); + } + @EventHandler public void onBlockPlace(BlockPlaceEvent event) { if (pistOrders.isEmpty()) return; - Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { - pistOrders.values().forEach(PistOrder::calculate); - }, 1); + scheduleRecalculate(); } @EventHandler @@ -138,9 +145,7 @@ public class PistonCalculator implements Listener { PistOrder pistOrder = pistOrders.remove(event.getBlock().getLocation()); pistOrder.server.close(); } - Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { - pistOrders.values().forEach(PistOrder::calculate); - }, 1); + scheduleRecalculate(); } @EventHandler @@ -166,7 +171,7 @@ public class PistonCalculator implements Listener { private final Map pistOrders = new HashMap<>(); private final class PistOrder { - private Block piston; + private final Block piston; private REntityServer server = new REntityServer(); private boolean pulling = false; From 8318d408b63e9d3f033127443ca5cdae7bfa19b8 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 19 Oct 2026 09:27:27 +0200 Subject: [PATCH 31/43] Fix REntity relative move rotation in 1.21 Pass yaw before pitch to ClientboundMoveEntityPacket.Rot/PosRot and stop converting the already byte-encoded rotation a second time. --- .../SpigotCore_Main/src/de/steamwar/entity/REntity.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java index ba849b29..bbab5bbf 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java @@ -340,7 +340,7 @@ public class REntity { } private Object getTeleportPacket() { - PositionMoveRotation rot = new PositionMoveRotation(new Vec3(x, y, z), Vec3.ZERO, pitch * 360F / 256F, yaw * 360F / 256F); + PositionMoveRotation rot = new PositionMoveRotation(new Vec3(x, y, z), Vec3.ZERO, yaw * 360F / 256F, pitch * 360F / 256F); return new ClientboundEntityPositionSyncPacket(entityId, rot, false); } @@ -348,16 +348,14 @@ public class REntity { short x = (short) (diffX * 4096); short y = (short) (diffY * 4096); short z = (short) (diffZ * 4096); - byte yaw = (byte) (this.yaw * 256 / 360); - byte pitch = (byte) (this.pitch * 256 / 360); if (diffX == 0 && diffY == 0 && diffZ == 0) { if (rotEq) return null; - return new ClientboundMoveEntityPacket.Rot(entityId, pitch, yaw, false); + return new ClientboundMoveEntityPacket.Rot(entityId, yaw, pitch, false); } else if (rotEq) { return new ClientboundMoveEntityPacket.Pos(entityId, x, y, z, false); } else { - return new ClientboundMoveEntityPacket.PosRot(entityId, x, y, z, pitch, yaw, false); + return new ClientboundMoveEntityPacket.PosRot(entityId, x, y, z, yaw, pitch, false); } } From 21d0a4359d0b8252a0cd248b6703195e44809181 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 19 Oct 2026 09:31:38 +0200 Subject: [PATCH 32/43] Update .gitignore --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index ba7d8990..6b5c70a1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,14 @@ !gradle/wrapper/gradle-wrapper.jar steamwar.properties +# Kotlin +.kotlin + # IntelliJ IDEA *.iml +.idea +!.idea/copyright +!.idea/runConfigurations # VSCode bin/ From 37f017b84c864b7e9c89f52dbf4f60691f71c6d2 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 19 Oct 2026 09:59:02 +0200 Subject: [PATCH 33/43] Improve action bar loading bar for server start --- .../src/de/steamwar/persistent/Subserver.java | 92 ++++++++++++++----- 1 file changed, 68 insertions(+), 24 deletions(-) diff --git a/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java b/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java index 711bab09..f2a237aa 100644 --- a/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java +++ b/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java @@ -22,6 +22,7 @@ package de.steamwar.persistent; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; +import com.velocitypowered.api.scheduler.ScheduledTask; import lombok.Getter; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -32,7 +33,10 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.logging.Level; @@ -98,7 +102,13 @@ public class Subserver { @Getter private boolean started; - private final List cachedPlayers = new LinkedList<>(); + private static final int MAX_PROGRESS = 10; + private static final Pattern PLUGIN_COUNT = Pattern.compile("Bukkit plugins \\((\\d+)\\)"); + + private final List cachedPlayers = new CopyOnWriteArrayList<>(); + private volatile int progress = 0; + private int pluginCount = 0; + private int enabledPlugins = 0; @Getter private final Map tablistNames = new HashMap<>(); @@ -127,7 +137,7 @@ public class Subserver { public void sendPlayer(Player p) { if (!started) { - p.sendActionBar(generateBar(0)); + p.sendActionBar(generateBar(progress)); cachedPlayers.add(p); } else { p.createConnectionRequest(registeredServer).connect(); @@ -246,31 +256,32 @@ public class Subserver { private void run() { Exception ex = null; try { - if (checkpoint) { - start(process.getErrorStream(), line -> line.contains("Restore finished successfully.")); - start(process.getInputStream(), line -> line.contains("Checkpoint restored")); - Thread.sleep(100); - } else { - sendProgress(0); - start(process.getInputStream(), line -> { - if (line.contains("Loading libraries, please wait")) { - sendProgress(2); - } else if (line.contains("Starting Minecraft server on")) { - sendProgress(5); - } else if (line.contains("Preparing start region")) { - sendProgress(8); - } - return line.contains("Done ("); - }); - } + ScheduledTask refresh = Persistent.getInstance().getProxy().getScheduler() + .buildTask(Persistent.getInstance(), () -> sendProgress(progress)) + .repeat(2, TimeUnit.SECONDS).schedule(); - if (!started) return; + try { + if (checkpoint) { + start(process.getErrorStream(), line -> line.contains("Restore finished successfully.")); + start(process.getInputStream(), line -> line.contains("Checkpoint restored")); + Thread.sleep(100); + } else { + start(process.getInputStream(), line -> { + updateProgress(line); + return line.contains("Done ("); + }); + } - sendProgress(10); - for (Player cachedPlayer : cachedPlayers) { - sendPlayer(cachedPlayer); + if (!started) return; + + setProgress(MAX_PROGRESS); + for (Player cachedPlayer : cachedPlayers) { + sendPlayer(cachedPlayer); + } + cachedPlayers.clear(); + } finally { + refresh.cancel(); } - cachedPlayers.clear(); process.waitFor(); } catch (IOException e) { @@ -285,6 +296,39 @@ public class Subserver { } } + private void updateProgress(String line) { + if (line.startsWith("Loading Paper")) { + setProgress(1); + } else if (line.startsWith("Bukkit plugins (")) { + Matcher matcher = PLUGIN_COUNT.matcher(line); + if (matcher.find()) pluginCount = Integer.parseInt(matcher.group(1)); + } else if (line.startsWith("Starting minecraft server version")) { + setProgress(2); + } else if (line.contains("Starting Minecraft server on")) { + setProgress(3); + } else if (line.contains("libraries... please wait")) { + setProgress(4); + } else if (line.startsWith("Loading server plugin")) { + setProgress(5); + } else if (line.startsWith("Preparing level")) { + setProgress(6); + } else if (line.startsWith("Preparing start region")) { + setProgress(7); + } else if (line.startsWith("Enabling ")) { + enabledPlugins++; + // 7 -> 9 over all plugins, 9 is reached with the last plugin + setProgress(7 + (pluginCount > 0 ? Math.min(2, 2 * enabledPlugins / pluginCount) : 0)); + } else if (line.startsWith("Done preparing level")) { + setProgress(9); + } + } + + private void setProgress(int newProgress) { + if (newProgress <= progress) return; + progress = newProgress; + sendProgress(newProgress); + } + private Component generateBar(int progress) { return Component.text("⬛".repeat(Math.max(0, progress))).color(NamedTextColor.YELLOW) .append(Component.text("⬛".repeat(Math.max(0, 10 - progress))).color(NamedTextColor.DARK_GRAY)); From 27d260a8d2ffda38732c4182e231a66521a71187 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 19 Oct 2026 10:14:10 +0200 Subject: [PATCH 34/43] Delete CRIU checkpoint on /bau delete /bau delete only removed the world folder. The checkpoint of the build server stayed in place, so the next start restored the old JVM state against a freshly copied world (superflat world, plugins inactive). Extract ServerStarter#checkpointDir and delete the checkpoint together with the world. Fixes #438 --- .../src/de/steamwar/velocitycore/ServerStarter.java | 6 +++++- .../src/de/steamwar/velocitycore/commands/BauCommand.java | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/VelocityCore/src/de/steamwar/velocitycore/ServerStarter.java b/VelocityCore/src/de/steamwar/velocitycore/ServerStarter.java index a4dc58fc..eab63787 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/ServerStarter.java +++ b/VelocityCore/src/de/steamwar/velocitycore/ServerStarter.java @@ -303,7 +303,7 @@ public class ServerStarter { worldSetup.run(); arguments.put("logPath", worldName); - File checkpointDir = new File("/tmp/" + System.getProperty("user.name") + ".checkpoints/" + directory.getName() + "/" + worldName); + File checkpointDir = checkpointDir(directory, worldName); if (checkpoint) { arguments.put("checkpoint", checkpointDir.getPath()); } @@ -326,6 +326,10 @@ public class ServerStarter { }).schedule(); } + public static File checkpointDir(File serverDirectory, String worldName) { + return new File("/tmp/" + System.getProperty("user.name") + ".checkpoints/" + serverDirectory.getName() + "/" + worldName); + } + private void regularStart(String serverName, int port) { postStart(constructor.construct(serverName, port, node.startServer( version, directory, worldDir, worldName, port, arguments.entrySet().stream().map(entry -> entry.getKey() + "=" + entry.getValue()).toArray(String[]::new) diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java index 048e3125..d97e1e46 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java @@ -261,6 +261,7 @@ public class BauCommand extends SWCommand { } SubserverSystem.deleteFolder(VelocityCore.local, world); + SubserverSystem.deleteFolder(VelocityCore.local, ServerStarter.checkpointDir(version.getServerDirectory("Bau"), String.valueOf(sender.user().getId())).getPath()); sender.system("BAU_DELETE_DELETED"); }).schedule(); From 0c3e63d52141b86f0a07f7504dffa51ac7fac4b7 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 19 Oct 2026 08:22:05 +0000 Subject: [PATCH 35/43] Restore BauweltMember cache and notify all dev servers on member changes getBauMember never read its cache after the Exposed migration, so every scoreboard render hit the DB (Too many connections, #479). Cache by (bauwelt, member) incl. negative results, evict on addMember/remove, and send BaumemberUpdatePacket to the Bauserver and all dev servers (also on addmember). BauMemberUpdate clears the cache itself to not depend on handler order. --- .../features/world/BauMemberUpdate.java | 3 ++ .../SQL/src/de/steamwar/sql/BauweltMember.kt | 28 ++++++++++------- .../velocitycore/commands/BauCommand.java | 30 ++++++++++++++----- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauMemberUpdate.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauMemberUpdate.java index d3416e08..d8827c4b 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauMemberUpdate.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauMemberUpdate.java @@ -28,6 +28,7 @@ import de.steamwar.bausystem.utils.bossbar.BossBarService; import de.steamwar.linkage.Linked; import de.steamwar.network.packets.PacketHandler; import de.steamwar.network.packets.server.BaumemberUpdatePacket; +import de.steamwar.sql.BauweltMember; import io.papermc.paper.event.player.PlayerPickBlockEvent; import org.bukkit.Bukkit; import org.bukkit.boss.BarColor; @@ -57,6 +58,8 @@ public class BauMemberUpdate extends PacketHandler implements Listener { } public static void baumemberUpdate() { + // Packet handlers run in no guaranteed order, so don't rely on CoreNetworkHandler having cleared the cache yet. + BauweltMember.clear(); Set newSpectator = new HashSet<>(); Set newBuilder = new HashSet<>(); Bukkit.getOnlinePlayers().forEach(player -> { diff --git a/CommonCore/SQL/src/de/steamwar/sql/BauweltMember.kt b/CommonCore/SQL/src/de/steamwar/sql/BauweltMember.kt index 8c94f52b..71c63d5c 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/BauweltMember.kt +++ b/CommonCore/SQL/src/de/steamwar/sql/BauweltMember.kt @@ -30,6 +30,7 @@ import org.jetbrains.exposed.v1.dao.CompositeEntity import org.jetbrains.exposed.v1.dao.CompositeEntityClass import org.jetbrains.exposed.v1.jdbc.insertIgnore import java.util.* +import java.util.concurrent.ConcurrentHashMap object BauweltMemberTable : CompositeIdTable("BauweltMember") { val bauweltId = reference("BauweltID", SteamwarUserTable).index() @@ -48,10 +49,12 @@ object BauweltMemberTable : CompositeIdTable("BauweltMember") { class BauweltMember(id: EntityID) : CompositeEntity(id) { companion object : CompositeEntityClass(BauweltMemberTable) { - private val cache = mutableMapOf() + // Keyed by (bauweltId, memberId). Optional.empty() caches "is not a member", which is the + // common case for spectators and would otherwise hit the DB on every permission check. + private val cache = ConcurrentHashMap, Optional>() private fun cache(member: BauweltMember) = - cache.put(member.memberID, member) + cache.put(member.bauweltID to member.memberID, Optional.of(member)) @JvmStatic fun clear() = @@ -75,20 +78,20 @@ class BauweltMember(id: EntityID) : CompositeEntity(id) { it[worldEdit] = false it[world] = false } - } + }.also { cache.remove(ownerId.value to newMemberId.value) } @JvmStatic @Deprecated("Use getBauMember(bauwelt: Int, member: Int)") fun getBauMember(bauwelt: UUID, member: UUID) = - useDb { - find { (bauweltId eq SteamwarUser.get(bauwelt)!!.id) and (BauweltMemberTable.memberId eq SteamwarUser.get(member)!!.id) }.firstOrNull()?.also { cache(it) } - } + getBauMember(SteamwarUser.get(bauwelt)!!.id.value, SteamwarUser.get(member)!!.id.value) @JvmStatic - fun getBauMember(bauwelt: Int, member: Int) = - useDb { - find { (bauweltId eq bauwelt) and (BauweltMemberTable.memberId eq member) }.firstOrNull()?.also { cache(it) } - } + fun getBauMember(bauwelt: Int, member: Int): BauweltMember? = + cache.computeIfAbsent(bauwelt to member) { + Optional.ofNullable(useDb { + find { (bauweltId eq bauwelt) and (BauweltMemberTable.memberId eq member) }.firstOrNull() + }) + }.orElse(null) @JvmStatic @Deprecated("Use getMembers(bauwelt: Int)") @@ -140,8 +143,11 @@ class BauweltMember(id: EntityID) : CompositeEntity(id) { fun isWorld() = world - fun remove() = + fun remove() { + val key = bauweltID to memberID useDb { delete() } + cache.remove(key) + } } \ No newline at end of file diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java index d97e1e46..04529020 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java @@ -19,6 +19,7 @@ package de.steamwar.velocitycore.commands; +import com.velocitypowered.api.proxy.server.RegisteredServer; import de.steamwar.command.PreviousArguments; import de.steamwar.command.SWCommand; import de.steamwar.command.TypeMapper; @@ -45,7 +46,9 @@ import de.steamwar.velocitycore.network.NetworkSender; import de.steamwar.velocitycore.util.BauLock; import java.io.File; +import java.util.ArrayList; import java.util.Collection; +import java.util.UUID; import java.util.function.Consumer; @Linked @@ -72,6 +75,7 @@ public class BauCommand extends SWCommand { @Register(value = "addmember", description = "BAU_ADDMEMBER_USAGE") public void addmember(Chatter sender, @Validator("addMemberTarget") SteamwarUser target) { BauweltMember.addMember(sender.user().getUUID(), target.getUUID()); + sendBaumemberUpdate(sender.user().getUUID()); sender.system("BAU_ADDMEMBER_ADDED"); Chatter.of(target.getUUID()).system("BAU_ADDMEMBER_ADDED_TARGET", sender); } @@ -161,10 +165,7 @@ public class BauCommand extends SWCommand { withMember(owner, user, target -> { setter.accept(target); - Bauserver bauserver = Bauserver.get(owner.user().getUUID()); - if (bauserver != null) { - bauserver.getRegisteredServer().getPlayersConnected().stream().findAny().ifPresent(player -> NetworkSender.send(player, new BaumemberUpdatePacket())); - } + sendBaumemberUpdate(owner.user().getUUID()); Chatter.of(user.getUUID()).system("BAU_MEMBER_SET_TARGET", owner, new Message(permName)); owner.system("BAU_MEMBER_SET", new Message(permName)); @@ -184,9 +185,7 @@ public class BauCommand extends SWCommand { } }); - if (bauserver != null) { - bauserver.getRegisteredServer().getPlayersConnected().stream().findAny().ifPresent(player -> NetworkSender.send(player, new BaumemberUpdatePacket())); - } + sendBaumemberUpdate(owner.user().getUUID()); try { deletePlayerData(owner, user); @@ -289,6 +288,23 @@ public class BauCommand extends SWCommand { BauLock.setLocked(sender, BauLockState.OPEN); } + /** + * Tells the owner's Bauserver and every dev server to drop their cached {@link BauweltMember}s, + * since those run in separate processes and would otherwise keep serving stale permissions. + * Plugin messages need a connected player, so servers without players are skipped. + */ + private static void sendBaumemberUpdate(UUID owner) { + Bauserver bauserver = Bauserver.get(owner); + if (bauserver != null) { + sendBaumemberUpdate(bauserver.getRegisteredServer()); + } + new ArrayList<>(DevCommand.getDevServers().values()).forEach(BauCommand::sendBaumemberUpdate); + } + + private static void sendBaumemberUpdate(RegisteredServer server) { + server.getPlayersConnected().stream().findAny().ifPresent(player -> NetworkSender.send(player, new BaumemberUpdatePacket())); + } + private static void withMember(Chatter owner, SteamwarUser member, Consumer function) { if (member == null) { owner.system("UNKNOWN_PLAYER"); From 9d5fcc07e8a2b2e9afcbce679a03b9e73bdc56f6 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 19 Oct 2026 08:24:43 +0000 Subject: [PATCH 36/43] Only notify the owner's dev server about Bauwelt member changes Dev servers are registered per owner (lowercase username), so there is no need to wake every dev server. --- .../src/de/steamwar/velocitycore/commands/BauCommand.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java index 04529020..67eadd80 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java @@ -46,7 +46,6 @@ import de.steamwar.velocitycore.network.NetworkSender; import de.steamwar.velocitycore.util.BauLock; import java.io.File; -import java.util.ArrayList; import java.util.Collection; import java.util.UUID; import java.util.function.Consumer; @@ -289,7 +288,7 @@ public class BauCommand extends SWCommand { } /** - * Tells the owner's Bauserver and every dev server to drop their cached {@link BauweltMember}s, + * Tells the owner's Bauserver and the owner's own dev server to drop their cached {@link BauweltMember}s, * since those run in separate processes and would otherwise keep serving stale permissions. * Plugin messages need a connected player, so servers without players are skipped. */ @@ -298,7 +297,10 @@ public class BauCommand extends SWCommand { if (bauserver != null) { sendBaumemberUpdate(bauserver.getRegisteredServer()); } - new ArrayList<>(DevCommand.getDevServers().values()).forEach(BauCommand::sendBaumemberUpdate); + RegisteredServer devServer = DevCommand.getDevServers().get(SteamwarUser.get(owner).getUserName().toLowerCase()); + if (devServer != null) { + sendBaumemberUpdate(devServer); + } } private static void sendBaumemberUpdate(RegisteredServer server) { From 3fe2e006b910c811a1f19a8566eb6bfae73cffd2 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 19 Oct 2026 08:32:16 +0000 Subject: [PATCH 37/43] REntityServer: use per-entity view range instead of chunk view distance Entities now decide per player whether they are visible. REntity defaults to the player view radius, RDisplay derives it from its viewRange (1 = 64 blocks) capped at the client view distance, so values like Float.MAX_VALUE stay cheap. Changing the viewRange re-evaluates the viewers. Fixes #488 --- .../src/de/steamwar/entity/RDisplay.java | 10 + .../src/de/steamwar/entity/REntity.java | 15 ++ .../src/de/steamwar/entity/REntityServer.java | 193 +++++++----------- 3 files changed, 101 insertions(+), 117 deletions(-) diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RDisplay.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RDisplay.java index d96d085c..d1c7cc98 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RDisplay.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RDisplay.java @@ -26,6 +26,7 @@ import net.minecraft.network.syncher.EntityDataSerializers; import org.bukkit.Color; import org.bukkit.Location; import org.bukkit.entity.Display; +import org.bukkit.entity.Player; import org.bukkit.entity.EntityType; import org.bukkit.util.Transformation; import org.joml.Quaternionf; @@ -135,6 +136,15 @@ public abstract class RDisplay extends REntity { public void setViewRange(float viewRange) { this.viewRange = viewRange; sendPacket(updatePacketSink, this::getViewRange); + server.updateViewers(this); + } + + /** + * A view range of 1 equals 64 blocks. This is only a comparison, so huge values like {@link Float#MAX_VALUE} are cheap. + */ + @Override + protected boolean canView(Player player, int chunkDistance) { + return chunkDistance <= Math.ceil(viewRange * 64.0 / 16.0); } private static final EntityDataAccessor viewRangeWatcher = new EntityDataAccessor<>(17, EntityDataSerializers.FLOAT); diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java index bbab5bbf..5d6f422f 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java @@ -39,6 +39,7 @@ import org.bukkit.Location; import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.craftbukkit.util.CraftNamespacedKey; import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import java.util.*; @@ -56,6 +57,10 @@ public class REntity { private static final Random random = new Random(); protected final REntityServer server; + /** + * Players this entity is currently spawned for, maintained by the {@link REntityServer}. + */ + final Set viewers = new HashSet<>(); private final EntityType entityType; @Getter protected final int entityId; @@ -120,6 +125,16 @@ public class REntity { this.objectData = objectData; } + /** + * Whether the given player should see this entity. + * Defaults to the view distance of the player. + * + * @param chunkDistance the larger of the chunk distances between the player and the entity on the x and z axis + */ + protected boolean canView(Player player, int chunkDistance) { + return chunkDistance <= server.viewRadius(player); + } + public void hide(boolean hide) { if (hidden == hide) return; diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java index bff64bec..046c55f5 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java @@ -42,14 +42,8 @@ import java.util.stream.Collectors; public class REntityServer implements Listener { - private static final HashSet emptyEntities = new HashSet<>(0); - private static final HashSet emptyPlayers = new HashSet<>(0); - private final ConcurrentHashMap entityMap = new ConcurrentHashMap<>(); - private final HashMap> entities = new HashMap<>(); - private final HashMap> players = new HashMap<>(); private final HashMap lastLocation = new HashMap<>(); - private final HashMap viewDistance = new HashMap<>(); private final Set playersThatClicked = Collections.synchronizedSet(new HashSet<>()); @@ -78,20 +72,22 @@ public class REntityServer implements Listener { public void addPlayer(Player player) { Location location = player.getLocation(); lastLocation.put(player, location); - viewDistance.put(player, viewRadius(player)); - forChunkInView(player, location, (x, z) -> addPlayerToChunk(player, x, z)); + updateViewer(player, posToChunk(location.getX()), posToChunk(location.getZ())); } public void removePlayer(Player player) { - if (!viewDistance.containsKey(player)) { + if (lastLocation.remove(player) == null) { return; } - forChunkInView(player, lastLocation.remove(player), (x, z) -> removePlayerFromChunk(player, x, z)); - viewDistance.remove(player); + for (REntity entity : entityMap.values()) { + if (entity.viewers.remove(player)) { + hideFrom(entity, player); + } + } } public List getPlayers() { - return new ArrayList<>(viewDistance.keySet()); + return new ArrayList<>(lastLocation.keySet()); } public void close() { @@ -104,43 +100,53 @@ public class REntityServer implements Listener { void addEntity(REntity entity) { entityMap.put(entity.entityId, entity); - addEntityToChunk(entity); + updateViewers(entity); entity.list(packet -> updateEntity(entity, packet)); entity.spawn(packet -> updateEntity(entity, packet)); } void preEntityMove(REntity entity, double toX, double toZ) { - long fromId = entityToId(entity); - long toId = posToId(toX, toZ); - if (fromId == toId) return; + if (posToChunk(entity.x()) == posToChunk(toX) && posToChunk(entity.z()) == posToChunk(toZ)) return; - if (!entity.isHidden()) { - onMissing(players.get(fromId), players.get(toId), entity::despawn); - } - onMissing(players.get(fromId), players.get(toId), entity::delist); - removeEntityFromChunk(entity); + int chunkX = posToChunk(toX); + int chunkZ = posToChunk(toZ); + entity.viewers.removeIf(player -> { + if (inView(entity, player, chunkX, chunkZ)) return false; + hideFrom(entity, player); + return true; + }); } void postEntityMove(REntity entity, double fromX, double fromZ) { - long fromId = posToId(fromX, fromZ); - long toId = entityToId(entity); - if (fromId == toId) return; + if (posToChunk(fromX) == posToChunk(entity.x()) && posToChunk(fromZ) == posToChunk(entity.z())) return; - addEntityToChunk(entity); - onMissing(players.get(toId), players.get(fromId), entity::list); - if (!entity.isHidden()) { - onMissing(players.get(toId), players.get(fromId), entity::spawn); - } + addMissingViewers(entity); } void updateEntity(REntity entity, Object packet) { if (entity.isHidden()) return; - for (Player player : players.getOrDefault(entityToId(entity), emptyPlayers)) { + for (Player player : entity.viewers) { TinyProtocol.instance.sendPacket(player, packet); } } + /** + * Re-evaluates which players see the entity, e.g. after its view range changed. + */ + void updateViewers(REntity entity) { + if (!entityMap.containsKey(entity.entityId)) return; + + int chunkX = posToChunk(entity.x()); + int chunkZ = posToChunk(entity.z()); + entity.viewers.removeIf(player -> { + if (inView(entity, player, chunkX, chunkZ)) return false; + hideFrom(entity, player); + return true; + }); + addMissingViewers(entity); + } + /** * Re-sends list and spawn packets of an already added entity to all current viewers, * e.g. when information that is part of the tab list entry changed after the entity was added. @@ -148,7 +154,7 @@ public class REntityServer implements Listener { void refreshEntity(REntity entity) { if (!entityMap.containsKey(entity.entityId)) return; - for (Player player : players.getOrDefault(entityToId(entity), emptyPlayers)) { + for (Player player : entity.viewers) { Consumer sink = packet -> TinyProtocol.instance.sendPacket(player, packet); if (!entity.isHidden()) entity.despawn(sink); entity.delist(sink); @@ -159,11 +165,12 @@ public class REntityServer implements Listener { void removeEntity(REntity entity) { entity.despawn(packet -> updateEntity(entity, packet)); - removeEntityFromChunk(entity); entity.delist(packet -> updateEntity(entity, packet)); + entity.viewers.clear(); entityMap.remove(entity.entityId); } + public List getEntities() { return new ArrayList<>(entityMap.values()); } @@ -172,18 +179,6 @@ public class REntityServer implements Listener { return entityMap.values().stream().filter(clazz::isInstance).map(clazz::cast).collect(Collectors.toList()); } - private void addEntityToChunk(REntity entity) { - entities.computeIfAbsent(entityToId(entity), i -> new HashSet<>()).add(entity); - } - - private void removeEntityFromChunk(REntity entity) { - long id = entityToId(entity); - HashSet entitiesInChunk = entities.get(id); - if (entitiesInChunk == null) return; - entitiesInChunk.remove(entity); - if (entitiesInChunk.isEmpty()) entities.remove(id); - } - @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) public void onMove(PlayerMoveEvent e) { Player player = e.getPlayer(); @@ -191,90 +186,63 @@ public class REntityServer implements Listener { Location to = e.getTo(); if (from == null || to == null) return; - int fromX = posToChunk(from.getX()); - int fromZ = posToChunk(from.getZ()); int toX = posToChunk(to.getX()); int toZ = posToChunk(to.getZ()); - if (fromX == toX && fromZ == toZ) return; + if (posToChunk(from.getX()) == toX && posToChunk(from.getZ()) == toZ) return; lastLocation.put(player, to); - - int toViewDistance = viewRadius(player); - forChunkInView(player, from, (x, z) -> { - if (Math.abs(x - toX) > toViewDistance || Math.abs(z - toZ) > toViewDistance) { - removePlayerFromChunk(player, x, z); - } - }); - - int fromViewDistance = this.viewDistance.put(player, toViewDistance); - forChunkInView(player, to, (x, z) -> { - if (Math.abs(x - fromX) > fromViewDistance || Math.abs(z - fromZ) > fromViewDistance) { - addPlayerToChunk(player, x, z); - } - }); + updateViewer(player, toX, toZ); } @EventHandler public void onQuit(PlayerQuitEvent e) { Player player = e.getPlayer(); - Location location = lastLocation.remove(player); - if (location == null) return; + if (lastLocation.remove(player) == null) return; - forChunkInView(player, location, (x, z) -> { - long id = chunkToId(x, z); - Set playersInChunk = players.get(id); - playersInChunk.remove(player); - if (playersInChunk.isEmpty()) players.remove(id); - }); - viewDistance.remove(player); + for (REntity entity : entityMap.values()) { + entity.viewers.remove(player); + } } - private void onMissing(Set of, Set in, Consumer> packetProvider) { - if (of == null) return; - - for (Player player : of) { - if (in == null || !in.contains(player)) { - packetProvider.accept(packet -> TinyProtocol.instance.sendPacket(player, packet)); + private void updateViewer(Player player, int chunkX, int chunkZ) { + for (REntity entity : entityMap.values()) { + boolean inView = inView(entity, player, chunkX, chunkZ); + if (inView) { + if (entity.viewers.add(player)) showTo(entity, player); + } else if (entity.viewers.remove(player)) { + hideFrom(entity, player); } } } - private void forChunkInView(Player player, Location location, BiConsumer func) { - int chunkX = posToChunk(location.getX()); - int chunkZ = posToChunk(location.getZ()); - int viewDistance = this.viewDistance.get(player); - - for (int x = chunkX - viewDistance; x <= chunkX + viewDistance; x++) { - for (int z = chunkZ - viewDistance; z <= chunkZ + viewDistance; z++) { - func.accept(x, z); + private void addMissingViewers(REntity entity) { + int chunkX = posToChunk(entity.x()); + int chunkZ = posToChunk(entity.z()); + for (Player player : lastLocation.keySet()) { + if (inView(entity, player, chunkX, chunkZ) && entity.viewers.add(player)) { + showTo(entity, player); } } } - private void addPlayerToChunk(Player player, int x, int z) { - long id = chunkToId(x, z); - players.computeIfAbsent(id, i -> new HashSet<>()).add(player); - for (REntity entity : entities.getOrDefault(id, emptyEntities)) { - entity.list(packet -> TinyProtocol.instance.sendPacket(player, packet)); - if (!entity.isHidden()) { - entity.spawn(packet -> TinyProtocol.instance.sendPacket(player, packet)); - } + private boolean inView(REntity entity, Player player, int entityChunkX, int entityChunkZ) { + Location location = lastLocation.get(player); + int distance = Math.max(Math.abs(posToChunk(location.getX()) - entityChunkX), Math.abs(posToChunk(location.getZ()) - entityChunkZ)); + return entity.canView(player, distance); + } + + private void showTo(REntity entity, Player player) { + entity.list(packet -> TinyProtocol.instance.sendPacket(player, packet)); + if (!entity.isHidden()) { + entity.spawn(packet -> TinyProtocol.instance.sendPacket(player, packet)); } } - private void removePlayerFromChunk(Player player, int x, int z) { - long id = chunkToId(x, z); - - Set playersInChunk = players.get(id); - playersInChunk.remove(player); - if (playersInChunk.isEmpty()) players.remove(id); - - for (REntity entity : entities.getOrDefault(id, emptyEntities)) { - if (!entity.isHidden()) { - entity.despawn(packet -> TinyProtocol.instance.sendPacket(player, packet)); - } - entity.delist(packet -> TinyProtocol.instance.sendPacket(player, packet)); + private void hideFrom(REntity entity, Player player) { + if (!entity.isHidden()) { + entity.despawn(packet -> TinyProtocol.instance.sendPacket(player, packet)); } + entity.delist(packet -> TinyProtocol.instance.sendPacket(player, packet)); } public void tick() { @@ -287,19 +255,10 @@ public class REntityServer implements Listener { return (int) (coord / 16) - (coord < 0 ? 1 : 0); } - private int viewRadius(Player player) { + /** + * Default view radius in chunks for entities without a view range of their own. + */ + int viewRadius(Player player) { return player.getClientViewDistance() / 2; } - - private long entityToId(REntity entity) { - return posToId(entity.x(), entity.z()); - } - - private long posToId(double x, double z) { - return chunkToId(posToChunk(x), posToChunk(z)); - } - - private long chunkToId(int x, int z) { - return ((long) x << 32) + z; - } } From 8e3e71dd962f8f72652b42d63e975c572fe93cb7 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 19 Oct 2026 08:38:46 +0000 Subject: [PATCH 38/43] CLine: render as a single display shown by view distance The line no longer needs start, middle and end parts that were moved around the player. It is one display that is visible for a player as soon as any chunk the line passes through is in their view distance. REntity#canView now receives the chunk positions of player and entity. --- .../src/de/steamwar/entity/CLine.java | 185 +++++------------- .../src/de/steamwar/entity/RDisplay.java | 4 +- .../src/de/steamwar/entity/REntity.java | 16 +- .../src/de/steamwar/entity/REntityServer.java | 3 +- 4 files changed, 63 insertions(+), 145 deletions(-) diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/CLine.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/CLine.java index 1f877cce..a7628c40 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/CLine.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/CLine.java @@ -19,21 +19,23 @@ package de.steamwar.entity; +import lombok.AccessLevel; import lombok.Getter; import org.bukkit.Location; import org.bukkit.block.data.BlockData; import org.bukkit.entity.Display; import org.bukkit.entity.Player; -import org.bukkit.util.Consumer; import org.bukkit.util.Transformation; import org.bukkit.util.Vector; import org.joml.Quaternionf; import org.joml.Vector3f; import java.util.Objects; +import java.util.function.Consumer; /** * Can be used for Axis Aligned Lines of near infinite length. + * The line is a single display, which is shown to players based on their view distance. */ @Getter public class CLine extends CEntity { @@ -42,6 +44,8 @@ public class CLine extends CEntity { private static final float offset = 1 / 1024f; private static final Vector offsetVec = new Vector(offset, offset, offset); + @Getter(AccessLevel.NONE) + private LineDisplay line; private Location from; private Location to; private float width = DEFAULT_WIDTH; @@ -99,9 +103,7 @@ public class CLine extends CEntity { if (hide == this.hide) return; this.hide = hide; if (hide) { - if (startLine != null) startLine.hide(true); - if (middleLine != null) middleLine.hide(true); - if (endLine != null) endLine.hide(true); + if (line != null) line.hide(true); } else { tick(); } @@ -109,148 +111,26 @@ public class CLine extends CEntity { @Override void tick() { - if (from == null || to == null) { - if (startLine != null) startLine.hide(true); - if (middleLine != null) middleLine.hide(true); - if (endLine != null) endLine.hide(true); - return; - } else { - if (startLine != null) startLine.hide(false); - if (middleLine != null) middleLine.hide(false); - if (endLine != null) endLine.hide(false); - } - if (hide) return; - updateStart(); - updateMiddle(); - updateEnd(); - } - - private RBlockDisplay startLine; - - private void updateStart() { - Vector vec = to.clone().subtract(from).toVector(); - if (vec.length() > 35) vec.normalize().multiply(35); - - if (startLine == null) { - startLine = new RBlockDisplay(server, new Location(null, 0, 0, 0)); - startLine.setBrightness(new Display.Brightness(15, 15)); - startLine.setViewRange(560); - startLine.setBlock(blockData); - entities.add(startLine); - } else { - startLine.hide(false); - } - - startLine.move(from.clone().subtract(offsetVec)); - startLine.setTransform(new Transformation(new Vector3f(0, 0, 0), new Quaternionf(0, 0, 0, 1), addWidth(vec).toVector3f(), new Quaternionf(0, 0, 0, 1))); - } - - private RBlockDisplay middleLine; - - private void updateMiddle() { - Vector vec = to.clone().subtract(from).toVector(); - if (vec.length() <= 70) { - if (middleLine != null) middleLine.hide(true); + if (from == null || to == null || hide) { + if (line != null) line.hide(true); return; } - if (vec.length() > 280) vec.normalize().multiply(280); - else vec = vec.clone().normalize().multiply(vec.length() - 60); - if (middleLine == null) { - middleLine = new RBlockDisplay(server, new Location(null, 0, 0, 0)); - middleLine.setBrightness(new Display.Brightness(15, 15)); - middleLine.setViewRange(560); - middleLine.setBlock(blockData); - entities.add(middleLine); + if (line == null) { + line = new LineDisplay(server); + line.setBrightness(new Display.Brightness(15, 15)); + line.setViewRange(560); + line.setBlock(blockData); + entities.add(line); } else { - middleLine.hide(false); + line.hide(false); } - Player player = server.getPlayers().stream().findFirst().orElse(null); - if (player == null) return; - - Vector tempVector = vec.clone().normalize().multiply(30); - Location from = this.from.clone().add(tempVector); - Location to = this.to.clone().subtract(tempVector); - - Vector lineVec = to.clone().subtract(from).toVector(); - Vector playerVec = player.getLocation().toVector().subtract(from.toVector()); - double lineVecDotItself = lineVec.dot(lineVec); - Vector projectionVec = lineVec.clone().multiply(lineVec.dot(playerVec)).divide(new Vector(lineVecDotItself, lineVecDotItself, lineVecDotItself)); - - Vector moveVec = from.toVector().add(projectionVec); - if (moveVec.getX() < from.getX()) { - moveVec.setX(from.getX()); - } - if (moveVec.getX() > to.getX()) { - moveVec.setX(to.getX()); - } - if (moveVec.getY() < from.getY()) { - moveVec.setY(from.getY()); - } - if (moveVec.getY() > to.getY()) { - moveVec.setY(to.getY()); - } - if (moveVec.getZ() < from.getZ()) { - moveVec.setZ(from.getZ()); - } - if (moveVec.getZ() > to.getZ()) { - moveVec.setZ(to.getZ()); - } - - Vector translation = vec.clone().divide(new Vector(2, 2, 2)); - translation.setX(-translation.getX()); - translation.setY(-translation.getY()); - translation.setZ(-translation.getZ()); - - Vector first = moveVec.clone().add(translation).subtract(from.toVector()); - if (first.getX() < 0) { - translation.setX(translation.getX() - first.getX()); - } - if (first.getY() < 0) { - translation.setY(translation.getY() - first.getY()); - } - if (first.getZ() < 0) { - translation.setZ(translation.getZ() - first.getZ()); - } - - Vector second = to.toVector().subtract(moveVec.clone().subtract(translation)); - if (second.getX() < 0) { - translation.setX(translation.getX() + second.getX()); - } - if (second.getY() < 0) { - translation.setY(translation.getY() + second.getY()); - } - if (second.getZ() < 0) { - translation.setZ(translation.getZ() + second.getZ()); - } - - middleLine.move(moveVec.toLocation(player.getWorld()).subtract(offsetVec)); - middleLine.setTransform(new Transformation(translation.toVector3f(), new Quaternionf(0, 0, 0, 1), addWidth(vec).toVector3f(), new Quaternionf(0, 0, 0, 1))); - } - - private RBlockDisplay endLine; - - private void updateEnd() { Vector vec = to.clone().subtract(from).toVector(); - if (vec.length() <= 35) { - if (endLine != null) endLine.hide(true); - return; - } - if (vec.length() > 35) vec.normalize().multiply(35); - - if (endLine == null) { - endLine = new RBlockDisplay(server, new Location(null, 0, 0, 0)); - endLine.setBrightness(new Display.Brightness(15, 15)); - endLine.setViewRange(560); - endLine.setBlock(blockData); - entities.add(endLine); - } else { - endLine.hide(false); - } - - endLine.move(to.clone().subtract(offsetVec)); - endLine.setTransform(new Transformation(vec.toVector3f().negate(), new Quaternionf(0, 0, 0, 1), addWidth(vec).toVector3f(), new Quaternionf(0, 0, 0, 1))); + line.setSpan(from, to); + line.move(from.clone().subtract(offsetVec)); + line.setTransform(new Transformation(new Vector3f(0, 0, 0), new Quaternionf(0, 0, 0, 1), addWidth(vec).toVector3f(), new Quaternionf(0, 0, 0, 1))); + server.updateViewers(line); } private Vector addWidth(Vector vector) { @@ -267,4 +147,31 @@ public class CLine extends CEntity { vector.add(offsetVec).add(offsetVec); return vector; } + + /** + * The line is one display that is visible for a player as soon as any chunk it passes through is in the view distance, + * independent of the chunk its origin is in. + */ + private static class LineDisplay extends RBlockDisplay { + + private int minChunkX, maxChunkX, minChunkZ, maxChunkZ; + + private LineDisplay(REntityServer server) { + super(server, new Location(null, 0, 0, 0)); + } + + private void setSpan(Location from, Location to) { + minChunkX = Math.floorDiv((int) Math.floor(Math.min(from.getX(), to.getX())), 16); + maxChunkX = Math.floorDiv((int) Math.floor(Math.max(from.getX(), to.getX())), 16); + minChunkZ = Math.floorDiv((int) Math.floor(Math.min(from.getZ(), to.getZ())), 16); + maxChunkZ = Math.floorDiv((int) Math.floor(Math.max(from.getZ(), to.getZ())), 16); + } + + @Override + protected boolean canView(Player player, int playerChunkX, int playerChunkZ, int entityChunkX, int entityChunkZ) { + int distanceX = Math.max(0, Math.max(minChunkX - playerChunkX, playerChunkX - maxChunkX)); + int distanceZ = Math.max(0, Math.max(minChunkZ - playerChunkZ, playerChunkZ - maxChunkZ)); + return inViewDistance(player, Math.max(distanceX, distanceZ)); + } + } } diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RDisplay.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RDisplay.java index d1c7cc98..28d9744a 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RDisplay.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RDisplay.java @@ -143,8 +143,8 @@ public abstract class RDisplay extends REntity { * A view range of 1 equals 64 blocks. This is only a comparison, so huge values like {@link Float#MAX_VALUE} are cheap. */ @Override - protected boolean canView(Player player, int chunkDistance) { - return chunkDistance <= Math.ceil(viewRange * 64.0 / 16.0); + protected boolean canView(Player player, int playerChunkX, int playerChunkZ, int entityChunkX, int entityChunkZ) { + return chunkDistance(playerChunkX, playerChunkZ, entityChunkX, entityChunkZ) <= Math.ceil(viewRange * 64.0 / 16.0); } private static final EntityDataAccessor viewRangeWatcher = new EntityDataAccessor<>(17, EntityDataSerializers.FLOAT); diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java index 5d6f422f..8b6697ed 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java @@ -129,9 +129,21 @@ public class REntity { * Whether the given player should see this entity. * Defaults to the view distance of the player. * - * @param chunkDistance the larger of the chunk distances between the player and the entity on the x and z axis + * @param playerChunkX the chunk the player is in + * @param entityChunkX the chunk the entity is in, the entity itself might not be moved there yet */ - protected boolean canView(Player player, int chunkDistance) { + protected boolean canView(Player player, int playerChunkX, int playerChunkZ, int entityChunkX, int entityChunkZ) { + return inViewDistance(player, chunkDistance(playerChunkX, playerChunkZ, entityChunkX, entityChunkZ)); + } + + /** + * @return the larger of the chunk distances on the x and z axis + */ + protected static int chunkDistance(int chunkX1, int chunkZ1, int chunkX2, int chunkZ2) { + return Math.max(Math.abs(chunkX1 - chunkX2), Math.abs(chunkZ1 - chunkZ2)); + } + + protected boolean inViewDistance(Player player, int chunkDistance) { return chunkDistance <= server.viewRadius(player); } diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java index 046c55f5..005dbf42 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java @@ -227,8 +227,7 @@ public class REntityServer implements Listener { private boolean inView(REntity entity, Player player, int entityChunkX, int entityChunkZ) { Location location = lastLocation.get(player); - int distance = Math.max(Math.abs(posToChunk(location.getX()) - entityChunkX), Math.abs(posToChunk(location.getZ()) - entityChunkZ)); - return entity.canView(player, distance); + return entity.canView(player, posToChunk(location.getX()), posToChunk(location.getZ()), entityChunkX, entityChunkZ); } private void showTo(REntity entity, Player player) { From 9935ab785ddb09b4e13b6aa5f2b28226eae665e4 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 19 Oct 2026 11:21:15 +0200 Subject: [PATCH 39/43] Fix NoClipCommand --- .../features/util/NoClipCommand.java | 121 +++++------------- 1 file changed, 31 insertions(+), 90 deletions(-) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NoClipCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NoClipCommand.java index 91a79f5e..9994e3d4 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NoClipCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NoClipCommand.java @@ -22,123 +22,71 @@ package de.steamwar.bausystem.features.util; import com.comphenix.tinyprotocol.TinyProtocol; import com.mojang.authlib.GameProfile; import de.steamwar.bausystem.BauSystem; -import de.steamwar.bausystem.features.tpslimit.TPSUtils; import de.steamwar.bausystem.utils.BauMemberUpdateEvent; import de.steamwar.command.SWCommand; import de.steamwar.core.ProtocolWrapper; import de.steamwar.core.SWPlayer; import de.steamwar.linkage.Linked; -import net.minecraft.network.protocol.game.*; -import net.minecraft.world.entity.player.Abilities; -import net.minecraft.world.level.GameType; -import org.bukkit.Bukkit; +import io.papermc.paper.event.player.PlayerFailMoveEvent; +import net.minecraft.network.protocol.game.ClientboundGameEventPacket; import org.bukkit.GameMode; -import org.bukkit.craftbukkit.entity.CraftPlayer; -import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.block.BlockCanBuildEvent; import org.bukkit.event.player.PlayerGameModeChangeEvent; -import org.bukkit.event.player.PlayerTeleportEvent; import org.bukkit.event.player.PlayerToggleFlightEvent; -import java.util.function.BiFunction; - @Linked public class NoClipCommand extends SWCommand implements Listener { public static class NoClipData implements SWPlayer.Component { - private long lastTick = -1; - @Override - public void onUnmount(SWPlayer player) { - player.getPlayer().setGameMode(GameMode.CREATIVE); + public void onUnmount(SWPlayer swPlayer) { + Player player = swPlayer.getPlayer(); + showClientGameMode(player, GameMode.CREATIVE, GameMode.CREATIVE); } } public NoClipCommand() { super("noclip", "nc"); - - BiFunction first = (player, o) -> { - NoClipData noClipData = SWPlayer.of(player).getComponent(NoClipData.class).orElse(null); - if (noClipData == null) return o; - if (noClipData.lastTick == TPSUtils.currentTick.get()) return o; - setInternalGameMode(player, GameMode.SPECTATOR); - noClipData.lastTick = TPSUtils.currentTick.get(); - return o; - }; - TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.Pos.class, first); - TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.PosRot.class, first); - - BiFunction second = (player, o) -> { - NoClipData noClipData = SWPlayer.of(player).getComponent(NoClipData.class).orElse(null); - if (noClipData == null) return o; - setInternalGameMode(player, GameMode.CREATIVE); - noClipData.lastTick = TPSUtils.currentTick.get(); - return o; - }; - TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, second); - TinyProtocol.instance.addFilter(ServerboundPlayerActionPacket.class, second); - TinyProtocol.instance.addFilter(ServerboundContainerClickPacket.class, second); - - BiFunction third = (player, o) -> { - if (SWPlayer.of(player).hasComponent(NoClipData.class)) { - int index = o.slotNum(); - if (index >= 36 && index <= 44) { - index -= 36; - } else if (index > 44) { - index -= 5; - } else if (index <= 8) { - index = index - 8 + 36; - } - player.getInventory().setItem(index, CraftItemStack.asBukkitCopy(o.itemStack())); - if (index < 9) player.getInventory().setHeldItemSlot(index); - player.updateInventory(); - } - return o; - }; - TinyProtocol.instance.addFilter(ServerboundSetCreativeModeSlotPacket.class, third); } - private void setInternalGameMode(Player player, GameMode gameMode) { - ((CraftPlayer) player).getHandle().gameMode.gameModeForPlayer = GameType.byId(gameMode.getValue()); - } - - @Register(help = true) + @Register public void genericCommand(@Validator Player player) { SWPlayer swPlayer = SWPlayer.of(player); if (swPlayer.hasComponent(NoClipData.class)) { swPlayer.removeComponent(NoClipData.class); } else { - player.setGameMode(GameMode.SPECTATOR); - Abilities abilities = (((CraftPlayer) player).getHandle()).getAbilities(); - abilities.mayBuild = true; - abilities.mayfly = true; - abilities.instabuild = true; - + player.setGameMode(GameMode.CREATIVE); + player.setAllowFlight(true); + player.setFlying(true); swPlayer.setComponent(new NoClipData()); BauSystem.MESSAGE.send("OTHER_NOCLIP_SLOT_INFO", player); - TinyProtocol.instance.sendPacket(player, new ClientboundGameEventPacket(ClientboundGameEventPacket.CHANGE_GAME_MODE, 1F)); - pseudoGameMode(player, GameMode.SPECTATOR); + showClientGameMode(player, GameMode.CREATIVE, GameMode.SPECTATOR); } } @EventHandler - public void onBauMemberUpdate(BauMemberUpdateEvent event) { - event.getNewSpectator().forEach(player -> { - SWPlayer.of(player).removeComponent(NoClipData.class); - }); + public void onPlayerFailMove(PlayerFailMoveEvent event) { + if (event.getFailReason() != PlayerFailMoveEvent.FailReason.CLIPPED_INTO_BLOCK) return; + if (!SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) return; + event.setAllowed(true); } - @EventHandler(ignoreCancelled = true) + @EventHandler + public void onBauMemberUpdate(BauMemberUpdateEvent event) { + event.getNewSpectator().forEach(player -> SWPlayer.of(player).removeComponent(NoClipData.class)); + } + + @EventHandler public void onPlayerGameModeChange(PlayerGameModeChangeEvent event) { if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) { event.setCancelled(true); } } - @EventHandler(ignoreCancelled = true) + @EventHandler public void onBlock(BlockCanBuildEvent event) { if (event.getPlayer() == null) return; if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) { @@ -146,7 +94,7 @@ public class NoClipCommand extends SWCommand implements Listener { } } - @EventHandler(ignoreCancelled = true) + @EventHandler public void onPlayerToggleFlight(PlayerToggleFlightEvent event) { if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) { event.setCancelled(true); @@ -154,20 +102,13 @@ public class NoClipCommand extends SWCommand implements Listener { } } - @EventHandler(ignoreCancelled = true) - public void onPlayerTeleport(PlayerTeleportEvent event) { - if (event.getCause() != PlayerTeleportEvent.TeleportCause.SPECTATE) { - return; - } - if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) { - event.setCancelled(true); - Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { - event.getPlayer().setSpectatorTarget(null); - }, 1L); - } + /** + * @param player player + * @param localMode GameMode for breaking and placing + * @param listedMode GameMode for noPhysics + */ + private static void showClientGameMode(Player player, GameMode localMode, GameMode listedMode) { + TinyProtocol.instance.sendPacket(player, new ClientboundGameEventPacket(ClientboundGameEventPacket.CHANGE_GAME_MODE, localMode.getValue())); + TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.GAMEMODE, new GameProfile(player.getUniqueId(), player.getName()), listedMode)); } - - private static void pseudoGameMode(Player player, GameMode gameMode) { - TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.GAMEMODE, new GameProfile(player.getUniqueId(), player.getName()), gameMode)); - } -} \ No newline at end of file +} From 11798c3ec00b9cfaf4ca9e3d1944fafc2e9e4790 Mon Sep 17 00:00:00 2001 From: Lukas <54+12312@noreply.localhost> Date: Mon, 19 Oct 2026 22:19:33 +0200 Subject: [PATCH 40/43] Fixed Replay not starting for Normal Users Fixed Replay not starting for Normal Users. --- .../src/de/steamwar/velocitycore/commands/ReplayCommand.java | 1 - 1 file changed, 1 deletion(-) diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ReplayCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ReplayCommand.java index dc6af940..3f5802c5 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/ReplayCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ReplayCommand.java @@ -65,7 +65,6 @@ public class ReplayCommand extends SWCommand { private void startReplay(PlayerChatter sender, boolean isAdmin, String optionalMap, Fight fight) { if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.NoFightServer)) return; - if (!sender.user().hasPerm(UserPerm.TEAM)) return; GameModeConfig mode = ArenaMode.getBySchemType(fight.getSchemType()); ServerStarter starter = new ServerStarter().replay(fight.getFightID()).blueLeader(sender.getPlayer()); From c238b9bdf2992122810da9ce596e9896a4b48207 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Wed, 21 Oct 2026 08:02:01 +0200 Subject: [PATCH 41/43] Improve CheckCommand --- .../velocitycore/commands/CheckCommand.java | 57 +------------------ 1 file changed, 1 insertion(+), 56 deletions(-) diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/CheckCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/CheckCommand.java index 289f22dc..b130a2d7 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/CheckCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/CheckCommand.java @@ -156,8 +156,7 @@ public class CheckCommand extends SWCommand { } SchematicNode schem = SchematicNode.getSchematicNode(Integer.parseInt(schemID)); - GameModeConfig gameModeConfig = ArenaMode.getBySchemType(schem.getSchemtype()); - if (!gameModeConfig.Checkers.isEmpty() && !gameModeConfig.Checkers.contains(sender.user().getId())) { + if (!mayCheck(sender.user(), schem)) { sender.system("CHECK_SCHEMATIC_EVENT"); return; } @@ -313,60 +312,6 @@ public class CheckCommand extends SWCommand { } private void accept() { - // TODO: This Code is only for the WGS and because YoyoNow is not available this can be removed or changed after the WGS! - if (schematic.getSchemtype().toDB().equals("cwargearseason26")) { - int userId = schematic.getOwner(); - SteamwarUser user = SteamwarUser.byId(userId); - int teamId = user.getTeam(); - - SchematicNode teamFolder = SchematicNode.getSchematicNodeInNode(172325) - .stream() - .filter(schematicNode -> schematicNode.getName().startsWith(teamId + "_")) - .findFirst() - .orElse(null); - if (teamFolder == null) { - internalAccept(); - return; - } - - // Remove any added players from the schematic in the folder - for (SchematicNode schematicNode : SchematicNode.getSchematicNodeInNode(teamFolder.getNodeId())) { - for (NodeMember nodeMember : NodeMember.getNodeMembers(schematicNode.getNodeId())) { - nodeMember.delete(); - } - } - - // Copy Schem into team folder of -1 user - String name = DateTimeFormatter.ofPattern("yyyy.MM.dd_HH:mm:ss").format(schematic.getLastUpdate().toLocalDateTime()); - NodeData data = NodeData.getLatest(schematic); - SchematicNode node = SchematicNode.createSchematic(-1, name, teamFolder.getNodeId()); - NodeData.saveFromStream(node, data.schemData(false), data.getNodeFormat()); - - // Accept the team folder schematic and set other to Normal as well as adding the original owner on the schematic - node.setSchemtype(GameModeConfig.getBySchematicType(schematic.getSchemtype()).Schematic.Type); - NodeMember.createNodeMember(node.getNodeId(), schematic.getOwner()); - for (NodeMember nodeMember : NodeMember.getNodeMembers(schematic.getId())) { - NodeMember.createNodeMember(node.getNodeId(), nodeMember.getMember()); - } - - // Conclude by setting send in schematic to normal and broadcast - concludeCheckSession("freigegeben", SchematicType.Normal, () -> { - Chatter owner = Chatter.of(SteamwarUser.byId(schematic.getOwner()).getUUID()); - owner.withPlayerOrOffline( - player -> owner.system("CHECK_ACCEPTED", schematic.getSchemtype().name(), schematic.getName()), - () -> DiscordAlert.send(owner, Color.GREEN, new Message("DC_TITLE_SCHEMINFO"), new Message("DC_SCHEM_ACCEPT", schematic.getName()), true) - ); - notifyTeam(new Message("CHECK_ACCEPTED_TEAM", schematic.getName(), owner.user().getUserName())); - - return owner.getPlayer() != null; - }); - return; - } - - internalAccept(); - } - - private void internalAccept() { concludeCheckSession("freigegeben", GameModeConfig.getBySchematicType(schematic.getSchemtype()).Schematic.Type, () -> { Chatter owner = Chatter.of(SteamwarUser.byId(schematic.getOwner()).getUUID()); owner.withPlayerOrOffline( From c4b46e44311b002bf0238a6592b6156bf0d73877 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Wed, 21 Oct 2026 08:22:05 +0200 Subject: [PATCH 42/43] Add override for Publics in GameModeConfig.Schematic --- .../src/de/steamwar/sql/GameModeConfig.java | 10 ++++ .../de/steamwar/fightsystem/ai/DummyAI.java | 10 +--- .../fightsystem/fight/FightSchematic.java | 47 +++++++++++++------ 3 files changed, 44 insertions(+), 23 deletions(-) diff --git a/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java b/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java index 2292ed86..7c6d369a 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java +++ b/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java @@ -617,6 +617,15 @@ public final class GameModeConfig { */ public final List SubTypes; + /** + * List of publics that should be used. Mainly useful for an Event Mode + * where a subset of the regular publics should be used without duplicating + * those in the Database. + * + * @implSpec {@code []} by default, thus the GameModes publics are used. + */ + public final List Publics; + /** * Shortcut of the schematic type * @@ -725,6 +734,7 @@ public final class GameModeConfig { Inset = new InsetConfig(loader.with("Inset")); Type = null; SubTypesStrings = loader.getStringList("SubTypes"); + Publics = loader.getIntList("Publics"); SubTypes = new ArrayList<>(); Shortcut = loader.getString("Shortcut", ""); Material = loader.getMaterial("Material", "STONE_BUTTON"); diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/DummyAI.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/DummyAI.java index f4a5bfb0..f74ad6b9 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/DummyAI.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/DummyAI.java @@ -20,19 +20,14 @@ package de.steamwar.fightsystem.ai; import com.sk89q.worldedit.extent.clipboard.Clipboard; -import de.steamwar.fightsystem.Config; +import de.steamwar.fightsystem.fight.FightSchematic; import de.steamwar.fightsystem.fight.FightTeam; import de.steamwar.fightsystem.states.FightState; import de.steamwar.sql.SchematicNode; import de.steamwar.sql.SteamwarUser; -import java.util.List; -import java.util.Random; - public class DummyAI extends AI { - private static final Random random = new Random(); - public DummyAI(FightTeam team) { super(team, SteamwarUser.get("public")); @@ -41,8 +36,7 @@ public class DummyAI extends AI { @Override public SchematicNode chooseSchematic() { - List publics = SchematicNode.getAllSchematicsOfType(0, Config.GameModeConfig.Schematic.Type.toDB()); - return publics.get(random.nextInt(publics.size())); + return FightSchematic.getPublicSchematic().orElseThrow(); } @Override diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightSchematic.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightSchematic.java index c8174349..014b9181 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightSchematic.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightSchematic.java @@ -46,6 +46,7 @@ import org.bukkit.util.Vector; import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Random; import java.util.logging.Level; @@ -109,31 +110,47 @@ public class FightSchematic extends StateDependent { clipboard = null; } + private static final Random random = new Random(); + + public static Optional getPublicSchematic() { + List publicsOverridden = Config.GameModeConfig.Schematic.Publics; + if (!publicsOverridden.isEmpty()) { + int publicId = publicsOverridden.get(random.nextInt(publicsOverridden.size())); + SchematicNode schematicNode = SchematicNode.getSchematicNode(publicId); + if (schematicNode != null) { + return Optional.of(schematicNode); + } + } + + List publics = SchematicNode.getAllSchematicsOfType(0, Config.GameModeConfig.Schematic.Type.toDB()); + if (publics.isEmpty()) { + for (SchematicType type : Config.GameModeConfig.Schematic.SubTypes) { + publics = SchematicNode.getAllSchematicsOfType(0, type.toDB()); + if (!publics.isEmpty()) { + break; + } + } + if (publics.isEmpty()) { + return Optional.empty(); + } + } + return Optional.of(publics.get(random.nextInt(publics.size()))); + } + @Override public void enable() { if (FightState.getFightState() == FightState.SPECTATE) return; if (clipboard == null) { - List publics = SchematicNode.getAllSchematicsOfType(0, Config.GameModeConfig.Schematic.Type.toDB()); - if (publics.isEmpty()) { - for (SchematicType type : Config.GameModeConfig.Schematic.SubTypes) { - publics = SchematicNode.getAllSchematicsOfType(0, type.toDB()); - if (!publics.isEmpty()) { - break; - } - } - if (publics.isEmpty()) { - return; - } - } - - setSchematic(publics.get(new Random().nextInt(publics.size()))); + Optional schematicNode = getPublicSchematic(); + if (schematicNode.isEmpty()) return; + setSchematic(schematicNode.get()); } if (ArenaMode.AntiReplay.contains(Config.mode)) { boolean changeRotation = false; if (Config.GameModeConfig.WinConditions.contains(Winconditions.RANDOM_ROTATE)) { - changeRotation = new Random().nextBoolean(); + changeRotation = random.nextBoolean(); usedRotate = rotate ^ changeRotation; } if (team.isBlue()) { From bfbf98c1d61e55442bb2095d113a58463dc5116c Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 23 Oct 2026 11:05:28 +0200 Subject: [PATCH 43/43] Fix GUI.schemDialog for overriden Public list --- .../src/de/steamwar/fightsystem/commands/GUI.java | 10 +++++++++- .../src/de/steamwar/inventory/SchematicSelector.java | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/commands/GUI.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/commands/GUI.java index a65886b0..c70fbeca 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/commands/GUI.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/commands/GUI.java @@ -255,7 +255,15 @@ public class GUI { private static void schemDialog(Player p, SchematicType type, boolean publicSchems, boolean unchecked) { SchematicSelector selector = new SchematicSelector(p, Config.test() ? SchematicSelector.selectSchematic() : SchematicSelector.selectSchematicType(unchecked ? type.checkType() : type), node -> { schemSelect(p, node); - }); + }) { + @Override + protected List getSchematicList(SchematicNode parent) { + if (!publicSchems || parent != null) return super.getSchematicList(parent); + List publics = Config.GameModeConfig.Schematic.Publics; + if (publics.isEmpty()) return super.getSchematicList(null); + return publics.stream().map(SchematicNode::byId).collect(Collectors.toList()); + } + }; selector.setPublicMode(publicSchems ? SchematicSelector.PublicMode.PUBLIC_ONLY : SchematicSelector.PublicMode.PRIVATE_ONLY); selector.open(); } diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/SchematicSelector.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/SchematicSelector.java index 5d649766..59df352e 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/SchematicSelector.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/SchematicSelector.java @@ -467,7 +467,7 @@ public class SchematicSelector { return nodes; } - private List getSchematicList(SchematicNode parent) { + protected List getSchematicList(SchematicNode parent) { List nodes = new ArrayList<>(); switch (target.target) { case DIRECTORY: