From 8401f67b24819d2211e29f785dab4acc9ff20469 Mon Sep 17 00:00:00 2001 From: Hut512 Date: Thu, 3 Sep 2026 02:20:43 +0200 Subject: [PATCH 01/32] Fix CRIU subserver stop --- .../src/de/steamwar/persistent/Subserver.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java b/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java index 88902b9a..711bab09 100644 --- a/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java +++ b/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java @@ -138,14 +138,16 @@ public class Subserver { writer.println(command); } + private ProcessHandle serverProcess() { + if (checkpoint) { + return process.children().findAny().orElseGet(process::toHandle); + } + return process.toHandle(); + } + public void sleep() { try { - long pid = process.pid(); - if (checkpoint) { - pid = process.children().findAny().map(ProcessHandle::pid).orElse(pid); - } - - Runtime.getRuntime().exec(new String[]{"kill", "-SIGUSR1", Long.toString(pid)}); + Runtime.getRuntime().exec(new String[]{"kill", "-SIGUSR1", Long.toString(serverProcess().pid())}); } catch (IOException e) { logger.log(Level.SEVERE, "Failed to send SIGUSR1 to subserver.", e); } @@ -162,7 +164,7 @@ public class Subserver { public void stop() { try { - process.destroy(); + serverProcess().destroy(); if (!process.waitFor(1, TimeUnit.MINUTES)) { forceStop(); } else if (thread.isAlive()) { @@ -177,6 +179,7 @@ public class Subserver { public void forceStop() { try { logger.log(Level.SEVERE, () -> serverName + " did not stop correctly, forcibly stopping!"); + serverProcess().destroyForcibly(); process.destroyForcibly(); if (thread.isAlive()) thread.join(); From 0113788da208a661702906ed978fdcfe0dec395d Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 5 Sep 2026 09:50:47 +0200 Subject: [PATCH 02/32] Fix SteamEditWand --- .../core/worldedit/SteamEditWand.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java index 9d42538d..cb478827 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java @@ -20,6 +20,7 @@ package de.steamwar.core.worldedit; import com.comphenix.tinyprotocol.TinyProtocol; +import com.sk89q.worldedit.IncompleteRegionException; import com.sk89q.worldedit.LocalSession; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.bukkit.WorldEditPlugin; @@ -121,13 +122,7 @@ public class SteamEditWand implements Listener { } public void setLocation(Location location) { - float distance = (float) player.getLocation().distanceSquared(location); wireframe.setPos1And2(location, location); - float width = distance / 1600; - width = ((int) (width * 16)) / 16F; - width = Math.max(width, 1 / 16F); - width = Math.min(width, 1 / 2F); - wireframe.setWidth(width); wireframe.setBlock(WorldEditRendererCUIEditor.Type.CURSOR.getMaterial(player).createBlockData()); } } @@ -338,7 +333,11 @@ public class SteamEditWand implements Listener { } private void selectPos1(RegionSelector regionSelector, Player player, LocalSession session, BlockVector3 pos1) { - if (!pos1.equals(regionSelector.getPrimaryPosition())) { + try { + if (!pos1.equals(regionSelector.getPrimaryPosition())) { + SWPlayer.of(player).setComponent(new Pos1Message(regionSelector, player, session, pos1)); + } + } catch (IncompleteRegionException e) { SWPlayer.of(player).setComponent(new Pos1Message(regionSelector, player, session, pos1)); } regionSelector.selectPrimary(pos1, PermissiveSelectorLimits.getInstance()); @@ -362,7 +361,11 @@ public class SteamEditWand implements Listener { } private void selectPos2(RegionSelector regionSelector, Player player, LocalSession session, BlockVector3 pos2) { - if (regionSelector.getVertices().size() == 2 && !pos2.equals(regionSelector.getVertices().get(1))) { + try { + if (regionSelector.getVertices().size() == 2 && !pos2.equals(regionSelector.getVertices().get(1))) { + SWPlayer.of(player).setComponent(new Pos2Message(regionSelector, player, session, pos2)); + } + } catch (IncompleteRegionException e) { SWPlayer.of(player).setComponent(new Pos2Message(regionSelector, player, session, pos2)); } regionSelector.selectSecondary(pos2, PermissiveSelectorLimits.getInstance()); From 6f1403df9c80afa142b7d7dbec900f384c2d702c Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 5 Sep 2026 14:52:59 +0200 Subject: [PATCH 03/32] Cleanup SWPlayer --- .../bausystem/features/observer/ObserverTracerListener.java | 2 +- .../de/steamwar/bausystem/features/util/NoClipCommand.java | 2 +- .../SpigotCore_Main/src/de/steamwar/core/SWPlayer.java | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/observer/ObserverTracerListener.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/observer/ObserverTracerListener.java index 987b2a23..d823bff4 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/observer/ObserverTracerListener.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/observer/ObserverTracerListener.java @@ -46,7 +46,7 @@ public class ObserverTracerListener implements Listener { public ObserverTracerListener() { Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> { SWPlayer.allWithSingleComponent(ObserverTracer.class).forEach(pair -> { - if (pair.getPlayer().getGameMode() != GameMode.SPECTATOR) return; + if (pair.getSwPlayer().getPlayer().getGameMode() != GameMode.SPECTATOR) return; pair.getComponent().show(); }); }, 15L, 15L); 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 dd143f0a..d0a7ef71 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 @@ -53,7 +53,7 @@ public class NoClipCommand extends SWCommand implements Listener { @Override public void onUnmount(SWPlayer player) { - player.setGameMode(GameMode.CREATIVE); + player.getPlayer().setGameMode(GameMode.CREATIVE); } } diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/SWPlayer.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/SWPlayer.java index 692c2031..f5cbffe4 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/SWPlayer.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/SWPlayer.java @@ -21,8 +21,8 @@ package de.steamwar.core; import de.steamwar.message.Message; import lombok.Data; +import lombok.Getter; import lombok.NonNull; -import lombok.experimental.Delegate; import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.chat.ClickEvent; import org.bukkit.Bukkit; @@ -99,11 +99,11 @@ public class SWPlayer { @Data public static final class SWPlayerWithComponent { - private final SWPlayer player; + private final SWPlayer swPlayer; private final T component; } - @Delegate + @Getter private final Player player; private final Map, Component> components = new HashMap<>(); From 6209b861491a5bfb7471170185dfba21301a3cf0 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 7 Sep 2026 14:03:06 +0200 Subject: [PATCH 04/32] Fix TeamCommand.info --- .../src/de/steamwar/velocitycore/commands/TeamCommand.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java index c73b256c..0e9b7aba 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java @@ -287,6 +287,10 @@ public class TeamCommand extends SWCommand { @Register(value = "info", description = "TEAM_INFO_USAGE") public void info(Chatter sender, @ErrorMessage("UNKNOWN_TEAM") Team team) { + if (team.getMembers().isEmpty()) { + sender.system("UNKNOWN_TEAM"); + return; + } List users = team.getMembers().stream().map(SteamwarUser::byId).toList(); sender.system("TEAM_INFO_TEAM", team.getTeamName(), team.getTeamColor(), team.getTeamKuerzel()); From 5a02a584372c4128a50e49982dc5bafe46ec3835 Mon Sep 17 00:00:00 2001 From: Hut512 Date: Fri, 4 Sep 2026 19:44:59 +0200 Subject: [PATCH 05/32] Fix SteamwarUser.setLocale --- CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.kt b/CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.kt index 0d19c712..09b7bf68 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.kt +++ b/CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.kt @@ -307,12 +307,12 @@ class SteamwarUser(id: EntityID) : IntEntity(id) { punishments[punishment] = Punishment.createPunishment(this@SteamwarUser.id.value, from, punishment, reason, time, perma) } - fun setLocale(locale: Locale?, manualeLocale: Boolean) { + fun setLocale(locale: Locale?, manualLocale: Boolean) { if (locale == null || (this.manualLocale && !manualLocale)) return useDb { this@SteamwarUser.locale = locale - this@SteamwarUser.manualLocale = manualeLocale + this@SteamwarUser.manualLocale = manualLocale } } From 3aa1bf7ffdf83bbc40fadef66e6070601ede2d90 Mon Sep 17 00:00:00 2001 From: Hut512 Date: Mon, 7 Sep 2026 21:11:21 +0200 Subject: [PATCH 06/32] Fix scoreboard cleanup --- .../de/steamwar/bausystem/features/world/BauScoreboard.java | 1 + .../src/de/steamwar/scoreboard/SWScoreboard.java | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauScoreboard.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauScoreboard.java index e7492216..a56a255e 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauScoreboard.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauScoreboard.java @@ -131,6 +131,7 @@ public class BauScoreboard implements Listener { @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { + SWScoreboard.impl.removeScoreboard(event.getPlayer()); ADDITIONAL_SCOREBOARD_LINES.remove(event.getPlayer()); } } diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/scoreboard/SWScoreboard.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/scoreboard/SWScoreboard.java index c7c50e88..16a33267 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/scoreboard/SWScoreboard.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/scoreboard/SWScoreboard.java @@ -67,7 +67,9 @@ public class SWScoreboard { } public void removeScoreboard(Player player) { - player.getScoreboard().getObjective(SIDEBAR).unregister(); + if (player.getScoreboard().getObjective(SIDEBAR) != null) { + player.getScoreboard().getObjective(SIDEBAR).unregister(); + } playerBoards.remove(player); } } From c3b3e3e60f712ff74e5484e0892e1b2fa8e585ef Mon Sep 17 00:00:00 2001 From: Hut512 Date: Mon, 7 Sep 2026 22:21:53 +0200 Subject: [PATCH 07/32] Fix NoClip instant block break --- .../src/de/steamwar/bausystem/features/util/NoClipCommand.java | 1 + 1 file changed, 1 insertion(+) 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 d0a7ef71..91a79f5e 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 @@ -115,6 +115,7 @@ public class NoClipCommand extends SWCommand implements Listener { Abilities abilities = (((CraftPlayer) player).getHandle()).getAbilities(); abilities.mayBuild = true; abilities.mayfly = true; + abilities.instabuild = true; swPlayer.setComponent(new NoClipData()); BauSystem.MESSAGE.send("OTHER_NOCLIP_SLOT_INFO", player); From 35c27923aac1acc57d8f1fb07e5d90dbe4137b80 Mon Sep 17 00:00:00 2001 From: Hut512 Date: Sat, 12 Sep 2026 20:07:07 +0200 Subject: [PATCH 08/32] Fix region water level --- .../src/de/steamwar/bausystem/region/fixed/FixedRegion.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegion.java b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegion.java index 62b83882..4a87922d 100644 --- a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegion.java +++ b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegion.java @@ -325,7 +325,7 @@ public class FixedRegion implements Region { } floorLevel = prototype.getFloorOffset() != 0 ? minPoint.getY() + prototype.getFloorOffset() : 0; - waterLevel = prototype.getWaterOffset() != 0 ? minPoint.getX() + prototype.getWaterOffset() : 0; + waterLevel = prototype.getWaterOffset() != 0 ? minPoint.getY() + prototype.getWaterOffset() : 0; File found = null; File baseFile = new File(BauSystem.getInstance().getDataFolder().getParentFile(), "FightSystem"); From 1e85b14667d052ac7ce325024f81d83c1b443e4f Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 14 Sep 2026 10:02:39 +0200 Subject: [PATCH 09/32] Improve SteamEditWand --- .../core/worldedit/SteamEditWand.java | 78 ++++--------------- 1 file changed, 14 insertions(+), 64 deletions(-) diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java index cb478827..eb1f180a 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java @@ -30,16 +30,13 @@ import com.sk89q.worldedit.regions.selector.limit.PermissiveSelectorLimits; import com.sk89q.worldedit.world.World; import de.steamwar.core.Core; import de.steamwar.core.SWPlayer; -import de.steamwar.entity.CWireframe; -import de.steamwar.entity.REntityServer; import de.steamwar.inventory.SWItem; import net.minecraft.core.BlockPos; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.*; -import org.bukkit.Bukkit; -import org.bukkit.FluidCollisionMode; -import org.bukkit.Location; -import org.bukkit.Material; +import org.bukkit.*; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeModifier; import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -47,6 +44,7 @@ import org.bukkit.event.Listener; import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.scheduler.BukkitTask; import org.bukkit.util.RayTraceResult; @@ -65,6 +63,16 @@ public class SteamEditWand implements Listener { }; public static final ItemStack ITEM = new SWItem(MATERIAL, NAME).setLore(LORE).getItemStack(); + static { + ItemMeta itemMeta = ITEM.getItemMeta(); + itemMeta.addAttributeModifier(Attribute.BLOCK_INTERACTION_RANGE, new AttributeModifier( + NamespacedKey.fromString("range", Core.getInstance()), + 35.0, + AttributeModifier.Operation.ADD_NUMBER + )); + ITEM.setItemMeta(itemMeta); + } + private boolean noWand(Player player) { return player.getInventory().getItemInMainHand().getType() != SteamEditWand.MATERIAL; } @@ -87,15 +95,6 @@ public class SteamEditWand implements Listener { TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, this::rightClick); TinyProtocol.instance.addFilter(ServerboundPickItemFromBlockPacket.class, this::middleClick); TinyProtocol.instance.addFilter(ServerboundPlayerActionPacket.class, this::swapHands); - TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.Pos.class, this::movePlayerPacket); - TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.Rot.class, this::movePlayerPacket); - TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.PosRot.class, this::movePlayerPacket); - - Bukkit.getScheduler().runTaskTimer(Core.getInstance(), () -> { - for (Player player : Bukkit.getOnlinePlayers()) { - if (noWand(player)) SWPlayer.of(player).removeComponent(SWEditCursor.class); - } - }, 1, 1); } @EventHandler @@ -104,29 +103,6 @@ public class SteamEditWand implements Listener { event.setCancelled(true); } - public static class SWEditCursor implements SWPlayer.Component { - private Player player; - private REntityServer entityServer = new REntityServer(); - private CWireframe wireframe; - - @Override - public void onMount(SWPlayer player) { - this.player = player.getPlayer(); - entityServer.addPlayer(player.getPlayer()); - wireframe = new CWireframe(entityServer); - } - - @Override - public void onUnmount(SWPlayer player) { - entityServer.close(); - } - - public void setLocation(Location location) { - wireframe.setPos1And2(location, location); - wireframe.setBlock(WorldEditRendererCUIEditor.Type.CURSOR.getMaterial(player).createBlockData()); - } - } - private Packet leftClick(Player player, Packet packet) { if (noWand(player)) return packet; if (player.getOpenInventory().getType() != InventoryType.CREATIVE) return packet; @@ -380,30 +356,4 @@ public class SteamEditWand implements Listener { }); return null; } - - public ServerboundMovePlayerPacket movePlayerPacket(Player player, ServerboundMovePlayerPacket packet) { - SWPlayer swPlayer = SWPlayer.of(player); - - if (noWand(player)) { - swPlayer.removeComponent(SWEditCursor.class); - return packet; - } - - double distance = WorldEditRendererCUIEditor.Type.CURSOR.getWidth(player).blocks; - RayTraceResult result = player.rayTraceBlocks(distance, FluidCollisionMode.NEVER); - Block block = result == null ? null : result.getHitBlock(); - if (result == null || block == null) { - swPlayer.removeComponent(SWEditCursor.class); - return packet; - } - - if (!swPlayer.hasComponent(SWEditCursor.class)) { - swPlayer.setComponent(new SWEditCursor()); - } - - swPlayer.getComponent(SWEditCursor.class).ifPresent(cursor -> { - cursor.setLocation(block.getLocation()); - }); - return packet; - } } 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 10/32] 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 11/32] 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 12/32] 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 13/32] 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 2b42f0d13caa980ad8a88894131ec63ac9177e4e Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Tue, 29 Sep 2026 14:26:57 +0200 Subject: [PATCH 14/32] Remove YAPION from BauSystem_Main module --- BauSystem/BauSystem_Main/build.gradle.kts | 2 +- .../src/de/steamwar/bausystem/BauSystem.java | 10 +- .../bausystem/config/ConfigConverter.java | 47 +++ .../bausystem/config/ConfigStore.java | 364 ++++++++++++++++++ .../bausystem/config/ConfigStoreListener.java | 45 +++ .../bausystem/config/VersionedConfig.java | 29 ++ .../config/legacy/YapionItemDeserializer.java | 95 +++++ .../bausystem/config/legacy/YapionReader.java | 323 ++++++++++++++++ .../serializer/ItemStackTypeAdapter.java | 63 +++ .../bausystem/configplayer/Config.java | 168 +++----- .../configplayer/ConfigConverter.java | 56 --- .../bausystem/configplayer/ConfigCreator.java | 61 --- .../bausystem/configplayer/ConfigV1.java | 104 +++++ .../bausystem/configplayer/converter/.gitkeep | 0 .../converter/ConfigV1ToV2Converter.java | 75 ++++ .../ConfigurationSerializableSerializer.java | 90 ----- .../features/cannon/depth/Depth.java | 2 +- .../features/cannon/depth/DepthCommand.java | 6 +- .../features/detonator/Detonator.java | 2 +- .../features/detonator/DetonatorCommand.java | 4 +- .../features/gui/editor/BauGuiMapping.java | 31 +- .../features/hotbar/DefaultHotbar.java | 70 ++-- .../InventoryFillBauGuiItem.java | 2 +- .../inventoryfiller/InventoryFiller.java | 4 +- .../InventoryFillerCommand.java | 4 +- .../rayvisualizer/RayVisualizerCommand.java | 8 +- .../simulator/data/SimulatorElement.java | 6 +- .../simulator/data/SimulatorPhase.java | 6 +- .../data/observer/ObserverPhase.java | 10 +- .../simulator/data/tnt/TNTElement.java | 14 +- .../features/simulator/data/tnt/TNTPhase.java | 22 +- .../storage/SimFormatSimulatorLoader.java | 66 ++-- .../simulator/storage/SimulatorConfig.java | 117 ++++++ .../SimulatorFormatSimulatorLoader.java | 78 ++-- .../simulator/storage/SimulatorSaver.java | 57 ++- .../storage/YAPIONFormatSimulatorLoader.java | 52 ++- .../slaves/laufbau/LaufbauConfig.java | 64 +++ .../slaves/laufbau/LaufbauConfigV1.java | 65 ++++ .../features/slaves/laufbau/LaufbauUtils.java | 53 +-- .../LaufbauConfigV1ToV2Converter.java | 40 ++ .../smartplace/SmartPlaceCommand.java | 4 +- .../smartplace/SmartPlaceListener.java | 6 +- .../testblock/blockcounter/BlockCounter.java | 9 +- .../tracer/rendering/TraceEntity.java | 5 +- .../features/util/NightVisionCommand.java | 9 +- .../features/util/TNTClickListener.java | 14 +- .../bausystem/features/warp/Warp.java | 32 +- .../bausystem/features/warp/WarpCommand.java | 10 +- .../steamwar/bausystem/region/RegionData.java | 67 +--- .../bausystem/worlddata/SimulatorData.java | 101 ----- .../bausystem/worlddata/WorldData.java | 115 +++--- .../bausystem/worlddata/WorldDataV1.java | 111 ++++++ .../converter/WorldDataV1ToV2Converter.java | 75 ++++ .../region/fixed/FixedGlobalRegionData.java | 4 +- .../bausystem/region/fixed/FixedRegion.java | 3 +- .../region/fixed/FixedRegionData.java | 4 +- .../bausystem/region/fixed/Prototype.java | 8 +- .../region/fixed/loader/RegionLoader.java | 18 +- 58 files changed, 2033 insertions(+), 877 deletions(-) create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigConverter.java create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigStore.java create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigStoreListener.java create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/VersionedConfig.java create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/legacy/YapionItemDeserializer.java create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/legacy/YapionReader.java create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/serializer/ItemStackTypeAdapter.java delete mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigConverter.java delete mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigCreator.java create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigV1.java delete mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/converter/.gitkeep create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/converter/ConfigV1ToV2Converter.java delete mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/serializer/ConfigurationSerializableSerializer.java create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorConfig.java create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauConfig.java create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauConfigV1.java create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/converter/LaufbauConfigV1ToV2Converter.java delete mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/SimulatorData.java create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/WorldDataV1.java create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/converter/WorldDataV1ToV2Converter.java diff --git a/BauSystem/BauSystem_Main/build.gradle.kts b/BauSystem/BauSystem_Main/build.gradle.kts index c6316f7d..c387bc36 100644 --- a/BauSystem/BauSystem_Main/build.gradle.kts +++ b/BauSystem/BauSystem_Main/build.gradle.kts @@ -47,7 +47,7 @@ dependencies { compileOnly(libs.netty) implementation(libs.luaj) - implementation(files("$projectDir/../libs/YAPION-SNAPSHOT.jar")) + // implementation(files("$projectDir/../libs/YAPION-SNAPSHOT.jar")) } widener { diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java index 5a8cac69..041e4d83 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java @@ -20,8 +20,7 @@ package de.steamwar.bausystem; import de.steamwar.bausystem.config.BauServer; -import de.steamwar.bausystem.configplayer.Config; -import de.steamwar.bausystem.configplayer.ConfigConverter; +import de.steamwar.bausystem.config.ConfigStore; import de.steamwar.bausystem.features.gui.BauGUI; import de.steamwar.bausystem.features.script.lua.SteamWarLuaPlugin; import de.steamwar.bausystem.features.script.lua.libs.LuaLib; @@ -36,7 +35,6 @@ import de.steamwar.bausystem.region.Region; import de.steamwar.bausystem.region.RegionSystem; import de.steamwar.bausystem.utils.ScoreboardElement; import de.steamwar.bausystem.utils.TickManager; -import de.steamwar.bausystem.worlddata.WorldData; import de.steamwar.command.AbstractValidator; import de.steamwar.command.SWCommandUtils; import de.steamwar.core.CRIUSleepEvent; @@ -112,9 +110,6 @@ public class BauSystem extends JavaPlugin implements Listener { if (any instanceof PanzernAlgorithm) { Panzern.add((PanzernAlgorithm) any); } - if (any instanceof ConfigConverter) { - Config.addConfigConverter((ConfigConverter) any); - } if (any instanceof BoundingBoxLoader) { ((BoundingBoxLoader) any).load(); } @@ -160,9 +155,8 @@ public class BauSystem extends JavaPlugin implements Listener { @Override public void onDisable() { linker.unlink(); - WorldData.write(); RegionSystem.INSTANCE.save(); - Config.getInstance().saveAll(); + ConfigStore.saveAll(); } private AbstractValidator validator(Permission permission) { diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigConverter.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigConverter.java new file mode 100644 index 00000000..a76db061 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigConverter.java @@ -0,0 +1,47 @@ +/* + * 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.bausystem.config; + +/** + * Converts a config of one version into the next version, e.g. ConfigV1 to + * ConfigV2. The converter for the second newest version converts into the + * current version. Converters are registered in the {@link ConfigStore} + * of the config. + * + * @param the config version to convert from + * @param the config version to convert to + */ +public interface ConfigConverter { + + /** + * @return the version this converter converts from + */ + int version(); + + /** + * Converts the config to the next version. If anything goes wrong do not + * silently exit this method, throw an Exception. The {@link ConfigStore} + * will deal with it. + * + * @param config the config to convert + * @return the converted config + */ + T convert(F config); +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigStore.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigStore.java new file mode 100644 index 00000000..21ff6fcb --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigStore.java @@ -0,0 +1,364 @@ +/* + * 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.bausystem.config; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import de.steamwar.sql.UserConfig; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.logging.Level; + +/** + * Loads, converts and saves a versioned config stored as JSON. Older versions + * are converted with the registered {@link ConfigConverter}s, legacy formats + * that are not JSON can be read with {@link #legacy(Predicate, Function)}. + *

+ * A store can be bound to where the config is stored: + *

    + *
  • {@link #userConfig(String)}: one config per player in the {@link UserConfig}, use {@link #get(Player)}
  • + *
  • {@link #file(File)}: one config in a file, use {@link #get()}
  • + *
  • unbound: use {@link #load(File)} and {@link #save(File, VersionedConfig)}
  • + *
+ * + * @param the current config version + */ +public class ConfigStore { + + private static final List> STORES = new ArrayList<>(); + + private final Class type; + private final int version; + private final Supplier defaultConfig; + + private final Map> jsonVersions = new HashMap<>(); + private final Map> converters = new HashMap<>(); + private final List legacyFormats = new ArrayList<>(); + private Gson gson = new Gson(); + private Integer unversionedVersion = null; + + private String userConfigKey = null; + private Predicate removeIf = config -> false; + private final Map playerConfigs = new ConcurrentHashMap<>(); + + private File file = null; + private File legacyFile = null; + private Function legacyFileReader = null; + private T fileConfig = null; + + private record LegacyFormat(Predicate matches, Function reader) { + } + + public ConfigStore(Class type, int version, Supplier defaultConfig) { + this.type = type; + this.version = version; + this.defaultConfig = defaultConfig; + jsonVersions.put(version, type); + STORES.add(this); + } + + public ConfigStore gson(Gson gson) { + this.gson = gson; + return this; + } + + /** + * Registers an older config version that is stored as JSON. + */ + public ConfigStore jsonVersion(int version, Class type) { + jsonVersions.put(version, type); + return this; + } + + public ConfigStore converter(ConfigConverter converter) { + if (converters.putIfAbsent(converter.version(), converter) != null) { + throw new IllegalStateException("Duplicate converter for version " + converter.version() + " of " + type.getSimpleName()); + } + return this; + } + + /** + * Registers a legacy format that is not JSON, stored strings matching the predicate are read by the reader. + */ + public ConfigStore legacy(Predicate matches, Function reader) { + legacyFormats.add(new LegacyFormat(matches, reader)); + return this; + } + + /** + * The version of stored JSON without a version field. + */ + public ConfigStore unversioned(int version) { + this.unversionedVersion = version; + return this; + } + + /** + * Binds this store to the {@link UserConfig} with the given key, one config per player. + */ + public ConfigStore userConfig(String key) { + this.userConfigKey = key; + return this; + } + + /** + * Removes the config from the {@link UserConfig} instead of saving it, if the predicate matches. + */ + public ConfigStore removeIf(Predicate removeIf) { + this.removeIf = removeIf; + return this; + } + + /** + * Binds this store to a single file. + */ + public ConfigStore file(File file) { + this.file = file; + return this; + } + + /** + * Legacy file to read, if the file of this store does not exist yet. + */ + public ConfigStore legacyFile(File legacyFile, Function reader) { + this.legacyFile = legacyFile; + this.legacyFileReader = reader; + return this; + } + + /** + * Reads a stored config and converts it to the current version. + * + * @throws RuntimeException if the config could not be read or converted + */ + public T read(String stored) { + for (LegacyFormat legacyFormat : legacyFormats) { + if (legacyFormat.matches().test(stored)) { + return update(legacyFormat.reader().apply(stored)); + } + } + + JsonObject json = JsonParser.parseString(stored).getAsJsonObject(); + int storedVersion; + if (json.has("version")) { + storedVersion = json.get("version").getAsInt(); + } else if (unversionedVersion != null) { + storedVersion = unversionedVersion; + json.addProperty("version", storedVersion); + } else { + throw new IllegalStateException("Missing version of " + type.getSimpleName()); + } + Class storedType = jsonVersions.get(storedVersion); + if (storedType == null) { + throw new IllegalStateException("Unknown version " + storedVersion + " of " + type.getSimpleName()); + } + return update(gson.fromJson(json, storedType)); + } + + public String write(T config) { + return gson.toJson(config); + } + + /** + * Converts a config to the current version. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public T update(VersionedConfig config) { + while (config.getVersion() < version) { + ConfigConverter converter = converters.get(config.getVersion()); + if (converter == null) { + throw new IllegalStateException("No converter found for version " + config.getVersion() + " of " + type.getSimpleName()); + } + VersionedConfig converted = converter.convert(config); + if (converted.getVersion() <= config.getVersion()) { + throw new IllegalStateException("Version did not increase after conversion of version " + config.getVersion() + " of " + type.getSimpleName()); + } + config = converted; + } + if (!type.isInstance(config)) { + throw new IllegalStateException("Version " + config.getVersion() + " is not the current version of " + type.getSimpleName()); + } + return type.cast(config); + } + + /** + * @return the config stored in the file, or the default config if the file is missing or empty + * @throws RuntimeException if the config could not be read or converted + */ + public T load(File file) throws IOException { + if (file.length() == 0) { + return defaultConfig.get(); + } + return read(Files.readString(file.toPath(), StandardCharsets.UTF_8)); + } + + public void save(File file, T config) throws IOException { + Files.writeString(file.toPath(), write(config), StandardCharsets.UTF_8); + } + + /** + * Get the config of a player, loads it from the {@link UserConfig} and converts it if necessary. + */ + public T get(Player player) { + requireUserConfig(); + return playerConfigs.computeIfAbsent(player.getUniqueId(), this::loadPlayer); + } + + /** + * Save the config of a player, this does not unload the config. + */ + public void save(Player player) { + requireUserConfig(); + T config = playerConfigs.get(player.getUniqueId()); + if (config != null) { + savePlayer(player.getUniqueId(), config); + } + } + + /** + * Save and unload the config of a player. + */ + public void unload(Player player) { + if (userConfigKey == null) return; + T config = playerConfigs.remove(player.getUniqueId()); + if (config != null) { + savePlayer(player.getUniqueId(), config); + } + } + + private T loadPlayer(UUID uuid) { + String stored = UserConfig.getConfig(uuid, userConfigKey); + if (stored == null) { + return defaultConfig.get(); + } + try { + return read(stored); + } catch (Exception e) { + Bukkit.getLogger().log(Level.SEVERE, "Could not load " + type.getSimpleName() + " of " + uuid, e); + return defaultConfig.get(); + } + } + + private void savePlayer(UUID uuid, T config) { + if (removeIf.test(config)) { + UserConfig.removePlayerConfig(uuid, userConfigKey); + } else { + UserConfig.updatePlayerConfig(uuid, userConfigKey, write(config)); + } + } + + private void requireUserConfig() { + if (userConfigKey == null) { + throw new IllegalStateException(type.getSimpleName() + " is not stored in the UserConfig"); + } + } + + /** + * Get the config of the file of this store, loads it and converts it if necessary. + */ + public T get() { + requireFile(); + if (fileConfig == null) { + fileConfig = loadFile(); + save(); + } + return fileConfig; + } + + /** + * Save the config to the file of this store, if it was loaded. + */ + public void save() { + requireFile(); + if (fileConfig == null) return; + try { + save(file, fileConfig); + } catch (IOException e) { + Bukkit.getLogger().log(Level.SEVERE, "Could not write " + file, e); + } + } + + private T loadFile() { + try { + if (file.length() != 0) { + return load(file); + } + if (legacyFile != null && legacyFile.length() != 0) { + return update(legacyFileReader.apply(Files.readString(legacyFile.toPath(), StandardCharsets.UTF_8))); + } + return defaultConfig.get(); + } catch (Exception e) { + Bukkit.getLogger().log(Level.SEVERE, "Could not load " + file + ", using default " + type.getSimpleName(), e); + backupBrokenFile(); + return defaultConfig.get(); + } + } + + private void backupBrokenFile() { + if (file.length() == 0) return; + try { + Files.copy(file.toPath(), new File(file.getParentFile(), file.getName() + ".broken").toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + Bukkit.getLogger().log(Level.SEVERE, "Could not backup broken " + file, e); + } + } + + private void requireFile() { + if (file == null) { + throw new IllegalStateException(type.getSimpleName() + " is not bound to a file"); + } + } + + static void unloadAll(Player player) { + STORES.forEach(store -> store.unload(player)); + } + + /** + * Saves every loaded config and unloads the configs of the players. + */ + public static void saveAll() { + for (ConfigStore store : STORES) { + if (store.userConfigKey != null) { + store.playerConfigs.forEach((uuid, config) -> saveUnchecked(store, uuid, config)); + store.playerConfigs.clear(); + } + if (store.file != null) { + store.save(); + } + } + } + + @SuppressWarnings("unchecked") + private static void saveUnchecked(ConfigStore store, UUID uuid, Object config) { + store.savePlayer(uuid, (T) config); + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigStoreListener.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigStoreListener.java new file mode 100644 index 00000000..67276588 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigStoreListener.java @@ -0,0 +1,45 @@ +/* + * 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.bausystem.config; + +import de.steamwar.core.CRIUSleepEvent; +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.PlayerQuitEvent; + +/** + * Saves and unloads the configs of the {@link ConfigStore}s bound to the UserConfig + * when a player quits, configs are loaded on first use. + */ +@Linked +public class ConfigStoreListener implements Listener { + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerQuit(PlayerQuitEvent event) { + ConfigStore.unloadAll(event.getPlayer()); + } + + @EventHandler + public void onCRIUSleep(CRIUSleepEvent event) { + ConfigStore.saveAll(); + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/VersionedConfig.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/VersionedConfig.java new file mode 100644 index 00000000..ca036e06 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/VersionedConfig.java @@ -0,0 +1,29 @@ +/* + * 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.bausystem.config; + +/** + * Common interface of every version of a config stored by a {@link ConfigStore}, + * the current version (e.g. Config) and every older version (ConfigV1, ConfigV2, ...). + */ +public interface VersionedConfig { + + int getVersion(); +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/legacy/YapionItemDeserializer.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/legacy/YapionItemDeserializer.java new file mode 100644 index 00000000..1ce4ab25 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/legacy/YapionItemDeserializer.java @@ -0,0 +1,95 @@ +/* + * 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.bausystem.config.legacy; + +import lombok.experimental.UtilityClass; +import org.bukkit.configuration.serialization.ConfigurationSerialization; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Deserializes {@link ItemStack}s that were serialized by YAPION, read with {@link YapionReader}. + */ +@UtilityClass +@Deprecated +public class YapionItemDeserializer { + + private static final String TYPE_IDENTIFIER = "@type"; + + public ItemStack deserializeItem(Object yapion) { + if (yapion == null) return null; + Object result = deserialize(yapion); + if (result instanceof ItemStack itemStack) return itemStack; + throw new IllegalArgumentException("Not an ItemStack: " + result); + } + + private Object deserialize(Object yapion) { + if (yapion instanceof List list) { + List result = new ArrayList<>(); + list.forEach(o -> result.add(deserialize(o))); + return result; + } + if (yapion instanceof YapionReader.YapionMap map) { + Map result = new LinkedHashMap<>(); + map.entries().forEach((key, value) -> result.put(deserialize(key), deserialize(value))); + return result; + } + if (yapion instanceof Map object) { + return deserializeObject(object); + } + return yapion; + } + + private Object deserializeObject(Map object) { + Map data = new LinkedHashMap<>(); + object.forEach((key, value) -> { + if (!TYPE_IDENTIFIER.equals(key)) data.put((String) key, deserialize(value)); + }); + + if (!(object.get(TYPE_IDENTIFIER) instanceof String type)) { + return data; + } + + Class javaType = javaUtilClass(type); + if (javaType != null && Collection.class.isAssignableFrom(javaType)) { + return data.getOrDefault("values", new ArrayList<>()); + } + if (javaType != null && Map.class.isAssignableFrom(javaType)) { + return data.containsKey("values") ? data.get("values") : data; + } + + data.put(ConfigurationSerialization.SERIALIZED_TYPE_KEY, type.equals("org.bukkit.inventory.meta.ItemMeta") ? "ItemMeta" : type); + return ConfigurationSerialization.deserializeObject(data); + } + + private Class javaUtilClass(String type) { + if (!type.startsWith("java.util.")) return null; + try { + return Class.forName(type); + } catch (ClassNotFoundException e) { + return null; + } + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/legacy/YapionReader.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/legacy/YapionReader.java new file mode 100644 index 00000000..c3fb84d4 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/legacy/YapionReader.java @@ -0,0 +1,323 @@ +/* + * 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.bausystem.config.legacy; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Minimal reader for the legacy YAPION format, only used to migrate old data. + *

+ * The result is built from plain java objects: + *

    + *
  • YAPION objects {@code {key(value)}} become a {@link LinkedHashMap} of String to value
  • + *
  • YAPION arrays {@code [a,b]} become a {@link List}
  • + *
  • YAPION maps {@code <(a):(b)>} become a {@link YapionMap}
  • + *
  • YAPION values {@code (value)} become null, Boolean, Number or String
  • + *
+ */ +@Deprecated +public class YapionReader { + + private static final Pattern INTEGER = Pattern.compile("[-+]?\\d+"); + private static final Pattern DECIMAL = Pattern.compile("[-+]?(\\d+\\.\\d*|\\.\\d+|\\d+)([eE][-+]?\\d+)?"); + + public record YapionMap(Map entries) { + } + + public static Map parse(String yapion) { + YapionReader reader = new YapionReader(yapion); + reader.skipWhitespace(); + Map result = reader.readObject(); + reader.skipWhitespace(); + if (reader.index != yapion.length()) { + throw reader.error("Unexpected trailing content"); + } + return result; + } + + public static double getDouble(Map object, String key, double defaultValue) { + return object.get(key) instanceof Number value ? value.doubleValue() : defaultValue; + } + + public static int getInt(Map object, String key, int defaultValue) { + return object.get(key) instanceof Number value ? value.intValue() : defaultValue; + } + + public static boolean getBoolean(Map object, String key, boolean defaultValue) { + return object.get(key) instanceof Boolean value ? value : defaultValue; + } + + public static String getString(Map object, String key, String defaultValue) { + return object.get(key) instanceof String value ? value : defaultValue; + } + + /** + * @return the entries of the array that are YAPION objects + */ + public static List> getObjects(Map object, String key) { + List> objects = new ArrayList<>(); + if (object.get(key) instanceof List array) { + array.forEach(o -> { + if (o instanceof Map map) objects.add(map); + }); + } + return objects; + } + + private final String input; + private int index = 0; + + private YapionReader(String input) { + this.input = input; + } + + private Object readAny() { + return switch (peek()) { + case '{' -> readObject(); + case '[' -> readArray(); + case '<' -> readMap(); + case '(' -> readValue(); + default -> throw error("Expected '{', '[', '<' or '('"); + }; + } + + private Map readObject() { + expect('{'); + Map object = new LinkedHashMap<>(); + while (true) { + skipWhitespace(); + if (peek() == '}') { + index++; + return object; + } + StringBuilder key = new StringBuilder(); + while (true) { + char c = peek(); + if (c == '(' || c == '{' || c == '[' || c == '<') break; + if (c == '}') throw error("Key without value"); + key.append(readChar()); + } + object.put(key.toString(), readAny()); + } + } + + private List readArray() { + expect('['); + List array = new ArrayList<>(); + skipWhitespace(); + if (peek() == ']') { + index++; + return array; + } + while (true) { + int start = index; + skipWhitespace(); + char c = peek(); + if (c == '{' || c == '[' || c == '<' || c == '(') { + array.add(readAny()); + skipWhitespace(); + } else { + // Whitespace is part of plain values + index = start; + StringBuilder value = new StringBuilder(); + while (peek() != ',' && peek() != ']') { + value.append(readChar()); + } + array.add(typed(value.toString())); + } + char separator = input.charAt(index++); + if (separator == ']') return array; + if (separator != ',') throw error("Expected ',' or ']'"); + } + } + + private YapionMap readMap() { + expect('<'); + Map map = new LinkedHashMap<>(); + while (true) { + skipWhitespace(); + if (peek() == '>') { + index++; + return new YapionMap(map); + } + Object key = readAny(); + skipWhitespace(); + expect(':'); + skipWhitespace(); + map.put(key, readAny()); + } + } + + /** + * YAPION does not escape parentheses in values, balanced parentheses are part of the value. + * A backslash only escapes a backslash, a unicode sequence or a control character, otherwise it is kept. + */ + private Object readValue() { + expect('('); + StringBuilder value = new StringBuilder(); + int depth = 0; + while (true) { + char c = peek(); + if (c == '\\') { + value.append(readValueEscape()); + continue; + } + index++; + if (c == '(') { + depth++; + } else if (c == ')') { + if (depth == 0) break; + depth--; + } + value.append(c); + } + return typed(value.toString()); + } + + private String readValueEscape() { + index++; + if (index >= input.length()) return "\\"; + char escaped = input.charAt(index); + switch (escaped) { + case '\\', '(', ')': + index++; + return String.valueOf(escaped); + case 'n': + index++; + return "\n"; + case 't': + index++; + return "\t"; + case 'r': + index++; + return "\r"; + case 'u': + if (index + 5 <= input.length() && input.substring(index + 1, index + 5).matches("[0-9a-fA-F]{4}")) { + char unicode = (char) Integer.parseInt(input.substring(index + 1, index + 5), 16); + index += 5; + return String.valueOf(unicode); + } + return "\\"; + default: + return "\\"; + } + } + + /** + * Reads one character of a key or plain array value, resolving backslash escapes. + * A backslash that does not escape anything is kept, as the old player config + * collapsed escaped backslashes into a single one. + */ + private char readChar() { + char c = input.charAt(index++); + if (c != '\\') return c; + if (index >= input.length()) throw error("Unterminated escape"); + char escaped = input.charAt(index); + switch (escaped) { + case 'n': + index++; + return '\n'; + case 't': + index++; + return '\t'; + case 'r': + index++; + return '\r'; + case 'u': + if (index + 5 <= input.length() && input.substring(index + 1, index + 5).matches("[0-9a-fA-F]{4}")) { + char unicode = (char) Integer.parseInt(input.substring(index + 1, index + 5), 16); + index += 5; + return unicode; + } + return '\\'; + case '\\', ',', ' ', '(', ')', '{', '}', '[', ']', '<', '>', '"', ':', '@': + index++; + return escaped; + default: + return '\\'; + } + } + + private static Object typed(String value) { + if (value.equals("null")) return null; + if (value.equals("true")) return true; + if (value.equals("false")) return false; + if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) { + return value.substring(1, value.length() - 1); + } + try { + if (INTEGER.matcher(value).matches()) { + long l = Long.parseLong(value); + if (l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) return (int) l; + return l; + } + if (value.length() > 1) { + String number = value.substring(0, value.length() - 1); + switch (value.charAt(value.length() - 1)) { + case 'L': + if (INTEGER.matcher(number).matches()) return Long.parseLong(number); + break; + case 'S': + if (INTEGER.matcher(number).matches()) return Short.parseShort(number); + break; + case 'B': + if (INTEGER.matcher(number).matches()) return Byte.parseByte(number); + break; + case 'F': + if (DECIMAL.matcher(number).matches()) return Float.parseFloat(number); + break; + case 'D': + if (DECIMAL.matcher(number).matches()) return Double.parseDouble(number); + break; + default: + break; + } + } + if (DECIMAL.matcher(value).matches()) return Double.parseDouble(value); + } catch (NumberFormatException e) { + // Not a number, fall through to String + } + return value; + } + + private void skipWhitespace() { + while (index < input.length() && Character.isWhitespace(input.charAt(index))) { + index++; + } + } + + private char peek() { + if (index >= input.length()) throw error("Unexpected end of input"); + return input.charAt(index); + } + + private void expect(char c) { + if (peek() != c) throw error("Expected '" + c + "'"); + index++; + } + + private IllegalArgumentException error(String message) { + return new IllegalArgumentException(message + " at index " + index); + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/serializer/ItemStackTypeAdapter.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/serializer/ItemStackTypeAdapter.java new file mode 100644 index 00000000..9edda398 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/serializer/ItemStackTypeAdapter.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.bausystem.config.serializer; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import org.bukkit.Bukkit; +import org.bukkit.inventory.ItemStack; + +import java.io.IOException; +import java.util.Base64; +import java.util.logging.Level; + +/** + * Stores an {@link ItemStack} as Base64 of {@link ItemStack#serializeAsBytes()}, which + * contains the data version and gets upgraded on newer Minecraft versions. + * Empty items are stored as null, items that can't be read are deserialized as null. + */ +public class ItemStackTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, ItemStack itemStack) throws IOException { + if (itemStack == null || itemStack.isEmpty()) { + out.nullValue(); + return; + } + out.value(Base64.getEncoder().encodeToString(itemStack.serializeAsBytes())); + } + + @Override + public ItemStack read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + String value = in.nextString(); + try { + return ItemStack.deserializeBytes(Base64.getDecoder().decode(value)); + } catch (Exception e) { + Bukkit.getLogger().log(Level.WARNING, "Could not deserialize ItemStack", e); + return null; + } + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/Config.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/Config.java index 77b8b706..19aef6d7 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/Config.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/Config.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 @@ -19,129 +19,77 @@ package de.steamwar.bausystem.configplayer; -import de.steamwar.bausystem.configplayer.serializer.ConfigurationSerializableSerializer; -import de.steamwar.linkage.Linked; -import de.steamwar.sql.UserConfig; +import com.google.gson.GsonBuilder; +import de.steamwar.bausystem.config.ConfigConverter; +import de.steamwar.bausystem.config.ConfigStore; +import de.steamwar.bausystem.config.VersionedConfig; +import de.steamwar.bausystem.config.serializer.ItemStackTypeAdapter; +import de.steamwar.bausystem.configplayer.converter.ConfigV1ToV2Converter; +import de.steamwar.bausystem.features.hotbar.DefaultHotbar; +import lombok.AccessLevel; import lombok.Getter; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import yapion.hierarchy.output.StringOutput; -import yapion.hierarchy.types.YAPIONObject; -import yapion.parser.YAPIONParser; -import yapion.serializing.SerializeManager; +import lombok.Setter; +import org.bukkit.inventory.ItemStack; import java.util.HashMap; import java.util.Map; -import java.util.UUID; -import java.util.logging.Level; -@Linked -public class Config implements Listener { +/** + * The current player config. It is stored as JSON, fields missing in the + * stored JSON keep the default value of their initializer, so adding a new + * field does not need a new version. + *

+ * When a field gets removed, renamed or its meaning changes, copy this class + * to ConfigV{@link #VERSION} (without the {@link #STORE}), increase {@link #VERSION}, + * write a {@link ConfigConverter} from the copy to this class and register it in the {@link #STORE}. + */ +@Getter +@Setter +public class Config implements VersionedConfig { - static { - SerializeManager.add(new ConfigurationSerializableSerializer()); - } + public static final int VERSION = 2; - @Getter - private static Config instance; + public static final ConfigStore STORE = new ConfigStore<>(Config.class, VERSION, Config::new) + .gson(new GsonBuilder().registerTypeHierarchyAdapter(ItemStack.class, new ItemStackTypeAdapter()).create()) + .legacy(s -> s.startsWith("{@version(1)"), ConfigV1::parse) + .converter(new ConfigV1ToV2Converter()) + .userConfig("bausystem"); - { - instance = this; - } - - private final Map playerConfigurations = new HashMap<>(); - - private static final Map CONFIG_CONVERTER_MAP = new HashMap<>(); - - public static void addConfigConverter(ConfigConverter configConverter) { - CONFIG_CONVERTER_MAP.putIfAbsent(configConverter.version(), configConverter); - } - - @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - get(event.getPlayer()); - } - - @EventHandler - public void onPlayerQuit(PlayerQuitEvent event) { - save(event.getPlayer()); - playerConfigurations.remove(event.getPlayer().getUniqueId()); - } + @Setter(AccessLevel.NONE) + protected int version = VERSION; /** - * Get a PlayerConfig, optionally loads it from the DataBase and migrates it if necessary. - * - * @param player the player from whom to get the config. - * @return the config object + * BauGui item id to inventory slot */ - public YAPIONObject get(Player player) { - UUID uuid = player.getUniqueId(); - if (!playerConfigurations.containsKey(uuid)) { - String s = UserConfig.getConfig(uuid, "bausystem"); - YAPIONObject yapionObject; - if (s == null) { - yapionObject = ConfigCreator.createDefaultConfig(); - } else { - yapionObject = YAPIONParser.parse(s); - } - yapionObject = update(yapionObject); - playerConfigurations.put(uuid, yapionObject); - return yapionObject; - } - return playerConfigurations.get(uuid); - } - - public void saveAll() { - playerConfigurations.forEach((uuid, yapionObject) -> { - String string = yapionObject.toYAPION(new StringOutput()).getResult().replaceAll("\\+", "\\"); - UserConfig.updatePlayerConfig(uuid, "bausystem", string); - }); - playerConfigurations.clear(); - } + protected Map bauGui = defaultBauGui(); + protected int bauGuiSize = 45; /** - * Save a PlayerConfig, this does not remove the key value mapping from the map. - * - * @param player the player to save the config. + * Slots 0-8 are the hotbar, slots 9-12 the armor contents */ - public void save(Player player) { - UUID uuid = player.getUniqueId(); - if (playerConfigurations.containsKey(uuid)) { - YAPIONObject yapionObject = playerConfigurations.get(uuid); - String string = yapionObject.toYAPION(new StringOutput()).getResult().replaceAll("\\\\+", "\\\\"); - UserConfig.updatePlayerConfig(uuid, "bausystem", string); - } - } + protected ItemStack[] hotbar = DefaultHotbar.defaultHotbar(); - private YAPIONObject update(YAPIONObject yapionObject) { - int version = yapionObject.getPlainValue("@version"); - while (version < ConfigCreator.currentVersion) { - ConfigConverter configConverter = CONFIG_CONVERTER_MAP.getOrDefault(version, null); - if (configConverter == null) { - Bukkit.getLogger().log(Level.SEVERE, "No updater found for version " + version); - return ConfigCreator.createDefaultConfig(); - } - try { - configConverter.update(yapionObject); - } catch (Exception e) { - Bukkit.getLogger().log(Level.SEVERE, e.getMessage(), e); - return ConfigCreator.createDefaultConfig(); - } - int newVersion = yapionObject.getPlainValue("@version"); - if (version == newVersion) { - Bukkit.getLogger().log(Level.SEVERE, "Version Tag was the same after conversion"); - return ConfigCreator.createDefaultConfig(); - } - if (newVersion < version) { - Bukkit.getLogger().log(Level.SEVERE, "Version Tag was earlier after conversion"); - return ConfigCreator.createDefaultConfig(); - } - version = newVersion; - } - return yapionObject; + protected boolean nightVision = false; + protected boolean tntClickDetails = true; + protected boolean detonatorAutostart = false; + protected boolean inventoryFill = false; + protected boolean rayVisualizer = false; + protected boolean smartPlace = false; + protected boolean blockCounter = false; + protected boolean depthMessage = true; + + public static Map defaultBauGui() { + // 0: ? | 1: ? | 2: 10 | 3: 3 | 4: 7 | 5: 17 | 6: 15 | 7: ? | 8: ? + // 9: 5 | 10: ? | 11: ? | 12: ? | 13: ? | 14: ? | 15: ? | 16: ? | 17: 16 + // 18: 4 | 19: ? | 20: 19 | 21: 21 | 22: 9 | 23: 0 | 24: 1 | 25: ? | 26: 11 + // 27: 6 | 28: ? | 29: ? | 30: ? | 31: ? | 32: ? | 33: ? | 34: ? | 35: 18 + // 36: ? | 37: 23 | 38: 20 | 39: 8 | 40: 22 | 41: 26 | 42: 12 | 43: 14 | 44: ? + Map bauGui = new HashMap<>(); + bauGui.put(10, 2); bauGui.put(3, 3); bauGui.put(7, 4); bauGui.put(17, 5); bauGui.put(15, 6); + bauGui.put(5, 9); bauGui.put(4, 18); bauGui.put(6, 27); + bauGui.put(16, 17); bauGui.put(11, 26); bauGui.put(18, 35); + bauGui.put(19, 20); bauGui.put(21, 21); bauGui.put(9, 22); bauGui.put(0, 23); bauGui.put(1, 24); + bauGui.put(23, 37); bauGui.put(20, 38); bauGui.put(8, 39); bauGui.put(22, 40); bauGui.put(26, 41); bauGui.put(12, 42); bauGui.put(14, 43); + return bauGui; } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigConverter.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigConverter.java deleted file mode 100644 index 46119887..00000000 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigConverter.java +++ /dev/null @@ -1,56 +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.bausystem.configplayer; - -import yapion.hierarchy.types.YAPIONObject; - -/** - * A new {@link ConfigConverter} should be written when you remove anything - * from the Config or modify any mayor part. When you move anything from - * any key to any other key you should write a new {@link ConfigConverter}. - * For adding any new key you should be able to get the default without - * having it in the Config. Anything you need to change you should also - * change the {@link ConfigCreator} accordingly, to produce the new Config. - */ -public interface ConfigConverter { - - /** - * This describes the version this Converter can convert from. The version - * it should convert to is the version 1 above this number. But this is not - * a necessity. In the config Object as parameter given in {@link #update(YAPIONObject)} - * you should update the @version variable in the root object to the - * new version this converter produced. - * - * @return the version number - */ - int version(); - - /** - * This method should update everything needed to go from a lower config - * version to a higher. It should update the @version variable - * accordingly. Anything else is up the implementation. If anything goes wrong - * do not silently exit this method, throw an Exception. The updater Code will - * deal with it. Never leave the inputted object in a corrupted state. - * - * @param config the config object to update - */ - void update(YAPIONObject config); - -} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigCreator.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigCreator.java deleted file mode 100644 index 9a887208..00000000 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigCreator.java +++ /dev/null @@ -1,61 +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.bausystem.configplayer; - -import de.steamwar.bausystem.features.hotbar.DefaultHotbar; -import lombok.experimental.UtilityClass; -import yapion.hierarchy.types.YAPIONObject; - -@UtilityClass -public class ConfigCreator { - - public static final int currentVersion = 1; - - public YAPIONObject createDefaultConfig() { - YAPIONObject yapionObject = new YAPIONObject(); - // This call should never be touched - yapionObject.add("@version", currentVersion); - - // Any initialising goes into here - yapionObject.add("baugui", defaultBauGui()); - - // Default Hotbar Gui - yapionObject.add("hotbar", DefaultHotbar.defaultHotbar()); - return yapionObject; - } - - private YAPIONObject defaultBauGui() { - YAPIONObject yapionObject = new YAPIONObject(); - - // 0: ? | 1: ? | 2: 10 | 3: 3 | 4: 7 | 5: 17 | 6: 15 | 7: ? | 8: ? - // 9: 5 | 10: ? | 11: ? | 12: ? | 13: ? | 14: ? | 15: ? | 16: ? | 17: 16 - // 18: 4 | 19: ? | 20: 19 | 21: 21 | 22: 9 | 23: 0 | 24: 1 | 25: ? | 26: 11 - // 27: 6 | 28: ? | 29: ? | 30: ? | 31: ? | 32: ? | 33: ? | 34: ? | 35: 18 - // 36: ? | 37: 23 | 38: 20 | 39: 8 | 40: 22 | 41: 26 | 42: 12 | 43: 14 | 44: ? - - yapionObject.add("10", 2).add("3", 3).add("7", 4).add("17", 5).add("15", 6); - yapionObject.add("5", 9).add("4", 18).add("6", 27); - yapionObject.add("16", 17).add("11", 26).add("18", 35); - yapionObject.add("19", 20).add("21", 21).add("9", 22).add("0", 23).add("1", 24); - yapionObject.add("23", 37).add("20", 38).add("8", 39).add("22", 40).add("26", 41).add("12", 42).add("14", 43); - yapionObject.add("size", 45); - return yapionObject; - } -} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigV1.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigV1.java new file mode 100644 index 00000000..ba1f8cd0 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigV1.java @@ -0,0 +1,104 @@ +/* + * 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.bausystem.configplayer; + +import de.steamwar.bausystem.config.VersionedConfig; +import de.steamwar.bausystem.config.legacy.YapionReader; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The legacy config that was stored in the YAPION format. + */ +@Getter +@Setter +public class ConfigV1 implements VersionedConfig { + + public static final int VERSION = 1; + + @Setter(AccessLevel.NONE) + protected int version = VERSION; + + /** + * BauGui item id to inventory slot + */ + protected Map bauGui = new HashMap<>(); + protected int bauGuiSize = 45; + + /** + * Minecraft version to the hotbar serialized by YAPION, read with {@link YapionReader}. + * Use {@link de.steamwar.bausystem.config.legacy.YapionItemDeserializer} to get the items. + */ + protected Map> hotbars = new HashMap<>(); + + protected boolean nightVision = false; + protected boolean tntClickDetails = true; + protected boolean detonatorAutostart = false; + protected boolean inventoryFill = false; + protected boolean rayVisualizer = false; + protected boolean smartPlace = false; + protected boolean blockCounter = false; + protected boolean depthMessage = true; + + public static ConfigV1 parse(String yapion) { + Map object = YapionReader.parse(yapion); + ConfigV1 config = new ConfigV1(); + config.version = (Integer) object.get("@version"); + + if (object.get("baugui") instanceof Map bauGui) { + bauGui.forEach((key, value) -> { + if (key.equals("size")) { + config.bauGuiSize = (Integer) value; + } else { + config.bauGui.put(Integer.parseInt((String) key), (Integer) value); + } + }); + } + + object.forEach((key, value) -> { + if (!(value instanceof List hotbar)) return; + if (key.equals("hotbar")) { + // Hotbars were stored without version prior to 1.19 + config.hotbars.putIfAbsent(19, hotbar); + } else if (key.startsWith("hotbar-")) { + config.hotbars.put(Integer.parseInt(key.substring("hotbar-".length())), hotbar); + } + }); + + config.nightVision = getBoolean(object, "nightvision", config.nightVision); + config.tntClickDetails = getBoolean(object, "tnt_click_details", config.tntClickDetails); + config.detonatorAutostart = getBoolean(object, "detonator-autostart", config.detonatorAutostart); + config.inventoryFill = getBoolean(object, "inventoryfill", config.inventoryFill); + config.rayVisualizer = getBoolean(object, "rayvisualizer", config.rayVisualizer); + config.smartPlace = getBoolean(object, "smartPlace", config.smartPlace); + config.blockCounter = getBoolean(object, "blockCounter", config.blockCounter); + config.depthMessage = getBoolean(object, "depth_message", config.depthMessage); + return config; + } + + private static boolean getBoolean(Map object, String key, boolean defaultValue) { + return object.get(key) instanceof Boolean value ? value : defaultValue; + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/converter/.gitkeep b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/converter/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/converter/ConfigV1ToV2Converter.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/converter/ConfigV1ToV2Converter.java new file mode 100644 index 00000000..9886de52 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/converter/ConfigV1ToV2Converter.java @@ -0,0 +1,75 @@ +/* + * 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.bausystem.configplayer.converter; + +import de.steamwar.bausystem.config.ConfigConverter; +import de.steamwar.bausystem.config.legacy.YapionItemDeserializer; +import de.steamwar.bausystem.configplayer.Config; +import de.steamwar.bausystem.configplayer.ConfigV1; +import de.steamwar.core.Core; +import org.bukkit.Bukkit; +import org.bukkit.inventory.ItemStack; + +import java.util.HashMap; +import java.util.List; +import java.util.logging.Level; + +@Deprecated +public class ConfigV1ToV2Converter implements ConfigConverter { + + @Override + public int version() { + return ConfigV1.VERSION; + } + + @Override + public Config convert(ConfigV1 old) { + Config config = new Config(); + config.setBauGui(new HashMap<>(old.getBauGui())); + config.setBauGuiSize(old.getBauGuiSize()); + + List hotbar = old.getHotbars().get(Core.getVersion()); + if (hotbar != null) { + config.setHotbar(convertHotbar(hotbar)); + } + + config.setNightVision(old.isNightVision()); + config.setTntClickDetails(old.isTntClickDetails()); + config.setDetonatorAutostart(old.isDetonatorAutostart()); + config.setInventoryFill(old.isInventoryFill()); + config.setRayVisualizer(old.isRayVisualizer()); + config.setSmartPlace(old.isSmartPlace()); + config.setBlockCounter(old.isBlockCounter()); + config.setDepthMessage(old.isDepthMessage()); + return config; + } + + private ItemStack[] convertHotbar(List hotbar) { + ItemStack[] items = new ItemStack[13]; + for (int i = 0; i < Math.min(hotbar.size(), items.length); i++) { + try { + items[i] = YapionItemDeserializer.deserializeItem(hotbar.get(i)); + } catch (Exception e) { + Bukkit.getLogger().log(Level.WARNING, "Could not convert hotbar item " + i, e); + } + } + return items; + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/serializer/ConfigurationSerializableSerializer.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/serializer/ConfigurationSerializableSerializer.java deleted file mode 100644 index 2d054c3d..00000000 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/serializer/ConfigurationSerializableSerializer.java +++ /dev/null @@ -1,90 +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.bausystem.configplayer.serializer; - -import org.bukkit.configuration.serialization.ConfigurationSerializable; -import org.bukkit.configuration.serialization.ConfigurationSerialization; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import yapion.hierarchy.api.groups.YAPIONAnyType; -import yapion.hierarchy.types.YAPIONObject; -import yapion.serializing.api.SerializerObject; -import yapion.serializing.data.DeserializeData; -import yapion.serializing.data.SerializeData; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -import static yapion.utils.IdentifierUtils.TYPE_IDENTIFIER; - -public class ConfigurationSerializableSerializer extends SerializerObject { - - @Override - public Class type() { - return ConfigurationSerializable.class; - } - - @Override - public boolean isInterface() { - return true; - } - - @Override - public YAPIONObject serialize(SerializeData serializeData) { - YAPIONObject yapionObject = new YAPIONObject(); - yapionObject.add(TYPE_IDENTIFIER, serializeData.object.getClass().getTypeName()); - if (serializeData.object instanceof ItemStack) { - yapionObject.add(TYPE_IDENTIFIER, ItemStack.class.getTypeName()); - } - if (serializeData.object instanceof ItemMeta) { - yapionObject.add(TYPE_IDENTIFIER, ItemMeta.class.getTypeName()); - } - Map serializeDataMap = serializeData.object.serialize(); - serializeDataMap.forEach((s, o) -> { - YAPIONAnyType yapionAnyType = serializeData.serialize(o); - if (yapionAnyType instanceof YAPIONObject) { - YAPIONObject object = (YAPIONObject) yapionAnyType; - if (object.containsKey(TYPE_IDENTIFIER) && object.getPlainValue(TYPE_IDENTIFIER).equals("com.google.common.collect.RegularImmutableList")) { - object.put(TYPE_IDENTIFIER, ArrayList.class.getTypeName()); - } - } - yapionObject.add(s, yapionAnyType); - }); - return yapionObject; - } - - @Override - public ConfigurationSerializable deserialize(DeserializeData deserializeData) { - Map deserializeDataMap = new HashMap<>(); - deserializeData.object.forEach((s, yapionAnyType) -> { - if (s.equals(TYPE_IDENTIFIER)) { - if (yapionAnyType.toString().equals("(org.bukkit.inventory.meta.ItemMeta)")) { - deserializeDataMap.put("==", "ItemMeta"); - } else { - deserializeDataMap.put("==", deserializeData.deserialize(yapionAnyType)); - } - return; - } - deserializeDataMap.put(s, deserializeData.deserialize(yapionAnyType)); - }); - return ConfigurationSerialization.deserializeObject(deserializeDataMap); - } -} \ No newline at end of file diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/cannon/depth/Depth.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/cannon/depth/Depth.java index 89f0daa2..61d16843 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/cannon/depth/Depth.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/cannon/depth/Depth.java @@ -64,7 +64,7 @@ public class Depth { dimensions.setZ(Math.abs(dimensions.getZ())); RegionUtils.message(region, player -> { - if (Config.getInstance().get(player).getPlainValueOrDefault("depth_message", true)) { + if (Config.STORE.get(player).isDepthMessage()) { player.spigot().sendMessage(getMessage(player, dimensions.getBlockX() + 1, dimensions.getBlockY() + 1, dimensions.getBlockZ() + 1, tntCount)); } }); diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/cannon/depth/DepthCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/cannon/depth/DepthCommand.java index b9f71398..8eee56a6 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/cannon/depth/DepthCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/cannon/depth/DepthCommand.java @@ -34,11 +34,11 @@ public class DepthCommand extends SWCommand { @Register public void toggle(Player player) { - if (Config.getInstance().get(player).getPlainValueOrDefault("depth_message", true)) { - Config.getInstance().get(player).put("depth_message", false); + if (Config.STORE.get(player).isDepthMessage()) { + Config.STORE.get(player).setDepthMessage(false); BauSystem.MESSAGE.send("DEPTH_COUNTER_DISABLE", player); } else { - Config.getInstance().get(player).put("depth_message", true); + Config.STORE.get(player).setDepthMessage(true); BauSystem.MESSAGE.send("DEPTH_COUNTER_ENABLE", player); } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/detonator/Detonator.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/detonator/Detonator.java index 86932f2e..e04c78e1 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/detonator/Detonator.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/detonator/Detonator.java @@ -95,7 +95,7 @@ public class Detonator { public static void activateDetonator(DetonatorStorage detonator) { Player p = detonator.getPlayer(); - if (Config.getInstance().get(p).getPlainValueOrDefault("detonator-autostart", false)) { + if (Config.STORE.get(p).isDetonatorAutostart()) { AutostartListener.instance.activate(p); } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/detonator/DetonatorCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/detonator/DetonatorCommand.java index 5c936c49..5ac946e1 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/detonator/DetonatorCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/detonator/DetonatorCommand.java @@ -76,8 +76,8 @@ public class DetonatorCommand extends SWCommand { @Register(value = "autostart", description = "DETONATOR_HELP_AUTOSTART") public void toggleAutostartTimer(Player p) { - boolean current = Config.getInstance().get(p).getPlainValueOrDefault("detonator-autostart", false); - Config.getInstance().get(p).put("detonator-autostart", !current); + boolean current = Config.STORE.get(p).isDetonatorAutostart(); + Config.STORE.get(p).setDetonatorAutostart(!current); if (!current) { BauSystem.MESSAGE.send("DETONATOR_AUTOSTART_ENABLE", p); } else { diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/gui/editor/BauGuiMapping.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/gui/editor/BauGuiMapping.java index 7dfed54d..1c0b56b1 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/gui/editor/BauGuiMapping.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/gui/editor/BauGuiMapping.java @@ -28,7 +28,6 @@ import lombok.Setter; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitTask; -import yapion.hierarchy.types.YAPIONObject; import java.util.*; @@ -52,15 +51,15 @@ public class BauGuiMapping { } @Getter - private final YAPIONObject object; + private final Config config; @Getter private final Player owner; @Getter @Setter private boolean saved; - protected BauGuiMapping(YAPIONObject object, Player p) { - this.object = object; + protected BauGuiMapping(Config config, Player p) { + this.config = config; this.owner = p; fromUUID.put(p.getUniqueId(), this); mappings.add(this); @@ -70,19 +69,18 @@ public class BauGuiMapping { BauGuiMapping mapping = fromUUID.get(p.getUniqueId()); if (mapping == null) { - YAPIONObject yapionObject = Config.getInstance().get(p); - mapping = new BauGuiMapping(yapionObject.getObject("baugui"), p); + mapping = new BauGuiMapping(Config.STORE.get(p), p); } return mapping; } public boolean isShown(int id) { - return object.getPlainValueOrDefault(Integer.toString(id), -1) >= 0; + return config.getBauGui().getOrDefault(id, -1) >= 0; } public int getSlot(int id) { - return object.getPlainValue(Integer.toString(id)); + return config.getBauGui().get(id); } public Map getMapping() { @@ -94,7 +92,7 @@ public class BauGuiMapping { } public int getSize() { - return object.getPlainValueOrDefault("size", 45); + return config.getBauGuiSize(); } public void setSize(int size) { @@ -102,7 +100,7 @@ public class BauGuiMapping { return; } - object.add("size", size); + config.setBauGuiSize(size); } private void tick() { @@ -111,16 +109,17 @@ public class BauGuiMapping { public void save() { if (saved) return; - YAPIONObject config = Config.getInstance().get(owner); - config.add("baugui", object); - Config.getInstance().save(owner); + Config current = Config.STORE.get(owner); + current.setBauGui(config.getBauGui()); + current.setBauGuiSize(config.getBauGuiSize()); + Config.STORE.save(owner); saved = true; } private Map internalReadMap() { Map map = new HashMap<>(); for (Map.Entry e : BauGUI.getITEMS().entrySet()) { - Integer value = object.getPlainValueOrDefault(e.getKey().toString(), -1); + Integer value = config.getBauGui().getOrDefault(e.getKey(), -1); if (value == -1) continue; map.put(e.getKey(), value); } @@ -129,10 +128,10 @@ public class BauGuiMapping { private void internalWriteMap(Map map) { for (Map.Entry e : BauGUI.getITEMS().entrySet()) { - object.remove(e.getKey().toString()); + config.getBauGui().remove(e.getKey()); Integer value = map.getOrDefault(e.getKey(), -1); if (value == -1) continue; - object.add(e.getKey().toString(), value); + config.getBauGui().put(e.getKey(), value); } } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hotbar/DefaultHotbar.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hotbar/DefaultHotbar.java index 602f1817..c8340a7c 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hotbar/DefaultHotbar.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hotbar/DefaultHotbar.java @@ -20,39 +20,23 @@ package de.steamwar.bausystem.features.hotbar; import de.steamwar.bausystem.configplayer.Config; -import de.steamwar.core.Core; import lombok.experimental.UtilityClass; +import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import yapion.hierarchy.types.YAPIONArray; -import yapion.hierarchy.types.YAPIONObject; -import yapion.hierarchy.types.YAPIONValue; -import yapion.parser.YAPIONParser; -import yapion.serializing.YAPIONDeserializer; -import yapion.serializing.YAPIONSerializer; +import org.bukkit.inventory.meta.ItemMeta; -import java.util.HashSet; -import java.util.Set; +import java.util.List; @UtilityClass public class DefaultHotbar { - private final YAPIONArray DEFAULT_HOTBAR = YAPIONParser.parse("{[{@type(org.bukkit.inventory.ItemStack)v(2230)type(WOODEN_AXE)meta{@type(org.bukkit.inventory.meta.ItemMeta)meta-type(UNSPECIFIC)display-name(WorldEdit Wand)lore{@type(java.util.ArrayList)values[Left click: select pos #1,Right click: select pos #2]}}},{@type(org.bukkit.inventory.ItemStack)v(2230)type(COMPASS)meta{@type(org.bukkit.inventory.meta.ItemMeta)meta-type(UNSPECIFIC)display-name(Navigation Wand)lore{@type(java.util.ArrayList)values[Left click: jump to location,Right click: pass through walls]}}},null,null,null,null,null,null,{@type(org.bukkit.inventory.ItemStack)v(2230)type(NETHER_STAR)meta{@type(org.bukkit.inventory.meta.ItemMeta)meta-type(UNSPECIFIC)display-name(§eBau GUI)}},null,null,{@type(org.bukkit.inventory.ItemStack)v(3117)type(ELYTRA)},null]}").getArray(""); - public void updateHotbar(Player p) { ItemStack[] hotbar = new ItemStack[13]; System.arraycopy(p.getInventory().getContents(), 0, hotbar, 0, 9); System.arraycopy(p.getInventory().getArmorContents(), 0, hotbar, 9, 4); - YAPIONArray yapionArray = new YAPIONArray(); - for (ItemStack itemStack : hotbar) { - if (itemStack != null) { - yapionArray.add(YAPIONSerializer.serialize(itemStack)); - } else { - yapionArray.add(new YAPIONValue<>(null)); - } - } - Config.getInstance().get(p).add("hotbar-" + Core.getVersion(), yapionArray); - Config.getInstance().save(p); + Config.STORE.get(p).setHotbar(copy(hotbar)); + Config.STORE.save(p); } public void setHotbar(Player p) { @@ -66,28 +50,34 @@ public class DefaultHotbar { } public ItemStack[] getItems(Player p) { - Config.getInstance().get(p).remap("hotbar", "hotbar-19"); - YAPIONArray yapionArray = Config.getInstance().get(p).getYAPIONArrayOrSetDefault("hotbar-" + Core.getVersion(), defaultHotbar()); + return copy(Config.STORE.get(p).getHotbar()); + } + + public ItemStack[] defaultHotbar() { ItemStack[] hotbar = new ItemStack[13]; - Set invalid = new HashSet<>(); - yapionArray.forEach((integer, yapionAnyType) -> { - if (yapionAnyType instanceof YAPIONValue) { - hotbar[integer] = null; - } else { - try { - hotbar[integer] = YAPIONDeserializer.deserialize((YAPIONObject) yapionAnyType); - } catch (Exception e) { - invalid.add(integer); - hotbar[integer] = null; - } - } - }); - invalid.forEach(i -> yapionArray.set(i, new YAPIONValue<>(null))); - if (!invalid.isEmpty()) Config.getInstance().save(p); + hotbar[0] = item(Material.WOODEN_AXE, "WorldEdit Wand", "Left click: select pos #1", "Right click: select pos #2"); + hotbar[1] = item(Material.COMPASS, "Navigation Wand", "Left click: jump to location", "Right click: pass through walls"); + hotbar[8] = item(Material.NETHER_STAR, "§eBau GUI"); + hotbar[11] = new ItemStack(Material.ELYTRA); return hotbar; } - public YAPIONArray defaultHotbar() { - return DEFAULT_HOTBAR; + private ItemStack item(Material material, String name, String... lore) { + ItemStack itemStack = new ItemStack(material); + ItemMeta meta = itemStack.getItemMeta(); + meta.setDisplayName(name); + if (lore.length > 0) { + meta.setLore(List.of(lore)); + } + itemStack.setItemMeta(meta); + return itemStack; + } + + private ItemStack[] copy(ItemStack[] hotbar) { + ItemStack[] copy = new ItemStack[13]; + for (int i = 0; i < Math.min(hotbar.length, copy.length); i++) { + copy[i] = hotbar[i] == null ? null : hotbar[i].clone(); + } + return copy; } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFillBauGuiItem.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFillBauGuiItem.java index 13e3af35..e23d71de 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFillBauGuiItem.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFillBauGuiItem.java @@ -49,7 +49,7 @@ public class InventoryFillBauGuiItem extends BauGuiItem { @Override public ItemStack getItem(Player player) { - String loreKey = Config.getInstance().get(player).getPlainValueOrDefault("inventoryfill", false) ? "OTHER_ITEMS_INVENTORY_FILL_LORE_ACTIVE" : "OTHER_ITEMS_INVENTORY_FILL_LORE_INACTIVE"; + String loreKey = Config.STORE.get(player).isInventoryFill() ? "OTHER_ITEMS_INVENTORY_FILL_LORE_ACTIVE" : "OTHER_ITEMS_INVENTORY_FILL_LORE_INACTIVE"; return new SWItem(Material.HOPPER, BauSystem.MESSAGE.parse("OTHER_ITEMS_INVENTORY_FILL_NAME", player), Collections.singletonList(BauSystem.MESSAGE.parse(loreKey, player)), false, clickType -> { }).getItemStack(); } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFiller.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFiller.java index 8312e77c..48341152 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFiller.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFiller.java @@ -40,7 +40,7 @@ public class InventoryFiller implements Listener { @EventHandler public void onPlayerDropItem(PlayerDropItemEvent event) { if (!Permission.BUILD.hasPermission(event.getPlayer())) return; - if (!Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("inventoryfill", false)) return; + if (!Config.STORE.get(event.getPlayer()).isInventoryFill()) return; if (!event.getPlayer().isSneaking()) return; Block block = event.getPlayer().getTargetBlockExact(5); if (block == null) return; @@ -62,7 +62,7 @@ public class InventoryFiller implements Listener { @EventHandler public void onPlayerItemHeld(PlayerItemHeldEvent event) { if (!Permission.BUILD.hasPermission(event.getPlayer())) return; - if (!Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("inventoryfill", false)) return; + if (!Config.STORE.get(event.getPlayer()).isInventoryFill()) return; if (!event.getPlayer().isSneaking()) return; ItemStack itemStack = event.getPlayer().getInventory().getItemInMainHand(); if (itemStack.getType() == Material.AIR) return; diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFillerCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFillerCommand.java index 0e9887ab..4d4b5f05 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFillerCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFillerCommand.java @@ -68,8 +68,8 @@ public class InventoryFillerCommand extends SWCommand { @Register(description = {"INVENTORY_FILL_HELP", "INVENTORY_FILL_INFO"}) public void toggle(Player player) { - boolean inventoryFill = Config.getInstance().get(player).getPlainValueOrDefault("inventoryfill", false); - Config.getInstance().get(player).put("inventoryfill", !inventoryFill); + boolean inventoryFill = Config.STORE.get(player).isInventoryFill(); + Config.STORE.get(player).setInventoryFill(!inventoryFill); if (!inventoryFill) { SWUtils.sendToActionbar(player, BauSystem.MESSAGE.parse("INVENTORY_FILL_ENABLE", player)); BauSystem.MESSAGE.send("INVENTORY_FILL_INFO", player); diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/rayvisualizer/RayVisualizerCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/rayvisualizer/RayVisualizerCommand.java index 2732c140..af5d4eb0 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/rayvisualizer/RayVisualizerCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/rayvisualizer/RayVisualizerCommand.java @@ -167,7 +167,7 @@ public class RayVisualizerCommand extends SWCommand implements Listener { @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { if (!Permission.BUILD.hasPermission(event.getPlayer())) return; - boolean rayvisualizer = Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("rayvisualizer", false); + boolean rayvisualizer = Config.STORE.get(event.getPlayer()).isRayVisualizer(); if (rayvisualizer) server.addPlayer(event.getPlayer()); } @@ -181,7 +181,7 @@ public class RayVisualizerCommand extends SWCommand implements Listener { Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { for (Player player : Bukkit.getOnlinePlayers()) { if (Permission.BUILD.hasPermission(player)) { - boolean rayvisualizer = Config.getInstance().get(player).getPlainValueOrDefault("rayvisualizer", false); + boolean rayvisualizer = Config.STORE.get(player).isRayVisualizer(); if (rayvisualizer) server.addPlayer(player); } else { server.removePlayer(player); @@ -192,8 +192,8 @@ public class RayVisualizerCommand extends SWCommand implements Listener { @Register public void toggle(@Validator Player player) { - boolean rayvisualizer = Config.getInstance().get(player).getPlainValueOrDefault("rayvisualizer", false); - Config.getInstance().get(player).put("rayvisualizer", !rayvisualizer); + boolean rayvisualizer = Config.STORE.get(player).isRayVisualizer(); + Config.STORE.get(player).setRayVisualizer(!rayvisualizer); if (!rayvisualizer) { SWUtils.sendToActionbar(player, BauSystem.MESSAGE.parse("RAY_VISUALIZER_ENABLE", player)); server.addPlayer(player); diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/SimulatorElement.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/SimulatorElement.java index 87719f2a..27538d7b 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/SimulatorElement.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/SimulatorElement.java @@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.simulator.data; import de.steamwar.bausystem.features.simulator.execute.SimulatorAction; import de.steamwar.bausystem.features.simulator.gui.base.SimulatorBaseGui; +import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig; import de.steamwar.inventory.InvCallback; import de.steamwar.inventory.SWItem; import lombok.Getter; @@ -28,7 +29,6 @@ import lombok.Setter; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.util.Vector; -import yapion.hierarchy.types.YAPIONObject; import java.util.ArrayList; import java.util.Comparator; @@ -121,9 +121,9 @@ public abstract class SimulatorElement { public abstract String getType(); - public void saveExtra(YAPIONObject elementObject) { + public void saveExtra(SimulatorConfig.Element element) { } - public void loadExtra(YAPIONObject elementObject) { + public void loadExtra(SimulatorConfig.Element element) { } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/SimulatorPhase.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/SimulatorPhase.java index b8c951ab..2eae0d82 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/SimulatorPhase.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/SimulatorPhase.java @@ -20,10 +20,10 @@ package de.steamwar.bausystem.features.simulator.data; import de.steamwar.bausystem.features.simulator.execute.SimulatorAction; +import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig; import lombok.Getter; import lombok.Setter; import org.bukkit.util.Vector; -import yapion.hierarchy.types.YAPIONObject; import java.util.function.BiConsumer; @@ -38,9 +38,9 @@ public abstract class SimulatorPhase { public abstract void toSimulatorActions(Vector position, BiConsumer tickStart, BiConsumer tickEnd); - public void saveExtra(YAPIONObject phaseObject) { + public void saveExtra(SimulatorConfig.Phase phase) { } - public void loadExtra(YAPIONObject phaseObject) { + public void loadExtra(SimulatorConfig.Phase phase) { } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/observer/ObserverPhase.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/observer/ObserverPhase.java index f69ff797..73c00d19 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/observer/ObserverPhase.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/observer/ObserverPhase.java @@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.simulator.data.observer; import de.steamwar.bausystem.features.simulator.data.SimulatorPhase; import de.steamwar.bausystem.features.simulator.execute.SimulatorAction; +import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -31,7 +32,6 @@ import org.bukkit.block.BlockFace; import org.bukkit.block.BlockState; import org.bukkit.block.data.type.Observer; import org.bukkit.util.Vector; -import yapion.hierarchy.types.YAPIONObject; import java.util.function.BiConsumer; @@ -70,12 +70,12 @@ public final class ObserverPhase extends SimulatorPhase { } @Override - public void saveExtra(YAPIONObject phaseObject) { - phaseObject.add("orientation", orientation.name()); + public void saveExtra(SimulatorConfig.Phase phase) { + phase.setOrientation(orientation); } @Override - public void loadExtra(YAPIONObject phaseObject) { - orientation = BlockFace.valueOf(phaseObject.getPlainValue("orientation")); + public void loadExtra(SimulatorConfig.Phase phase) { + if (phase.getOrientation() != null) orientation = phase.getOrientation(); } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/tnt/TNTElement.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/tnt/TNTElement.java index 735ca292..a8353907 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/tnt/TNTElement.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/tnt/TNTElement.java @@ -24,13 +24,13 @@ import de.steamwar.bausystem.features.simulator.data.SimulatorElement; import de.steamwar.bausystem.features.simulator.data.SimulatorGroup; import de.steamwar.bausystem.features.simulator.gui.SimulatorTNTGui; import de.steamwar.bausystem.features.simulator.gui.base.SimulatorBaseGui; +import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig; import de.steamwar.inventory.InvCallback; import de.steamwar.inventory.SWItem; import lombok.Getter; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.util.Vector; -import yapion.hierarchy.types.YAPIONObject; public final class TNTElement extends SimulatorElement { @@ -100,14 +100,14 @@ public final class TNTElement extends SimulatorElement { } @Override - public void saveExtra(YAPIONObject elementObject) { - elementObject.add("alignmentX", alignment.getX()); - elementObject.add("alignmentZ", alignment.getZ()); + public void saveExtra(SimulatorConfig.Element element) { + element.setAlignmentX(alignment.getX()); + element.setAlignmentZ(alignment.getZ()); } @Override - public void loadExtra(YAPIONObject elementObject) { - alignment.setX(elementObject.getDoubleOrSetDefault("alignmentX", 0)); - alignment.setZ(elementObject.getDoubleOrSetDefault("alignmentZ", 0)); + public void loadExtra(SimulatorConfig.Element element) { + alignment.setX(element.getAlignmentX() != null ? element.getAlignmentX() : 0); + alignment.setZ(element.getAlignmentZ() != null ? element.getAlignmentZ() : 0); } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/tnt/TNTPhase.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/tnt/TNTPhase.java index bb5be0ce..bb0f6eb2 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/tnt/TNTPhase.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/tnt/TNTPhase.java @@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.simulator.data.tnt; import de.steamwar.bausystem.features.simulator.data.SimulatorPhase; import de.steamwar.bausystem.features.simulator.execute.SimulatorAction; +import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig; import de.steamwar.bausystem.region.Region; import de.steamwar.bausystem.region.flags.Flag; import de.steamwar.bausystem.region.flags.FreezeMode; @@ -31,7 +32,6 @@ import org.bukkit.Location; import org.bukkit.World; import org.bukkit.entity.TNTPrimed; import org.bukkit.util.Vector; -import yapion.hierarchy.types.YAPIONObject; import java.util.function.BiConsumer; @@ -83,18 +83,18 @@ public final class TNTPhase extends SimulatorPhase { } @Override - public void saveExtra(YAPIONObject phaseObject) { - phaseObject.add("count", count); - phaseObject.add("xJump", xJump); - phaseObject.add("yJump", yJump); - phaseObject.add("zJump", zJump); + public void saveExtra(SimulatorConfig.Phase phase) { + phase.setCount(count); + phase.setXJump(xJump); + phase.setYJump(yJump); + phase.setZJump(zJump); } @Override - public void loadExtra(YAPIONObject phaseObject) { - count = phaseObject.getPlainValue("count"); - xJump = phaseObject.getPlainValue("xJump"); - yJump = phaseObject.getPlainValue("yJump"); - zJump = phaseObject.getPlainValue("zJump"); + public void loadExtra(SimulatorConfig.Phase phase) { + if (phase.getCount() != null) count = phase.getCount(); + if (phase.getXJump() != null) xJump = phase.getXJump(); + if (phase.getYJump() != null) yJump = phase.getYJump(); + if (phase.getZJump() != null) zJump = phase.getZJump(); } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimFormatSimulatorLoader.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimFormatSimulatorLoader.java index 5257ce12..9310fa2d 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimFormatSimulatorLoader.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimFormatSimulatorLoader.java @@ -19,6 +19,7 @@ package de.steamwar.bausystem.features.simulator.storage; +import com.google.gson.JsonParseException; import de.steamwar.bausystem.features.simulator.data.Simulator; import de.steamwar.bausystem.features.simulator.data.SimulatorElement; import de.steamwar.bausystem.features.simulator.data.SimulatorGroup; @@ -29,12 +30,7 @@ import de.steamwar.bausystem.features.simulator.data.redstone.RedstoneElement; import de.steamwar.bausystem.features.simulator.data.redstone.RedstonePhase; import de.steamwar.bausystem.features.simulator.data.tnt.TNTElement; import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase; -import org.bukkit.Material; import org.bukkit.util.Vector; -import yapion.exceptions.YAPIONException; -import yapion.hierarchy.types.YAPIONArray; -import yapion.hierarchy.types.YAPIONObject; -import yapion.parser.YAPIONParser; import java.io.File; import java.io.IOException; @@ -51,44 +47,41 @@ public class SimFormatSimulatorLoader implements SimulatorLoader { return Optional.empty(); } - YAPIONObject yapionObject; + SimulatorConfig config; try { - yapionObject = YAPIONParser.parse(file); - } catch (YAPIONException | IOException e) { + config = SimulatorConfig.STORE.load(file); + } catch (JsonParseException | IllegalStateException | IOException e) { return Optional.empty(); } String name = file.getName().substring(0, file.getName().length() - ".sim".length()); Simulator simulator = new Simulator(name); - loadSimulator(yapionObject, simulator); + loadSimulator(config, simulator); return Optional.of(Collections.singletonList(simulator)); } - private void loadSimulator(YAPIONObject simulatorObject, Simulator simulator) { - simulator.setMaterial(Material.valueOf(simulatorObject.getPlainValue("material"))); - simulator.setAutoTrace(simulatorObject.getPlainValue("autoTrace")); - simulator.setAutoTestblock(simulatorObject.getPlainValueOrDefault("autoTestblock", false)); + private void loadSimulator(SimulatorConfig config, Simulator simulator) { + if (config.getMaterial() != null) simulator.setMaterial(config.getMaterial()); + simulator.setAutoTrace(config.isAutoTrace()); + simulator.setAutoTestblock(config.isAutoTestblock()); - YAPIONArray groups = simulatorObject.getArray("groups"); - groups.streamObject().forEach(groupObject -> { + config.getGroups().forEach(groupConfig -> { SimulatorGroup simulatorGroup = new SimulatorGroup(); - loadGroup(groupObject, simulatorGroup); + loadGroup(groupConfig, simulatorGroup); simulator.add(simulatorGroup); }); } - private void loadGroup(YAPIONObject groupObject, SimulatorGroup group) { - group.setMaterial(Material.valueOf(groupObject.getPlainValue("material"))); - group.setDisabled(groupObject.getPlainValue("disabled")); + private void loadGroup(SimulatorConfig.Group groupConfig, SimulatorGroup group) { + if (groupConfig.getMaterial() != null) group.setMaterial(groupConfig.getMaterial()); + group.setDisabled(groupConfig.isDisabled()); - YAPIONArray elements = groupObject.getArray("elements"); - elements.streamObject().forEach(elementObject -> { + groupConfig.getElements().forEach(elementConfig -> { SimulatorElement element; Supplier phaseConstructor; - Vector position = new Vector(elementObject.getDouble("x"), elementObject.getDouble("y"), elementObject.getDouble("z")); - String type = elementObject.getPlainValue("type"); - switch (type) { + Vector position = new Vector(elementConfig.getX(), elementConfig.getY(), elementConfig.getZ()); + switch (String.valueOf(elementConfig.getType())) { case "TNT": element = new TNTElement(position); phaseConstructor = TNTPhase::new; @@ -109,28 +102,27 @@ public class SimFormatSimulatorLoader implements SimulatorLoader { if (element == null) { return; } - loadElement(elementObject, element, phaseConstructor); + loadElement(elementConfig, element, phaseConstructor); group.add(element); }); } - private void loadElement(YAPIONObject elementObject, SimulatorElement element, Supplier phaseConstructor) { - element.setMaterial(Material.valueOf(elementObject.getPlainValue("material"))); - element.setDisabled(elementObject.getPlainValue("disabled")); - element.loadExtra(elementObject); + private void loadElement(SimulatorConfig.Element elementConfig, SimulatorElement element, Supplier phaseConstructor) { + if (elementConfig.getMaterial() != null) element.setMaterial(elementConfig.getMaterial()); + element.setDisabled(elementConfig.isDisabled()); + element.loadExtra(elementConfig); - YAPIONArray phases = elementObject.getArray("phases"); - phases.streamObject().forEach(phaseObject -> { + elementConfig.getPhases().forEach(phaseConfig -> { SimulatorPhase phase = phaseConstructor.get(); - loadPhase(phaseObject, phase); + loadPhase(phaseConfig, phase); ((SimulatorElement) element).add(phase); }); } - private void loadPhase(YAPIONObject phaseObject, SimulatorPhase phase) { - phase.setTickOffset(phaseObject.getPlainValue("tickOffset")); - phase.setLifetime(phaseObject.getPlainValue("lifetime")); - phase.setOrder(phaseObject.getPlainValue("order")); - phase.loadExtra(phaseObject); + private void loadPhase(SimulatorConfig.Phase phaseConfig, SimulatorPhase phase) { + phase.setTickOffset(phaseConfig.getTickOffset()); + phase.setLifetime(phaseConfig.getLifetime()); + phase.setOrder(phaseConfig.getOrder()); + phase.loadExtra(phaseConfig); } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorConfig.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorConfig.java new file mode 100644 index 00000000..a1c56f86 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorConfig.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.bausystem.features.simulator.storage; + +import de.steamwar.bausystem.config.ConfigStore; +import de.steamwar.bausystem.config.VersionedConfig; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import org.bukkit.Material; +import org.bukkit.block.BlockFace; + +import java.util.ArrayList; +import java.util.List; + +/** + * The current stored form of a simulator, saved as JSON in a .sim file. Files + * saved before the version was stored have no version and are version 1. + * Fields missing in the stored JSON keep the default value of their initializer, + * so adding a new field does not need a new version. + *

+ * When a field gets removed, renamed or its meaning changes, copy this class + * to SimulatorConfigV{@link #VERSION} (without the {@link #STORE}), increase {@link #VERSION}, write a + * {@link de.steamwar.bausystem.config.ConfigConverter} from the copy to + * this class and register it in the {@link #STORE}. + */ +@Getter +@Setter +public class SimulatorConfig implements VersionedConfig { + + public static final int VERSION = 1; + + /** + * Files saved before the version was stored are version 1 + */ + public static final ConfigStore STORE = new ConfigStore<>(SimulatorConfig.class, VERSION, SimulatorConfig::new) + .unversioned(1); + + @Setter(AccessLevel.NONE) + protected int version = VERSION; + + protected Material material = Material.BARREL; + protected boolean autoTrace = false; + protected boolean autoTestblock = false; + protected List groups = new ArrayList<>(); + + @Getter + @Setter + public static class Group { + + protected Material material = Material.CHEST; + protected boolean disabled = false; + protected List elements = new ArrayList<>(); + } + + @Getter + @Setter + public static class Element { + + /** + * One of "TNT", "Redstone" or "Observer" + */ + protected String type; + protected Material material; + protected boolean disabled = false; + protected double x; + protected double y; + protected double z; + + /** + * Only used by TNT + */ + protected Double alignmentX; + protected Double alignmentZ; + + protected List phases = new ArrayList<>(); + } + + @Getter + @Setter + public static class Phase { + + protected int tickOffset; + protected int lifetime; + protected int order; + + /** + * Only used by TNT + */ + protected Integer count; + protected Boolean xJump; + protected Boolean yJump; + protected Boolean zJump; + + /** + * Only used by Observer + */ + protected BlockFace orientation; + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorFormatSimulatorLoader.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorFormatSimulatorLoader.java index 65815440..a7f52ede 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorFormatSimulatorLoader.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorFormatSimulatorLoader.java @@ -19,24 +19,30 @@ package de.steamwar.bausystem.features.simulator.storage; +import de.steamwar.bausystem.config.legacy.YapionReader; import de.steamwar.bausystem.features.simulator.data.Simulator; import de.steamwar.bausystem.features.simulator.data.SimulatorGroup; import de.steamwar.bausystem.features.simulator.data.tnt.TNTElement; import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase; import org.bukkit.Material; import org.bukkit.util.Vector; -import yapion.exceptions.YAPIONException; -import yapion.hierarchy.types.YAPIONArray; -import yapion.hierarchy.types.YAPIONObject; -import yapion.parser.YAPIONParser; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; +import static de.steamwar.bausystem.config.legacy.YapionReader.*; + +/** + * Loads the legacy .simulator files stored as YAPION, they are deleted after loading + * and saved in the current format by the {@link SimulatorSaver}. + */ +@Deprecated public class SimulatorFormatSimulatorLoader implements SimulatorLoader { @Override @@ -45,20 +51,19 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader { return Optional.empty(); } - YAPIONObject yapionObject; + Map yapionObject; try { - yapionObject = YAPIONParser.parse(file); - } catch (YAPIONException | IOException e) { + yapionObject = YapionReader.parse(Files.readString(file.toPath(), StandardCharsets.UTF_8)); + } catch (IllegalArgumentException | IOException e) { return Optional.empty(); } String name = file.getName().substring(0, file.getName().length() - ".simulator".length()); Simulator simulator = new Simulator(name); - simulator.setMaterial(Material.valueOf(yapionObject.getStringOrDefault("material", "BARREL"))); + simulator.setMaterial(Material.valueOf(getString(yapionObject, "material", "BARREL"))); - YAPIONArray tntElements = yapionObject.getArrayOrDefault("tntElements", new YAPIONArray()); - for (YAPIONObject element : tntElements.streamObject().collect(Collectors.toList())) { + for (Map element : getObjects(yapionObject, "tntElements")) { if (element.containsKey("elements")) { simulator.add(loadGroup(element)); } else { @@ -70,27 +75,26 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader { return Optional.of(Collections.singletonList(simulator)); } - private SimulatorGroup loadGroup(YAPIONObject element) { - double x = element.getDoubleOrDefault("x", 0); - double y = element.getDoubleOrDefault("y", 0); - double z = element.getDoubleOrDefault("z", 0); - int tickOffset = element.getIntOrDefault("tickOffset", 0); + private SimulatorGroup loadGroup(Map element) { + double x = getDouble(element, "x", 0); + double y = getDouble(element, "y", 0); + double z = getDouble(element, "z", 0); + int tickOffset = getInt(element, "tickOffset", 0); SimulatorGroup simulatorGroup = new SimulatorGroup(); - simulatorGroup.setDisabled(element.getBooleanOrDefault("disabled", false)); + simulatorGroup.setDisabled(getBoolean(element, "disabled", false)); - YAPIONArray elements = element.getArrayOrDefault("elements", new YAPIONArray()); - for (YAPIONObject e : elements.streamObject().collect(Collectors.toList())) { + for (Map e : getObjects(element, "elements")) { simulatorGroup.add(loadElement(e, x, y, z, tickOffset)); } return simulatorGroup; } - private TNTElement loadElement(YAPIONObject element, double dx, double dy, double dz, int dTickOffset) { - TNTElement tntElement = new TNTElement(new Vector(element.getDouble("x") + dx, element.getDouble("y") + dy, element.getDouble("z") + dz)); - tntElement.setDisabled(element.getBooleanOrDefault("disabled", false)); - tntElement.setMaterial(Material.valueOf(element.getStringOrDefault("material", "TNT"))); + private TNTElement loadElement(Map element, double dx, double dy, double dz, int dTickOffset) { + TNTElement tntElement = new TNTElement(new Vector(requireDouble(element, "x") + dx, requireDouble(element, "y") + dy, requireDouble(element, "z") + dz)); + tntElement.setDisabled(getBoolean(element, "disabled", false)); + tntElement.setMaterial(Material.valueOf(getString(element, "material", "TNT"))); TNTPhase tntPhase = loadPhase(element); tntPhase.setTickOffset(tntPhase.getTickOffset() + dTickOffset); @@ -99,12 +103,12 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader { return tntElement; } - private SimulatorGroup loadElement(YAPIONObject element) { + private SimulatorGroup loadElement(Map element) { SimulatorGroup simulatorGroup = new SimulatorGroup(); - TNTElement tntElement = new TNTElement(new Vector(element.getDouble("x"), element.getDouble("y"), element.getDouble("z"))); - tntElement.setDisabled(element.getBooleanOrDefault("disabled", false)); - tntElement.setMaterial(Material.valueOf(element.getStringOrDefault("material", "TNT"))); + TNTElement tntElement = new TNTElement(new Vector(requireDouble(element, "x"), requireDouble(element, "y"), requireDouble(element, "z"))); + tntElement.setDisabled(getBoolean(element, "disabled", false)); + tntElement.setMaterial(Material.valueOf(getString(element, "material", "TNT"))); simulatorGroup.add(tntElement); tntElement.add(loadPhase(element)); @@ -112,15 +116,15 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader { return simulatorGroup; } - private TNTPhase loadPhase(YAPIONObject element) { + private TNTPhase loadPhase(Map element) { TNTPhase tntPhase = new TNTPhase(); - tntPhase.setLifetime(element.getIntOrDefault("fuseTicks", 80)); - tntPhase.setCount(element.getIntOrDefault("count", 1)); - tntPhase.setTickOffset(element.getIntOrDefault("tickOffset", 0)); - tntPhase.setXJump(element.getBooleanOrDefault("xVelocity", false)); - tntPhase.setYJump(element.getBooleanOrDefault("yVelocity", false)); - tntPhase.setZJump(element.getBooleanOrDefault("zVelocity", false)); - switch (element.getStringOrDefault("order", "REPEATER")) { + tntPhase.setLifetime(getInt(element, "fuseTicks", 80)); + tntPhase.setCount(getInt(element, "count", 1)); + tntPhase.setTickOffset(getInt(element, "tickOffset", 0)); + tntPhase.setXJump(getBoolean(element, "xVelocity", false)); + tntPhase.setYJump(getBoolean(element, "yVelocity", false)); + tntPhase.setZJump(getBoolean(element, "zVelocity", false)); + switch (getString(element, "order", "REPEATER")) { case "REPEATER": tntPhase.setOrder(0); break; @@ -133,4 +137,8 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader { } return tntPhase; } + + private double requireDouble(Map element, String key) { + return ((Number) element.get(key)).doubleValue(); + } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorSaver.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorSaver.java index 1f3345e4..bfa278e7 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorSaver.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorSaver.java @@ -21,9 +21,6 @@ package de.steamwar.bausystem.features.simulator.storage; import de.steamwar.bausystem.features.simulator.data.Simulator; import lombok.experimental.UtilityClass; -import yapion.hierarchy.output.FileOutput; -import yapion.hierarchy.types.YAPIONArray; -import yapion.hierarchy.types.YAPIONObject; import java.io.File; import java.io.IOException; @@ -36,48 +33,42 @@ public class SimulatorSaver { directory.mkdirs(); } - YAPIONObject simulatorObject = new YAPIONObject(); - simulatorObject.add("material", simulator.getMaterial().name()); - simulatorObject.add("autoTrace", simulator.isAutoTrace()); - simulatorObject.add("autoTestblock", simulator.isAutoTestblock()); + SimulatorConfig config = new SimulatorConfig(); + config.setMaterial(simulator.getMaterial()); + config.setAutoTrace(simulator.isAutoTrace()); + config.setAutoTestblock(simulator.isAutoTestblock()); - YAPIONArray groups = new YAPIONArray(); simulator.getGroups().forEach(group -> { - YAPIONObject groupObject = new YAPIONObject(); - groupObject.add("material", group.getMaterial().name()); - groupObject.add("disabled", group.isDisabled()); + SimulatorConfig.Group groupConfig = new SimulatorConfig.Group(); + groupConfig.setMaterial(group.getMaterial()); + groupConfig.setDisabled(group.isDisabled()); - YAPIONArray elements = new YAPIONArray(); group.getElements().forEach(element -> { - YAPIONObject elementObject = new YAPIONObject(); - elementObject.add("type", element.getType()); - elementObject.add("material", element.getMaterial().name()); - elementObject.add("disabled", element.isDisabled()); - elementObject.add("x", element.getPosition().getX()); - elementObject.add("y", element.getPosition().getY()); - elementObject.add("z", element.getPosition().getZ()); - element.saveExtra(elementObject); + SimulatorConfig.Element elementConfig = new SimulatorConfig.Element(); + elementConfig.setType(element.getType()); + elementConfig.setMaterial(element.getMaterial()); + elementConfig.setDisabled(element.isDisabled()); + elementConfig.setX(element.getPosition().getX()); + elementConfig.setY(element.getPosition().getY()); + elementConfig.setZ(element.getPosition().getZ()); + element.saveExtra(elementConfig); - YAPIONArray phases = new YAPIONArray(); element.getPhases().forEach(phase -> { - YAPIONObject phaseObject = new YAPIONObject(); - phaseObject.add("tickOffset", phase.getTickOffset()); - phaseObject.add("lifetime", phase.getLifetime()); - phaseObject.add("order", phase.getOrder()); - phase.saveExtra(phaseObject); - phases.add(phaseObject); + SimulatorConfig.Phase phaseConfig = new SimulatorConfig.Phase(); + phaseConfig.setTickOffset(phase.getTickOffset()); + phaseConfig.setLifetime(phase.getLifetime()); + phaseConfig.setOrder(phase.getOrder()); + phase.saveExtra(phaseConfig); + elementConfig.getPhases().add(phaseConfig); }); - elementObject.add("phases", phases); - elements.add(elementObject); + groupConfig.getElements().add(elementConfig); }); - groupObject.add("elements", elements); - groups.add(groupObject); + config.getGroups().add(groupConfig); }); - simulatorObject.add("groups", groups); File file = new File(directory, simulator.getName() + ".sim"); try { - simulatorObject.toJSONLossy(new FileOutput(file)).close(); + SimulatorConfig.STORE.save(file, config); } catch (IOException e) { // Ignore } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/YAPIONFormatSimulatorLoader.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/YAPIONFormatSimulatorLoader.java index 6111eb32..a3c773c0 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/YAPIONFormatSimulatorLoader.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/YAPIONFormatSimulatorLoader.java @@ -19,6 +19,7 @@ package de.steamwar.bausystem.features.simulator.storage; +import de.steamwar.bausystem.config.legacy.YapionReader; import de.steamwar.bausystem.features.simulator.data.Simulator; import de.steamwar.bausystem.features.simulator.data.SimulatorGroup; import de.steamwar.bausystem.features.simulator.data.tnt.TNTElement; @@ -26,18 +27,23 @@ import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase; import de.steamwar.sql.SteamwarUser; import org.bukkit.Material; import org.bukkit.util.Vector; -import yapion.exceptions.YAPIONException; -import yapion.hierarchy.types.YAPIONArray; -import yapion.hierarchy.types.YAPIONObject; -import yapion.parser.YAPIONParser; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; +import static de.steamwar.bausystem.config.legacy.YapionReader.*; + +/** + * Loads the legacy per player .yapion files containing multiple simulators, they are + * deleted after loading and saved in the current format by the {@link SimulatorSaver}. + */ +@Deprecated public class YAPIONFormatSimulatorLoader implements SimulatorLoader { @Override @@ -46,10 +52,10 @@ public class YAPIONFormatSimulatorLoader implements SimulatorLoader { return Optional.empty(); } - YAPIONObject yapionObject; + Map yapionObject; try { - yapionObject = YAPIONParser.parse(file); - } catch (YAPIONException | IOException e) { + yapionObject = YapionReader.parse(Files.readString(file.toPath(), StandardCharsets.UTF_8)); + } catch (IllegalArgumentException | IOException e) { return Optional.empty(); } @@ -57,28 +63,28 @@ public class YAPIONFormatSimulatorLoader implements SimulatorLoader { SteamwarUser steamwarUser = SteamwarUser.byId(Integer.parseInt(name)); List simulators = new ArrayList<>(); - for (String s : yapionObject.getKeys()) { + for (String s : yapionObject.keySet()) { String newName = steamwarUser.getUserName() + (s.isEmpty() ? "" : "_" + s); - YAPIONArray content = yapionObject.getArray(s); + List> content = getObjects(yapionObject, s); if (content.isEmpty()) continue; Simulator simulator = new Simulator(newName); - for (YAPIONObject element : content.streamObject().collect(Collectors.toList())) { + for (Map element : content) { SimulatorGroup simulatorGroup = new SimulatorGroup(); simulator.add(simulatorGroup); - TNTElement tntElement = new TNTElement(new Vector(element.getDouble("positionX"), element.getDouble("positionY"), element.getDouble("positionZ"))); - tntElement.setDisabled(element.getBooleanOrDefault("disabled", false)); // IDK if this existed back then? - tntElement.setMaterial(Material.valueOf(element.getStringOrDefault("material", "TNT"))); + TNTElement tntElement = new TNTElement(new Vector(requireDouble(element, "positionX"), requireDouble(element, "positionY"), requireDouble(element, "positionZ"))); + tntElement.setDisabled(getBoolean(element, "disabled", false)); // IDK if this existed back then? + tntElement.setMaterial(Material.valueOf(getString(element, "material", "TNT"))); simulatorGroup.add(tntElement); TNTPhase tntPhase = new TNTPhase(); - tntPhase.setLifetime(element.getIntOrDefault("fuseTicks", 80)); - tntPhase.setCount(element.getIntOrDefault("count", 1)); - tntPhase.setTickOffset(element.getIntOrDefault("tickOffset", 0)); - tntPhase.setXJump(element.getBooleanOrDefault("xVelocity", false)); - tntPhase.setYJump(element.getBooleanOrDefault("yVelocity", false)); - tntPhase.setZJump(element.getBooleanOrDefault("zVelocity", false)); - tntPhase.setOrder(element.getBooleanOrDefault("comparator", false) ? 2 : 0); + tntPhase.setLifetime(getInt(element, "fuseTicks", 80)); + tntPhase.setCount(getInt(element, "count", 1)); + tntPhase.setTickOffset(getInt(element, "tickOffset", 0)); + tntPhase.setXJump(getBoolean(element, "xVelocity", false)); + tntPhase.setYJump(getBoolean(element, "yVelocity", false)); + tntPhase.setZJump(getBoolean(element, "zVelocity", false)); + tntPhase.setOrder(getBoolean(element, "comparator", false) ? 2 : 0); tntElement.add(tntPhase); } simulators.add(simulator); @@ -87,4 +93,8 @@ public class YAPIONFormatSimulatorLoader implements SimulatorLoader { file.delete(); return Optional.of(simulators); } + + private double requireDouble(Map element, String key) { + return ((Number) element.get(key)).doubleValue(); + } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauConfig.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauConfig.java new file mode 100644 index 00000000..c422807c --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauConfig.java @@ -0,0 +1,64 @@ +/* + * 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.bausystem.features.slaves.laufbau; + +import de.steamwar.bausystem.config.ConfigStore; +import de.steamwar.bausystem.config.VersionedConfig; +import de.steamwar.bausystem.features.slaves.laufbau.converter.LaufbauConfigV1ToV2Converter; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashSet; +import java.util.Set; + +/** + * The current laufbau config of a player. It is stored as JSON, fields missing + * in the stored JSON keep the default value of their initializer, so adding a + * new field does not need a new version. + *

+ * When a field gets removed, renamed or its meaning changes, copy this class + * to LaufbauConfigV{@link #VERSION} (without the {@link #STORE}), increase {@link #VERSION}, write a + * {@link de.steamwar.bausystem.config.ConfigConverter} from the copy to + * this class and register it in the {@link #STORE}. + */ +@Getter +@Setter +public class LaufbauConfig implements VersionedConfig { + + public static final int VERSION = 2; + + /** + * LaufbauConfigV1 is stored as Base64 of gzipped YAPION, the current version as JSON + */ + public static final ConfigStore STORE = new ConfigStore<>(LaufbauConfig.class, VERSION, LaufbauConfig::new) + .legacy(s -> !s.startsWith("{"), LaufbauConfigV1::parse) + .converter(new LaufbauConfigV1ToV2Converter()) + .userConfig("bausystem-laufbau") + .removeIf(config -> config.getDeactivated().isEmpty()); + + @Setter(AccessLevel.NONE) + protected int version = VERSION; + + /** + * Identifiers of the deactivated {@link BlockBoundingBox}es, see {@link LaufbauUtils#identifier(BlockBoundingBox)} + */ + protected Set deactivated = new HashSet<>(); +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauConfigV1.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauConfigV1.java new file mode 100644 index 00000000..d372ed5d --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauConfigV1.java @@ -0,0 +1,65 @@ +/* + * 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.bausystem.features.slaves.laufbau; + +import de.steamwar.bausystem.config.VersionedConfig; +import de.steamwar.bausystem.config.legacy.YapionReader; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashSet; +import java.util.Set; +import java.util.zip.GZIPInputStream; + +/** + * The legacy laufbau config that was stored as Base64 of the gzipped YAPION, + * an object with the deactivated identifiers as keys. + */ +@Getter +@Setter +@Deprecated +public class LaufbauConfigV1 implements VersionedConfig { + + public static final int VERSION = 1; + + @Setter(AccessLevel.NONE) + protected int version = VERSION; + + protected Set deactivated = new HashSet<>(); + + public static LaufbauConfigV1 parse(String base64) { + String yapion; + try (InputStream inputStream = new GZIPInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(base64)))) { + yapion = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + LaufbauConfigV1 config = new LaufbauConfigV1(); + config.deactivated.addAll(YapionReader.parse(yapion).keySet()); + return config; + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauUtils.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauUtils.java index a642c22d..45b94482 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauUtils.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauUtils.java @@ -20,63 +20,22 @@ package de.steamwar.bausystem.features.slaves.laufbau; import de.steamwar.bausystem.BauSystem; -import de.steamwar.core.SWPlayer; import de.steamwar.inventory.SWItem; -import de.steamwar.sql.UserConfig; -import lombok.Cleanup; import lombok.SneakyThrows; import lombok.experimental.UtilityClass; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.meta.ItemMeta; -import yapion.hierarchy.output.StreamOutput; -import yapion.hierarchy.types.YAPIONObject; -import yapion.parser.YAPIONParser; -import yapion.parser.options.StreamOptions; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Arrays; -import java.util.Base64; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; @UtilityClass public class LaufbauUtils { - public static class LaufbauComponent implements SWPlayer.Component { - - private YAPIONObject yapionObject = new YAPIONObject(); - - @Override - @SneakyThrows - public void onMount(SWPlayer player) { - String config = UserConfig.getConfig(player.getPlayer().getUniqueId(), "bausystem-laufbau"); - if (config == null) { - return; - } - byte[] bytes = Base64.getDecoder().decode(config); - @Cleanup GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(bytes)); - yapionObject = new YAPIONParser(gzipInputStream, new StreamOptions().stopOnStreamEnd(true)).parse().result(); - } - - @Override - @SneakyThrows - public void onUnmount(SWPlayer player) { - if (yapionObject.isEmpty()) { - UserConfig.removePlayerConfig(player.getPlayer().getUniqueId(), "bausystem-laufbau"); - return; - } - @Cleanup ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - @Cleanup GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream); - yapionObject.toYAPION(new StreamOutput(gzipOutputStream)).close(); - UserConfig.updatePlayerConfig(player.getPlayer().getUniqueId(), "bausystem-laufbau", Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray())); - } - } - public static Cuboid pixelCuboid(double pixelX, double pixelY, double pixelZ, double pixelDX, double pixelDY, double pixelDZ) { return new Cuboid(pixelToBlock(pixelX), pixelToBlock(pixelY), pixelToBlock(pixelZ), pixelToBlock(pixelDX), pixelToBlock(pixelDY), pixelToBlock(pixelDZ)); } @@ -123,7 +82,7 @@ public class LaufbauUtils { return false; } String identifier = identifier(bb); - return SWPlayer.of(p).getComponentOrDefault(LaufbauComponent.class, LaufbauComponent::new).yapionObject.containsKey(identifier); + return LaufbauConfig.STORE.get(p).getDeactivated().contains(identifier); } @SneakyThrows @@ -132,11 +91,9 @@ public class LaufbauUtils { return; } String identifier = identifier(bb); - YAPIONObject yapionObject = SWPlayer.of(p).getComponentOrDefault(LaufbauComponent.class, LaufbauComponent::new).yapionObject; - if (yapionObject.containsKey(identifier)) { - yapionObject.remove(identifier); - } else { - yapionObject.put(identifier, new YAPIONObject()); + Set deactivated = LaufbauConfig.STORE.get(p).getDeactivated(); + if (!deactivated.remove(identifier)) { + deactivated.add(identifier); } } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/converter/LaufbauConfigV1ToV2Converter.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/converter/LaufbauConfigV1ToV2Converter.java new file mode 100644 index 00000000..6e1c7aac --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/converter/LaufbauConfigV1ToV2Converter.java @@ -0,0 +1,40 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bausystem.features.slaves.laufbau.converter; + +import de.steamwar.bausystem.config.ConfigConverter; +import de.steamwar.bausystem.features.slaves.laufbau.LaufbauConfig; +import de.steamwar.bausystem.features.slaves.laufbau.LaufbauConfigV1; + +@Deprecated +public class LaufbauConfigV1ToV2Converter implements ConfigConverter { + + @Override + public int version() { + return LaufbauConfigV1.VERSION; + } + + @Override + public LaufbauConfig convert(LaufbauConfigV1 old) { + LaufbauConfig config = new LaufbauConfig(); + config.getDeactivated().addAll(old.getDeactivated()); + return config; + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceCommand.java index 4eac9b8f..b9244573 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceCommand.java @@ -35,8 +35,8 @@ public class SmartPlaceCommand extends SWCommand { @Register(description = {"SMART_PLACE_HELP", "SMART_PLACE_INFO"}) public void genericToggle(Player p) { - boolean smartPlace = Config.getInstance().get(p).getPlainValueOrDefault("smartPlace", false); - Config.getInstance().get(p).put("smartPlace", !smartPlace); + boolean smartPlace = Config.STORE.get(p).isSmartPlace(); + Config.STORE.get(p).setSmartPlace(!smartPlace); if (!smartPlace) { SWUtils.sendToActionbar(p, BauSystem.MESSAGE.parse("SMART_PLACE_ENABLE", p)); BauSystem.MESSAGE.send("SMART_PLACE_INFO", p); 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 6f70fc55..f403e265 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 @@ -87,7 +87,7 @@ public class SmartPlaceListener implements Listener { public SmartPlaceListener() { TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, (player, packet) -> { if (!Permission.BUILD.hasPermission(player)) return packet; - if (!Config.getInstance().get(player).getPlainValueOrDefault("smartPlace", false)) return packet; + if (!Config.STORE.get(player).isSmartPlace()) return packet; RayTraceResult rayTraceResult = player.rayTraceBlocks(6); Block block = rayTraceResult != null ? rayTraceResult.getHitBlock() : null; BlockFace blockFace = rayTraceResult != null ? rayTraceResult.getHitBlockFace() : null; @@ -127,7 +127,7 @@ public class SmartPlaceListener implements Listener { @EventHandler public void onPlayerInteract(PlayerInteractEvent event) { if (!Permission.BUILD.hasPermission(event.getPlayer())) return; - if (!Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("smartPlace", false)) return; + if (!Config.STORE.get(event.getPlayer()).isSmartPlace()) return; WAS_EXECUTED.add(event.getPlayer()); if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return; if (event.getPlayer().getGameMode() == GameMode.SPECTATOR) return; @@ -146,7 +146,7 @@ public class SmartPlaceListener implements Listener { @EventHandler public void onBlockPlace(BlockPlaceEvent event) { if (!Permission.BUILD.hasPermission(event.getPlayer())) return; - if (!Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("smartPlace", false)) return; + if (!Config.STORE.get(event.getPlayer()).isSmartPlace()) return; if (!SMART_PLACING.contains(event.getPlayer())) { if (CONTAINERS.contains(event.getBlockAgainst().getType())) { SoundGroup soundGroup = event.getBlockPlaced().getBlockData().getSoundGroup(); diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/testblock/blockcounter/BlockCounter.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/testblock/blockcounter/BlockCounter.java index 9e3e53e0..2ca16694 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/testblock/blockcounter/BlockCounter.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/testblock/blockcounter/BlockCounter.java @@ -35,18 +35,15 @@ public class BlockCounter { public final Map blockCountMap = new HashMap<>(); public boolean isActive(final Player p) { - if (!Config.getInstance().get(p).containsKey("blockCounter")) { - return false; - } - return Config.getInstance().get(p).getPlainValue("blockCounter"); + return Config.STORE.get(p).isBlockCounter(); } public void enable(final Player p) { - Config.getInstance().get(p).put("blockCounter", true); + Config.STORE.get(p).setBlockCounter(true); } public void disable(final Player p) { - Config.getInstance().get(p).put("blockCounter", false); + Config.STORE.get(p).setBlockCounter(false); } public String getMessage(Player player, int count, int tntCount, long tick, long lastTick) { diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/tracer/rendering/TraceEntity.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/tracer/rendering/TraceEntity.java index 160e4d58..2ec576f6 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/tracer/rendering/TraceEntity.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/tracer/rendering/TraceEntity.java @@ -36,13 +36,10 @@ import org.bukkit.entity.Player; import org.bukkit.util.Transformation; import org.joml.Quaternionf; import org.joml.Vector3f; -import yapion.hierarchy.types.YAPIONValue; import java.util.List; import java.util.stream.Collectors; -import static de.steamwar.bausystem.features.util.TNTClickListener.TNT_CLICK_DETAILS; - /** * Wrapper for the rendering of a record bundle */ @@ -102,7 +99,7 @@ public class TraceEntity extends RBlockDisplay { * @param player the player the message should be printed for */ public void printIntoChat(Player player) { - if (!Config.getInstance().get(player).getOrSetDefault(TNT_CLICK_DETAILS, new YAPIONValue<>(true)).asBoolean().orElse(true)) { + if (!Config.STORE.get(player).isTntClickDetails()) { return; } TNTPoint representative = records.get(0); diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NightVisionCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NightVisionCommand.java index 2f884047..d43502ad 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NightVisionCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NightVisionCommand.java @@ -30,7 +30,6 @@ import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; -import yapion.hierarchy.types.YAPIONObject; @Linked public class NightVisionCommand extends SWCommand implements Listener { @@ -41,15 +40,15 @@ public class NightVisionCommand extends SWCommand implements Listener { @Register(description = "NIGHT_VISION_HELP") public void genericCommand(Player p) { - YAPIONObject yapionObject = Config.getInstance().get(p); - boolean value = !yapionObject.getBooleanOrDefault("nightvision", false); - yapionObject.put("nightvision", value); + Config config = Config.STORE.get(p); + boolean value = !config.isNightVision(); + config.setNightVision(value); setNightVision(p, value); } @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { - setNightVision(event.getPlayer(), Config.getInstance().get(event.getPlayer()).getBooleanOrDefault("nightvision", false)); + setNightVision(event.getPlayer(), Config.STORE.get(event.getPlayer()).isNightVision()); } private void setNightVision(Player p, boolean value) { diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/TNTClickListener.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/TNTClickListener.java index ca17f6a5..8af264a1 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/TNTClickListener.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/TNTClickListener.java @@ -32,26 +32,22 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.inventory.EquipmentSlot; -import yapion.hierarchy.types.YAPIONObject; -import yapion.hierarchy.types.YAPIONValue; @Linked public class TNTClickListener extends SWCommand implements Listener { - public static final String TNT_CLICK_DETAILS = "tnt_click_details"; - public TNTClickListener() { super("tntdetails"); } @Register(description = "TNT_DETAILS_COMMAND") public void toggle(Player player) { - YAPIONObject yapionObject = Config.getInstance().get(player); - if (yapionObject.getOrSetDefault(TNT_CLICK_DETAILS, new YAPIONValue<>(true)).asBoolean().orElse(true)) { - yapionObject.put(TNT_CLICK_DETAILS, false); + Config config = Config.STORE.get(player); + if (config.isTntClickDetails()) { + config.setTntClickDetails(false); BauSystem.MESSAGE.send("TNT_DETAILS_OFF", player); } else { - yapionObject.put(TNT_CLICK_DETAILS, true); + config.setTntClickDetails(true); BauSystem.MESSAGE.send("TNT_DETAILS_ON", player); } } @@ -60,7 +56,7 @@ public class TNTClickListener extends SWCommand implements Listener { public void onPlayerInteractEntity(PlayerInteractEntityEvent event) { if (!Permission.BUILD.hasPermission(event.getPlayer())) return; if (event.getHand() != EquipmentSlot.HAND) return; - if (!Config.getInstance().get(event.getPlayer()).getOrSetDefault(TNT_CLICK_DETAILS, new YAPIONValue<>(true)).asBoolean().orElse(true)) { + if (!Config.STORE.get(event.getPlayer()).isTntClickDetails()) { return; } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/warp/Warp.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/warp/Warp.java index 696dac81..f2110927 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/warp/Warp.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/warp/Warp.java @@ -25,7 +25,6 @@ import lombok.Getter; import org.bukkit.*; import org.bukkit.entity.Player; import org.bukkit.event.player.PlayerTeleportEvent; -import yapion.hierarchy.types.YAPIONObject; import java.util.*; @@ -56,27 +55,22 @@ public class Warp { warpMap.put(name, this); } - public Warp(String name, YAPIONObject object) { + public Warp(String name, WorldData.WarpEntry warp) { this.name = name; - double x = object.getPlainValue("x"); - double y = object.getPlainValue("y"); - double z = object.getPlainValue("z"); - float yaw = object.getPlainValue("yaw"); - float pitch = object.getPlainValue("pitch"); - location = new Location(Bukkit.getWorlds().get(0), x, y, z, yaw, pitch); - mat = Material.getMaterial(object.getPlainValue("material")); + location = new Location(Bukkit.getWorlds().get(0), warp.getX(), warp.getY(), warp.getZ(), warp.getYaw(), warp.getPitch()); + mat = Material.getMaterial(warp.getMaterial()); warpMap.put(name, this); } - public YAPIONObject output() { - YAPIONObject yapionObject = new YAPIONObject(); - yapionObject.add("x", location.getX()); - yapionObject.add("y", location.getY()); - yapionObject.add("z", location.getZ()); - yapionObject.add("yaw", location.getYaw()); - yapionObject.add("pitch", location.getPitch()); - yapionObject.add("material", mat.toString()); - return yapionObject; + public WorldData.WarpEntry output() { + WorldData.WarpEntry warp = new WorldData.WarpEntry(); + warp.setX(location.getX()); + warp.setY(location.getY()); + warp.setZ(location.getZ()); + warp.setYaw(location.getYaw()); + warp.setPitch(location.getPitch()); + warp.setMaterial(mat.toString()); + return warp; } public static Set getWarpNames() { @@ -101,7 +95,7 @@ public class Warp { public void delete() { warpMap.remove(name); - WorldData.getWarpData().remove(name); + WorldData.STORE.get().getWarps().remove(name); } public void teleport(Player player) { diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/warp/WarpCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/warp/WarpCommand.java index e20a70b7..0c071276 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/warp/WarpCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/warp/WarpCommand.java @@ -34,9 +34,9 @@ import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.HoverEvent; import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.entity.Player; -import yapion.hierarchy.types.YAPIONObject; import java.util.ArrayList; +import java.util.Map; @Linked public class WarpCommand extends SWCommand implements Disable, Enable { @@ -135,17 +135,15 @@ public class WarpCommand extends SWCommand implements Disable, Enable { @Override public void enable() { - WorldData.getWarpData().forEach((name, yapionAnyType) -> { - new Warp(name, (YAPIONObject) yapionAnyType); - }); + WorldData.STORE.get().getWarps().forEach(Warp::new); Warp.enable(); } @Override public void disable() { - YAPIONObject yapionObject = WorldData.getWarpData(); + Map warps = WorldData.STORE.get().getWarps(); for (Warp warp : Warp.getWarps()) { - yapionObject.add(warp.getName(), warp.output()); + warps.put(warp.getName(), warp.output()); } } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/region/RegionData.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/region/RegionData.java index a1d94ed4..715dc1d6 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/region/RegionData.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/region/RegionData.java @@ -20,78 +20,39 @@ package de.steamwar.bausystem.region; import de.steamwar.bausystem.region.flags.Flag; +import de.steamwar.bausystem.worlddata.WorldData; import de.steamwar.sql.SchematicNode; import lombok.NonNull; -import yapion.hierarchy.types.YAPIONObject; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.function.Function; public abstract class RegionData { - private final List> properties = new ArrayList<>(); - - protected final YAPIONObject data; - protected final YAPIONObject flagData; + protected final WorldData.RegionEntry data; + protected final Map flagData; protected final Runnable onChange; protected final Map, Flag.Value> flagMap = new HashMap<>(); - private final class Property { - private final String field; - private final Function loader; - private final Function writer; + private SchematicNode testblockSchematic; - private T value; - - public Property(String field, Function loader, Function writer) { - this.field = field; - this.loader = loader; - this.writer = writer; - properties.add(this); - } - - public void load() { - if (flagData.containsKey(field)) { - value = loader.apply(flagData.getPlainValue(field)); - } else { - value = null; - } - } - - public T get() { - return value; - } - - public void set(T value) { - this.value = value; - if (value == null) { - flagData.remove(field); - } else { - flagData.put(field, writer.apply(value)); - } - } - } - - private Property testblockSchematic = new Property<>("testblockSchematic", SchematicNode::byId, SchematicNode::getId); - - protected RegionData(YAPIONObject data, Runnable onChange) { + protected RegionData(WorldData.RegionEntry data, Runnable onChange) { this.data = data; - this.flagData = data.getObjectOrSetDefault("flagStorage", new YAPIONObject()); + this.flagData = data.getFlags(); this.onChange = onChange; initialize(); for (final Flag flag : Flag.getFlags()) { if (!has(flag).isWritable()) continue; try { - String s = flagData.getPlainValue(flag.name()); + String s = flagData.get(flag.name()); flagMap.put(flag, flag.valueOfValue(s)); } catch (Exception e) { flagMap.put(flag, (Flag.Value) flag.getDefaultValue()); } } - properties.forEach(Property::load); + if (data.getTestblockSchematic() != null) { + testblockSchematic = SchematicNode.byId(data.getTestblockSchematic()); + } } protected void initialize() { @@ -126,7 +87,8 @@ public abstract class RegionData { flagData.remove(flag.name()); } } - properties.forEach(property -> property.set(null)); + testblockSchematic = null; + data.setTestblockSchematic(null); onChange.run(); } @@ -135,11 +97,12 @@ public abstract class RegionData { } public SchematicNode getTestblockSchematic() { - return testblockSchematic.get(); + return testblockSchematic; } public void setTestblockSchematic(SchematicNode schematic) { - testblockSchematic.set(schematic); + testblockSchematic = schematic; + data.setTestblockSchematic(schematic == null ? null : schematic.getId()); onChange.run(); } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/SimulatorData.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/SimulatorData.java deleted file mode 100644 index 2c1a3573..00000000 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/SimulatorData.java +++ /dev/null @@ -1,101 +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.bausystem.worlddata; - -import de.steamwar.sql.SteamwarUser; -import lombok.SneakyThrows; -import lombok.experimental.UtilityClass; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import yapion.hierarchy.output.FileOutput; -import yapion.hierarchy.types.YAPIONArray; -import yapion.hierarchy.types.YAPIONObject; -import yapion.parser.InputStreamCharsets; -import yapion.parser.YAPIONParser; -import yapion.parser.options.FileOptions; - -import java.io.File; -import java.util.List; - -@UtilityClass -public class SimulatorData { - - private final File simulatorsDir = new File(Bukkit.getWorlds().get(0).getWorldFolder(), "simulators"); - - static { - simulatorsDir.mkdirs(); - } - - @SneakyThrows - public YAPIONObject getSimulator(Player player) { - File file = getFile(player); - if (!file.exists()) { - return new YAPIONObject(); - } - return YAPIONParser.parse(file, new FileOptions().charset(InputStreamCharsets.UTF_8)); - } - - @SneakyThrows - private void internalSaveSimulator(Player player, YAPIONObject yapionObject) { - File file = getFile(player); - yapionObject.toYAPION(new FileOutput(file)).close(); - } - - @SneakyThrows - void saveSimulator(SteamwarUser steamwarUser, YAPIONObject yapionObject) { - File file = new File(simulatorsDir, steamwarUser.getId() + ".yapion"); - yapionObject.toYAPION(new FileOutput(file)).close(); - } - - public List listSimulator(Player player) { - List strings = getSimulator(player).getKeys(); - strings.remove(""); - return strings; - } - - public void saveSimulator(Player player, String name) { - YAPIONObject yapionObject = getSimulator(player); - yapionObject.put(name, yapionObject.getArray("").copy()); - internalSaveSimulator(player, yapionObject); - } - - public void saveSimulator(Player player, YAPIONArray simulator) { - YAPIONObject yapionObject = getSimulator(player); - yapionObject.put("", simulator); - internalSaveSimulator(player, yapionObject); - } - - public void loadSimulator(Player player, String name) { - YAPIONObject yapionObject = getSimulator(player); - yapionObject.put("", yapionObject.getArray(name).copy()); - internalSaveSimulator(player, yapionObject); - } - - public void removeSimulator(Player player, String name) { - YAPIONObject yapionObject = getSimulator(player); - yapionObject.remove(name); - internalSaveSimulator(player, yapionObject); - } - - private File getFile(Player player) { - SteamwarUser steamwarUser = SteamwarUser.get(player.getUniqueId()); - return new File(simulatorsDir, steamwarUser.getId() + ".yapion"); - } -} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/WorldData.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/WorldData.java index bd661172..d47f097a 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/WorldData.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/WorldData.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 @@ -19,58 +19,83 @@ package de.steamwar.bausystem.worlddata; -import lombok.experimental.UtilityClass; +import com.google.gson.GsonBuilder; +import de.steamwar.bausystem.config.ConfigStore; +import de.steamwar.bausystem.config.VersionedConfig; +import de.steamwar.bausystem.worlddata.converter.WorldDataV1ToV2Converter; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; import org.bukkit.Bukkit; -import yapion.hierarchy.output.FileOutput; -import yapion.hierarchy.types.YAPIONObject; -import yapion.parser.InputStreamCharsets; -import yapion.parser.YAPIONParser; -import yapion.parser.options.StreamOptions; -import java.io.BufferedInputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; +import java.util.HashMap; +import java.util.Map; -@UtilityClass -public class WorldData { +/** + * The current world data. It is stored as JSON, fields missing in the + * stored JSON keep the default value of their initializer, so adding a new + * field does not need a new version. + *

+ * When a field gets removed, renamed or its meaning changes, copy this class + * to WorldDataV{@link #VERSION} (without the {@link #STORE}), increase {@link #VERSION}, write a + * {@link de.steamwar.bausystem.config.ConfigConverter} from the copy to this class and register it in the {@link #STORE}. + */ +@Getter +@Setter +public class WorldData implements VersionedConfig { - private final File optionsFile = new File(Bukkit.getWorlds().get(0).getWorldFolder(), "options.yapion"); - private YAPIONObject worldData; + public static final int VERSION = 2; - public YAPIONObject getWorldData() { - if (worldData == null) { - read(); - } - return worldData; + public static final ConfigStore STORE = new ConfigStore<>(WorldData.class, VERSION, WorldData::new) + .gson(new GsonBuilder().setPrettyPrinting().create()) + .converter(new WorldDataV1ToV2Converter()) + .file(new File(Bukkit.getWorlds().get(0).getWorldFolder(), "options.json")) + .legacyFile(new File(Bukkit.getWorlds().get(0).getWorldFolder(), "options.yapion"), WorldDataV1::parse); + + @Setter(AccessLevel.NONE) + protected int version = VERSION; + + /** + * Region name to the data of the region + */ + protected Map regions = new HashMap<>(); + protected RegionEntry globalRegion = new RegionEntry(); + + /** + * Warp name to warp + */ + protected Map warps = new HashMap<>(); + + @Getter + @Setter + public static class RegionEntry { + + /** + * Overrides the prototype of the region config, null if not overridden + */ + protected String prototype = null; + + /** + * Flag name to flag value name + */ + protected Map flags = new HashMap<>(); + + /** + * Id of the testblock SchematicNode, null if the default testblock is used + */ + protected Integer testblockSchematic = null; } - public YAPIONObject getRegionsData() { - return getWorldData().getYAPIONObjectOrSetDefault("regions", new YAPIONObject()); - } + @Getter + @Setter + public static class WarpEntry { - public YAPIONObject getWarpData() { - return getWorldData().getYAPIONObjectOrSetDefault("warps", new YAPIONObject()); - } - - private void read() { - worldData = new YAPIONObject(); - if (optionsFile.length() != 0) { - try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(optionsFile))) { - worldData = YAPIONParser.parse(bufferedInputStream, new StreamOptions().charset(InputStreamCharsets.UTF_8)); - } catch (IOException e) { - // Ignored - } - } else { - write(); - } - } - - public void write() { - try { - worldData.toYAPION(new FileOutput(optionsFile)).close(); - } catch (IOException e) { - e.printStackTrace(); - } + protected double x; + protected double y; + protected double z; + protected float yaw; + protected float pitch; + protected String material; } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/WorldDataV1.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/WorldDataV1.java new file mode 100644 index 00000000..b7549246 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/WorldDataV1.java @@ -0,0 +1,111 @@ +/* + * 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.bausystem.worlddata; + +import de.steamwar.bausystem.config.VersionedConfig; +import de.steamwar.bausystem.config.legacy.YapionReader; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.Map; + +/** + * The legacy world data that was stored in the YAPION format in options.yapion. + */ +@Getter +@Setter +@Deprecated +public class WorldDataV1 implements VersionedConfig { + + public static final int VERSION = 1; + + @Setter(AccessLevel.NONE) + protected int version = VERSION; + + /** + * Region name to the data of the region, the global region is stored with the name "global" + */ + protected Map regions = new HashMap<>(); + + /** + * Warp name to warp + */ + protected Map warps = new HashMap<>(); + + @Getter + @Setter + public static class RegionEntry { + + protected String prototype = null; + + /** + * Flag name to flag value name and "testblockSchematic" to the SchematicNode id + */ + protected Map flagStorage = new HashMap<>(); + } + + @Getter + @Setter + public static class WarpEntry { + + protected double x; + protected double y; + protected double z; + protected float yaw; + protected float pitch; + protected String material; + } + + public static WorldDataV1 parse(String yapion) { + Map object = YapionReader.parse(yapion); + WorldDataV1 worldData = new WorldDataV1(); + + if (object.get("regions") instanceof Map regions) { + regions.forEach((name, value) -> { + if (!(value instanceof Map region)) return; + RegionEntry entry = new RegionEntry(); + if (region.get("prototype") instanceof String prototype) { + entry.prototype = prototype; + } + if (region.get("flagStorage") instanceof Map flagStorage) { + flagStorage.forEach((key, flag) -> entry.flagStorage.put((String) key, flag)); + } + worldData.regions.put((String) name, entry); + }); + } + + if (object.get("warps") instanceof Map warps) { + warps.forEach((name, value) -> { + if (!(value instanceof Map warp)) return; + WarpEntry entry = new WarpEntry(); + entry.x = ((Number) warp.get("x")).doubleValue(); + entry.y = ((Number) warp.get("y")).doubleValue(); + entry.z = ((Number) warp.get("z")).doubleValue(); + entry.yaw = ((Number) warp.get("yaw")).floatValue(); + entry.pitch = ((Number) warp.get("pitch")).floatValue(); + entry.material = (String) warp.get("material"); + worldData.warps.put((String) name, entry); + }); + } + return worldData; + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/converter/WorldDataV1ToV2Converter.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/converter/WorldDataV1ToV2Converter.java new file mode 100644 index 00000000..31bd3f85 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/converter/WorldDataV1ToV2Converter.java @@ -0,0 +1,75 @@ +/* + * 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.bausystem.worlddata.converter; + +import de.steamwar.bausystem.config.ConfigConverter; +import de.steamwar.bausystem.worlddata.WorldData; +import de.steamwar.bausystem.worlddata.WorldDataV1; + +@Deprecated +public class WorldDataV1ToV2Converter implements ConfigConverter { + + private static final String GLOBAL_REGION = "global"; + private static final String TESTBLOCK_SCHEMATIC = "testblockSchematic"; + + @Override + public int version() { + return WorldDataV1.VERSION; + } + + @Override + public WorldData convert(WorldDataV1 old) { + WorldData worldData = new WorldData(); + old.getRegions().forEach((name, region) -> { + if (name.equals(GLOBAL_REGION)) { + worldData.setGlobalRegion(convert(region)); + } else { + worldData.getRegions().put(name, convert(region)); + } + }); + old.getWarps().forEach((name, warp) -> worldData.getWarps().put(name, convert(warp))); + return worldData; + } + + private WorldData.RegionEntry convert(WorldDataV1.RegionEntry old) { + WorldData.RegionEntry region = new WorldData.RegionEntry(); + region.setPrototype(old.getPrototype()); + old.getFlagStorage().forEach((key, value) -> { + if (value == null) return; + if (key.equals(TESTBLOCK_SCHEMATIC)) { + region.setTestblockSchematic(((Number) value).intValue()); + } else { + region.getFlags().put(key, value.toString()); + } + }); + return region; + } + + private WorldData.WarpEntry convert(WorldDataV1.WarpEntry old) { + WorldData.WarpEntry warp = new WorldData.WarpEntry(); + warp.setX(old.getX()); + warp.setY(old.getY()); + warp.setZ(old.getZ()); + warp.setYaw(old.getYaw()); + warp.setPitch(old.getPitch()); + warp.setMaterial(old.getMaterial()); + return warp; + } +} diff --git a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedGlobalRegionData.java b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedGlobalRegionData.java index 065e2e20..5412adf7 100644 --- a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedGlobalRegionData.java +++ b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedGlobalRegionData.java @@ -25,12 +25,12 @@ import de.steamwar.bausystem.region.flags.ColorMode; import de.steamwar.bausystem.region.flags.Flag; import de.steamwar.bausystem.region.flags.ProtectMode; import de.steamwar.bausystem.region.flags.TNTMode; +import de.steamwar.bausystem.worlddata.WorldData; import lombok.NonNull; -import yapion.hierarchy.types.YAPIONObject; public class FixedGlobalRegionData extends RegionData { - public FixedGlobalRegionData(YAPIONObject data, Runnable onChange) { + public FixedGlobalRegionData(WorldData.RegionEntry data, Runnable onChange) { super(data, onChange); } diff --git a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegion.java b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegion.java index 4a87922d..913e1a1f 100644 --- a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegion.java +++ b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegion.java @@ -26,6 +26,7 @@ import de.steamwar.bausystem.region.flags.Flag; import de.steamwar.bausystem.region.flags.TestblockMode; import de.steamwar.bausystem.utils.FlatteningWrapper; import de.steamwar.bausystem.utils.PasteBuilder; +import de.steamwar.bausystem.worlddata.WorldData; import de.steamwar.core.Core; import de.steamwar.sql.GameModeConfig; import de.steamwar.sql.SchematicType; @@ -139,7 +140,7 @@ public class FixedRegion implements Region { } } - public FixedRegion(String name, FixedRegionData flagStorage, Prototype prototype, YAPIONObject regionConfig, YAPIONObject regionData) { + public FixedRegion(String name, FixedRegionData flagStorage, Prototype prototype, YAPIONObject regionConfig, WorldData.RegionEntry regionData) { this.name = name; uuid = UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8)); this.flagStorage = flagStorage; diff --git a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegionData.java b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegionData.java index c2de7042..ffe7c327 100644 --- a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegionData.java +++ b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegionData.java @@ -22,12 +22,12 @@ package de.steamwar.bausystem.region.fixed; import de.steamwar.bausystem.region.RegionData; import de.steamwar.bausystem.region.RegionFlagPolicy; import de.steamwar.bausystem.region.flags.Flag; +import de.steamwar.bausystem.worlddata.WorldData; import lombok.NonNull; -import yapion.hierarchy.types.YAPIONObject; public class FixedRegionData extends RegionData { - public FixedRegionData(YAPIONObject data, Runnable onChange) { + public FixedRegionData(WorldData.RegionEntry data, Runnable onChange) { super(data, onChange); } diff --git a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/Prototype.java b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/Prototype.java index 04cf7ee0..0fe5f2b5 100644 --- a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/Prototype.java +++ b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/Prototype.java @@ -210,14 +210,14 @@ public class Prototype { } } - public static void generateRegion(String name, YAPIONObject regionConfig, YAPIONObject regionData) { + public static void generateRegion(String name, YAPIONObject regionConfig, WorldData.RegionEntry regionData) { Prototype prototype; - if (regionData.containsKey("prototype", String.class)) { - prototype = PROTOTYPE_MAP.get(regionData.getPlainValue("prototype")); + if (regionData.getPrototype() != null) { + prototype = PROTOTYPE_MAP.get(regionData.getPrototype()); } else { prototype = PROTOTYPE_MAP.get(regionConfig.getPlainValue("prototype")); } - FixedRegionData flagStorage = new FixedRegionData(regionData, WorldData::write); + FixedRegionData flagStorage = new FixedRegionData(regionData, WorldData.STORE::save); FixedRegionSystem.addRegion(new FixedRegion(name, flagStorage, prototype, regionConfig, regionData)); } } diff --git a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/loader/RegionLoader.java b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/loader/RegionLoader.java index 3bfe538d..d19699e4 100644 --- a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/loader/RegionLoader.java +++ b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/loader/RegionLoader.java @@ -28,7 +28,6 @@ import org.bukkit.Bukkit; import yapion.hierarchy.diff.DiffChange; import yapion.hierarchy.diff.YAPIONDiff; import yapion.hierarchy.types.YAPIONObject; -import yapion.hierarchy.types.YAPIONType; import yapion.parser.YAPIONParser; import java.io.BufferedInputStream; @@ -55,7 +54,7 @@ public class RegionLoader { } loaded = yapionObject; - YAPIONObject optionsYapionObject = WorldData.getRegionsData(); + WorldData worldData = WorldData.STORE.get(); yapionObject.forEach((key, yapionAnyType) -> { if (key.equals("global")) { return; @@ -65,21 +64,10 @@ public class RegionLoader { } YAPIONObject regionConfig = (YAPIONObject) yapionAnyType; - YAPIONObject regionData = new YAPIONObject(); - if (optionsYapionObject.containsKey(key, YAPIONType.OBJECT)) { - regionData = optionsYapionObject.getObject(key); - } else { - optionsYapionObject.add(key, regionData); - } - + WorldData.RegionEntry regionData = worldData.getRegions().computeIfAbsent(key, k -> new WorldData.RegionEntry()); Prototype.generateRegion(key, regionConfig, regionData); }); - YAPIONObject globalOptions = optionsYapionObject.getObject("global"); - if (globalOptions == null) { - globalOptions = new YAPIONObject(); - optionsYapionObject.add("global", globalOptions); - } - FixedGlobalRegion.setFLAG_STORAGE(new FixedGlobalRegionData(globalOptions, WorldData::write)); + FixedGlobalRegion.setFLAG_STORAGE(new FixedGlobalRegionData(worldData.getGlobalRegion(), WorldData.STORE::save)); } } From d727982ce0b198a3d2efe67fe6e02b6a0053dd18 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Thu, 15 Oct 2026 17:53:06 +0200 Subject: [PATCH 15/32] Fix AutoChecker inventory rules Count every inventory block towards exactly one Schematic.Limited rule: filled blocks towards a rule whose Content allows all their items, empty blocks towards a rule without Content (unlimited if there is none). Each rule's Amount now caps how many blocks it covers, instead of taking the highest Amount of all rules for that block type. Inventory blocks whose items fit no rule are now checked and reported, items from different rules in one block are reported as mixed, and the auto-fix trims or removes them accordingly. Remove the hardcoded wargearseason26 wind charge handling and its 2048 limit, wind charges are configured via Limited rules now. Log a warning for Limited rules that match no blocks, e.g. a misspelled 'Materials' key. Fixes #458, #481, #502 --- .../src/de/steamwar/sql/GameModeConfig.java | 22 ++-- .../SQL/src/de/steamwar/sql/SQLWrapper.java | 2 +- .../src/SchematicSystem.properties | 3 +- .../src/SchematicSystem_de.properties | 3 +- .../autocheck/AutoChecker.java | 104 +++++++++++++----- .../autocheck/AutoCheckerResult.java | 56 ++++------ .../schematiccommand/SchematicCommand.java | 16 ++- 7 files changed, 123 insertions(+), 83 deletions(-) diff --git a/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java b/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java index 17bfccb5..2292ed86 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java +++ b/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java @@ -19,6 +19,7 @@ package de.steamwar.sql; +import de.steamwar.sql.internal.SQLConfig; import lombok.ToString; import java.io.File; @@ -250,6 +251,11 @@ public final class GameModeConfig { Times = new TimesConfig(loader.with("Times")); // Arena would be here to be in config order but needs Schematic.Size and EnterStages loaded afterwards Schematic = new SchematicConfig<>(loader.with("Schematic")); + for (int i = 0; i < Schematic.Limited.size(); i++) { + if (Schematic.Limited.get(i).Materials.isEmpty()) { + SQLConfig.impl.getLogger().warning(file.getName() + ": Schematic.Limited[" + i + "] matches no blocks, check its 'Materials' entry"); + } + } GameName = loader.getString("GameName", defaultGameName.apply(file)); ActiveMonths = loader.getIntList("ActiveMonths"); TeamChatPrefix = loader.getString("TeamChatPrefix", "+"); @@ -703,7 +709,10 @@ public final class GameModeConfig { public final double MaxDesignBlastResistance; /** - * List of limited block materials + * List of limited block materials
+ * Every inventory block is counted towards exactly one rule: a filled block towards a rule whose + * {@link BlockRule#Content} allows all of its items, an empty block towards a rule without + * {@link BlockRule#Content}. Empty inventory blocks without such a rule are unlimited. * * @implSpec {@link SQLWrapper#getInventoryMaterials)} are all disallowed by default to have any content but are unlimited in number. * @implSpec Any Block that has more Blast Resistance than {@link #MaxBlastResistance} will be defaulted to 0. @@ -759,18 +768,13 @@ public final class GameModeConfig { } public boolean isInventory(M material) { - return Limited.stream() - .filter(rule -> rule.Materials.contains(material)) - .anyMatch(rule -> rule.Content.loaded); + return SQLWrapper.impl.getInventoryMaterials().contains(material) || Limited.stream().anyMatch(rule -> rule.Content.loaded && rule.Materials.contains(material)); } - public Integer getItemAmount(M material, M item) { + public List> getRules(M material) { return Limited.stream() .filter(rule -> rule.Materials.contains(material)) - .filter(rule -> rule.Content.Items.contains(item)) - .mapToInt(rule -> rule.Content.Amount) - .max() - .orElse(0); + .collect(Collectors.toList()); } @ToString diff --git a/CommonCore/SQL/src/de/steamwar/sql/SQLWrapper.java b/CommonCore/SQL/src/de/steamwar/sql/SQLWrapper.java index 565687fe..47a6f0a6 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/SQLWrapper.java +++ b/CommonCore/SQL/src/de/steamwar/sql/SQLWrapper.java @@ -46,7 +46,7 @@ public interface SQLWrapper { } /** - * Every inventory material - the {@code all} group usable in {@code GameModeConfig}'s + * Every inventory material - the {@code inventory} group usable in {@code GameModeConfig}'s * {@code Limited}. Expected to be a superset of {@link #getStorageMaterials()}. */ default Set getInventoryMaterials() { diff --git a/SchematicSystem/src/SchematicSystem.properties b/SchematicSystem/src/SchematicSystem.properties index c72bcf96..adb68d09 100644 --- a/SchematicSystem/src/SchematicSystem.properties +++ b/SchematicSystem/src/SchematicSystem.properties @@ -262,9 +262,8 @@ AUTO_CHECKER_RESULT_BLOCKS = §7Blocks: §c{0}§7, Max: §e{1} AUTO_CHECKER_RESULT_UNKNOWN_MATERIAL = §7Unknown block: §c{0} AUTO_CHECKER_RESULT_TOO_MANY_BLOCK = §7{0}: §c{1}§7, Max: §e{2} AUTO_CHECKER_RESULT_FORBIDDEN_BLOCK = §7Forbidden block: §c{0} -AUTO_CHECKER_RESULT_WIND_CHARGES = §7Windcharges: §c{0}§7, Max: §e2048 -AUTO_CHECKER_RESULT_WIND_CHARGES_DISPENSER = §7Dispenser: §c[{0}, {1}, {2}]§7, Windcharges: §c{3}§7 AUTO_CHECKER_RESULT_FORBIDDEN_ITEM = §7Forbidden Item: [{0}, {1}, {2}] -> §c{3} +AUTO_CHECKER_RESULT_MIXED_ITEMS = §7Items of different rules in one inventory: [{0}, {1}, {2}] -> §c{3} AUTO_CHECKER_RESULT_DEFUNCT_NBT = §7Defunct NBT: §7[{0}, {1}, {2}] AUTO_CHECKER_RESULT_DESIGN_BLOCK = §7{0} in Design: [{1}, {2}, {3}] AUTO_CHECKER_RESULT_ENTITY = §7Entity: §7[{0}, {1}, {2}] diff --git a/SchematicSystem/src/SchematicSystem_de.properties b/SchematicSystem/src/SchematicSystem_de.properties index d584c602..6d54d9bc 100644 --- a/SchematicSystem/src/SchematicSystem_de.properties +++ b/SchematicSystem/src/SchematicSystem_de.properties @@ -242,9 +242,8 @@ AUTO_CHECKER_RESULT_BLOCKS = §7Blöcke: §c{0}§7, Max: §e{1} AUTO_CHECKER_RESULT_UNKNOWN_MATERIAL = §7Unbekannter Block: §c{0} AUTO_CHECKER_RESULT_TOO_MANY_BLOCK = §7{0}: §c{1}§7, Max: §e{2} AUTO_CHECKER_RESULT_FORBIDDEN_BLOCK = §7Verbotener Block: §c{0} -AUTO_CHECKER_RESULT_WIND_CHARGES = §7Windcharges: §c{0}§7, Max: §e2048 -AUTO_CHECKER_RESULT_WIND_CHARGES_DISPENSER = §7Werfer: §c[{0}, {1}, {2}]§7, Windcharges: §c{3}§7 AUTO_CHECKER_RESULT_FORBIDDEN_ITEM = §7Verbotener gegenstand: [{0}, {1}, {2}] -> §c{3} +AUTO_CHECKER_RESULT_MIXED_ITEMS = §7Gegenstände verschiedener Regeln in einem Inventar: [{0}, {1}, {2}] -> §c{3} AUTO_CHECKER_RESULT_DEFUNCT_NBT = §7Keine NBT-Daten: §c[{0}, {1}, {2}] AUTO_CHECKER_RESULT_DESIGN_BLOCK = §7{0} im Design: [{1}, {2}, {3}] AUTO_CHECKER_RESULT_RECORD = §7Schallplatte: §c[{0}, {1}, {2}] diff --git a/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoChecker.java b/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoChecker.java index cb23bbbd..1d0b6bc7 100644 --- a/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoChecker.java +++ b/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoChecker.java @@ -25,6 +25,7 @@ import com.sk89q.worldedit.extent.clipboard.Clipboard; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.world.block.BaseBlock; import de.steamwar.sql.GameModeConfig; +import de.steamwar.sql.GameModeConfig.SchematicConfig.BlockRule; import lombok.Getter; import lombok.ToString; import org.bukkit.Material; @@ -62,6 +63,7 @@ public class AutoChecker { public AutoChecker.BlockScanResult scan(Clipboard clipboard, GameModeConfig type) { AutoChecker.BlockScanResult result = new AutoChecker.BlockScanResult(); + Map>> ruleCandidates = new HashMap<>(); BlockVector3 min = clipboard.getMinimumPoint(); BlockVector3 max = clipboard.getMaximumPoint(); for (int x = min.x(); x <= max.x(); x++) { @@ -75,13 +77,13 @@ public class AutoChecker { BlockPos pos = new BlockPos(x, y, z); + result.getBlockCounts().merge(material, 1, Integer::sum); if (type.Schematic.isInventory(material)) { - checkInventory(result, block, material, pos, type); - if (!result.getInventoryItemCounts().getOrDefault(pos, Collections.emptyMap()).isEmpty()) { - result.getBlockCounts().merge(material, 1, Integer::sum); + result.getInventoryBlockType().put(pos, material); + List> candidates = checkInventory(result, block, material, pos, type); + if (!candidates.isEmpty()) { + ruleCandidates.put(pos, candidates); } - } else { - result.getBlockCounts().merge(material, 1, Integer::sum); } if (x == min.x() || x == max.x() || y == max.y() || z == min.z() || z == max.z()) { @@ -90,28 +92,28 @@ public class AutoChecker { } } } + assignRules(result, ruleCandidates); return result; } - private void checkInventory(AutoChecker.BlockScanResult result, BaseBlock block, Material material, BlockPos pos, GameModeConfig type) { + /** + * Scans the inventory and returns the rules this block may be counted towards. + * An empty list means the block is either faulty (reported in the result) or unlimited. + */ + private List> checkInventory(AutoChecker.BlockScanResult result, BaseBlock block, Material material, BlockPos pos, GameModeConfig type) { CompoundTag nbt = block.getNbtData(); if (nbt == null) { result.getDefunctNbt().add(pos); - return; + return Collections.emptyList(); } - if (material == Material.JUKEBOX && nbt.getValue().containsKey("RecordItem")) { result.getRecords().add(pos); - return; + return Collections.emptyList(); } - List items = nbt.getList("Items", CompoundTag.class); - if (items.isEmpty()) return; // Leeres Inventar - Map itemCounts = new EnumMap<>(Material.class); - int windChargeCount = 0; - for (CompoundTag item : items) { + for (CompoundTag item : nbt.getList("Items", CompoundTag.class)) { if (!item.containsKey("id")) { result.getDefunctNbt().add(pos); continue; @@ -121,22 +123,69 @@ public class AutoChecker { if (itemType == null) // Leere Slots continue; - if (type.Schematic.Type.getName().equals("wargearseason26") && material == Material.DISPENSER && itemType == Material.WIND_CHARGE) { - windChargeCount += item.getInt("count"); - } else if (type.Schematic.getItemAmount(material, itemType) == 0) { - result.getForbiddenItems().computeIfAbsent(pos, blockVector3 -> new HashSet<>()).add(itemType); - } else { - itemCounts.merge(itemType, item.getInt("count"), Integer::sum); - } + itemCounts.merge(itemType, item.getInt("count"), Integer::sum); if (item.containsKey("tag")) { result.getForbiddenNbt().computeIfAbsent(pos, blockVector3 -> new HashSet<>()).add(itemType); } } - if (!itemCounts.isEmpty()) { - result.getInventoryItemCounts().put(pos, itemCounts); - result.getInventoryBlockType().put(pos, material); + + List> rules = type.Schematic.getRules(material); + if (itemCounts.isEmpty()) { // Leeres Inventar + return rules.stream().filter(rule -> !rule.Content.loaded).collect(Collectors.toList()); } - result.getWindChargeCount().put(pos, windChargeCount); + result.getInventoryItemCounts().put(pos, itemCounts); + + List> contentRules = rules.stream().filter(rule -> rule.Content.loaded).collect(Collectors.toList()); + List> itemsFitting = contentRules.stream() + .filter(rule -> rule.Content.Items.containsAll(itemCounts.keySet())) + .collect(Collectors.toList()); + List> fitting = itemsFitting.stream() + .filter(rule -> itemCounts.values().stream().allMatch(count -> count <= rule.Content.Amount)) + .collect(Collectors.toList()); + if (!fitting.isEmpty()) { + return fitting; + } + + if (!itemsFitting.isEmpty()) { + BlockRule best = Collections.max(itemsFitting, Comparator.comparingInt(rule -> rule.Content.Amount)); + itemCounts.forEach((itemType, count) -> { + if (count > best.Content.Amount) { + result.getTooManyItems().computeIfAbsent(pos, blockVector3 -> new EnumMap<>(Material.class)).put(itemType, best.Content.Amount); + } + }); + return Collections.singletonList(best); + } + + Set allowed = contentRules.stream().flatMap(rule -> rule.Content.Items.stream()).collect(Collectors.toSet()); + Set forbidden = itemCounts.keySet().stream().filter(itemType -> !allowed.contains(itemType)).collect(Collectors.toSet()); + if (!forbidden.isEmpty()) { + result.getForbiddenItems().put(pos, forbidden); + } + + Set rest = itemCounts.keySet().stream().filter(allowed::contains).collect(Collectors.toSet()); + if (!rest.isEmpty()) { + // Every remaining item is allowed on its own, but no single rule allows all of them together + BlockRule best = Collections.max(contentRules, Comparator.comparingLong(rule -> rest.stream().filter(rule.Content.Items::contains).count())); + Set mixed = rest.stream().filter(itemType -> !best.Content.Items.contains(itemType)).collect(Collectors.toSet()); + if (!mixed.isEmpty()) { + result.getMixedItems().put(pos, mixed); + } + } + return Collections.emptyList(); + } + + /** + * Counts every block towards one of its candidate rules, preferring the rule with the most capacity left. + * Blocks with fewer candidates are assigned first. + */ + private void assignRules(AutoChecker.BlockScanResult result, Map>> ruleCandidates) { + ruleCandidates.entrySet().stream() + .sorted(Comparator.comparingInt(entry -> entry.getValue().size())) + .forEach(entry -> { + BlockRule rule = Collections.max(entry.getValue(), Comparator.comparingLong(candidate -> (long) candidate.Amount - result.getRuleCounts().getOrDefault(candidate, 0))); + result.getRuleCounts().merge(rule, 1, Integer::sum); + result.getRuleBlockTypes().computeIfAbsent(rule, r -> EnumSet.noneOf(Material.class)).add(result.getInventoryBlockType().get(entry.getKey())); + }); } @Getter @@ -148,8 +197,11 @@ public class AutoChecker { private final Map> designBlocks = new EnumMap<>(Material.class); private final Map> inventoryItemCounts = new HashMap<>(); private final Map inventoryBlockType = new HashMap<>(); - private final Map windChargeCount = new HashMap<>(); + private final Map, Integer> ruleCounts = new HashMap<>(); + private final Map, Set> ruleBlockTypes = new HashMap<>(); + private final Map> tooManyItems = new HashMap<>(); private final Map> forbiddenItems = new HashMap<>(); + private final Map> mixedItems = new HashMap<>(); private final Map> forbiddenNbt = new HashMap<>(); } } diff --git a/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoCheckerResult.java b/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoCheckerResult.java index c59f17ab..ca684244 100644 --- a/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoCheckerResult.java +++ b/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoCheckerResult.java @@ -31,6 +31,7 @@ import org.bukkit.entity.Player; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Builder @Getter @@ -46,12 +47,12 @@ public class AutoCheckerResult { public boolean isOk() { return blockScanResult.getRecords().isEmpty() && blockScanResult.getForbiddenItems().isEmpty() && + blockScanResult.getMixedItems().isEmpty() && blockScanResult.getForbiddenNbt().isEmpty() && isSizeOk() && isBlockCountOk() && isLimitedBlocksOK() && isInventoryItemsOK() && - isWindchargeCountOK() && !type.isAfterDeadline() && entities.isEmpty() && isDesignBlastResistanceOK(); @@ -62,23 +63,8 @@ public class AutoCheckerResult { !type.isAfterDeadline(); } - public boolean isWindchargeCountOK() { - if (type.Schematic.Type.getName().equals("wargearseason26")) { - int windChargesCount = blockScanResult.getWindChargeCount().values().stream().reduce(Integer::sum).orElse(0); - return windChargesCount <= 2048; - } else { - return true; - } - } - public boolean isInventoryItemsOK() { - return blockScanResult.getInventoryItemCounts().entrySet().stream().allMatch(posEntry -> { - Material inventory = blockScanResult.getInventoryBlockType().get(posEntry.getKey()); - return posEntry.getValue().entrySet().stream().allMatch(itemEntry -> { - int cap = type.Schematic.getItemAmount(inventory, itemEntry.getKey()); - return itemEntry.getValue() <= cap; - }); - }); + return blockScanResult.getTooManyItems().isEmpty(); } public boolean hasWarnings() { @@ -107,7 +93,10 @@ public class AutoCheckerResult { public boolean isLimitedBlocksOK() { return blockScanResult.getBlockCounts().entrySet().stream() - .allMatch(entry -> entry.getValue() <= type.Schematic.getMaxCount(entry.getKey())); + .filter(entry -> !type.Schematic.isInventory(entry.getKey())) + .allMatch(entry -> entry.getValue() <= type.Schematic.getMaxCount(entry.getKey())) && + blockScanResult.getRuleCounts().entrySet().stream() + .allMatch(entry -> entry.getValue() <= entry.getKey().Amount); } public boolean isDesignBlastResistanceOK() { @@ -132,6 +121,7 @@ public class AutoCheckerResult { } if (!isLimitedBlocksOK()) { blockScanResult.getBlockCounts().forEach((mat, count) -> { + if (type.Schematic.isInventory(mat)) return; int maxCount = type.Schematic.getMaxCount(mat); if (count > maxCount) { if (maxCount == 0) { @@ -141,30 +131,25 @@ public class AutoCheckerResult { } } }); - } - - if (!isWindchargeCountOK()) { - int windChargesCount = blockScanResult.getWindChargeCount().values().stream().reduce(Integer::sum).orElse(0); - SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_WIND_CHARGES", p, windChargesCount, 2048); - blockScanResult.getWindChargeCount().entrySet().stream().filter(blockVector3IntegerEntry -> blockVector3IntegerEntry.getValue() > 0).forEach(blockVector3IntegerEntry -> { - SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_WIND_CHARGES_DISPENSER", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3IntegerEntry.getKey()), - blockVector3IntegerEntry.getKey().getBlockX(), - blockVector3IntegerEntry.getKey().getBlockY(), - blockVector3IntegerEntry.getKey().getBlockZ(), - blockVector3IntegerEntry.getValue()); + blockScanResult.getRuleCounts().forEach((rule, count) -> { + if (count <= rule.Amount) return; + String blocks = blockScanResult.getRuleBlockTypes().get(rule).stream().map(Material::name).collect(Collectors.joining(", ")); + if (rule.Amount == 0) { + SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_FORBIDDEN_BLOCK", p, blocks); + } else { + SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_TOO_MANY_BLOCK", p, blocks, count, rule.Amount); + } }); } - blockScanResult.getInventoryItemCounts().forEach((pos, itemCounts) -> { + blockScanResult.getTooManyItems().forEach((pos, caps) -> { Material inventory = blockScanResult.getInventoryBlockType().get(pos); - itemCounts.forEach((itemType, count) -> { - int cap = type.Schematic.getItemAmount(inventory, itemType); - if (count <= cap) return; + caps.forEach((itemType, cap) -> { SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_TOO_MANY_INVENTORY_ITEMS", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(pos), pos.getBlockX(), pos.getBlockY(), pos.getBlockZ(), - count, + blockScanResult.getInventoryItemCounts().get(pos).get(itemType), cap, itemType.name(), inventory.name()); @@ -177,6 +162,9 @@ public class AutoCheckerResult { blockScanResult.getForbiddenItems().forEach((blockVector3, materials) -> { SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_FORBIDDEN_ITEM", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ(), setToString(materials)); }); + blockScanResult.getMixedItems().forEach((blockVector3, materials) -> { + SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_MIXED_ITEMS", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ(), setToString(materials)); + }); blockScanResult.getForbiddenNbt().forEach((blockVector3, materials) -> { SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_FORBIDDEN_ITEM_NBT", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ(), setToString(materials)); }); diff --git a/SchematicSystem/src/de/steamwar/schematicsystem/commands/schematiccommand/SchematicCommand.java b/SchematicSystem/src/de/steamwar/schematicsystem/commands/schematiccommand/SchematicCommand.java index e0b0b2ce..17525d8a 100644 --- a/SchematicSystem/src/de/steamwar/schematicsystem/commands/schematiccommand/SchematicCommand.java +++ b/SchematicSystem/src/de/steamwar/schematicsystem/commands/schematiccommand/SchematicCommand.java @@ -48,6 +48,7 @@ import java.util.*; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import static de.steamwar.schematicsystem.commands.schematiccommand.SchematicCommandHelp.*; import static de.steamwar.schematicsystem.commands.schematiccommand.SchematicCommandUtils.togglePublic; @@ -171,8 +172,8 @@ public class SchematicCommand extends SWCommand { Map> toBeCheckedInvs = new HashMap<>(); - toBeCheckedInvs.putAll(result.getBlockScanResult().getForbiddenItems()); - toBeCheckedInvs.putAll(result.getBlockScanResult().getForbiddenNbt()); + Stream.of(result.getBlockScanResult().getForbiddenItems(), result.getBlockScanResult().getMixedItems(), result.getBlockScanResult().getForbiddenNbt()) + .forEach(invs -> invs.forEach((pos, materials) -> toBeCheckedInvs.computeIfAbsent(pos, p -> new HashSet<>()).addAll(materials))); for (Map.Entry> entry : toBeCheckedInvs.entrySet()) { BlockPos pos = entry.getKey(); @@ -197,16 +198,13 @@ public class SchematicCommand extends SWCommand { clipboard.setBlock(vector, block.toBaseBlock(builder.build())); } - for (Map.Entry> posEntry : result.getBlockScanResult().getInventoryItemCounts().entrySet()) { + for (Map.Entry> posEntry : result.getBlockScanResult().getTooManyItems().entrySet()) { BlockPos pos = posEntry.getKey(); - Material inventory = result.getBlockScanResult().getInventoryBlockType().get(pos); + Map itemCounts = result.getBlockScanResult().getInventoryItemCounts().get(pos); for (Map.Entry itemEntry : posEntry.getValue().entrySet()) { Material itemType = itemEntry.getKey(); - Integer cap = type.Schematic.getItemAmount(inventory, itemType); - if (itemEntry.getValue() <= cap) { - continue; - } + int cap = itemEntry.getValue(); BlockVector3 vector = BlockVector3.at(pos.getX(), pos.getY(), pos.getZ()); BaseBlock block = clipboard.getFullBlock(vector); @@ -215,7 +213,7 @@ public class SchematicCommand extends SWCommand { List items = new ArrayList<>(tag.getList("Items", CompoundTag.class)); Collections.reverse(items); // To let the first item be in the inventory List list = new ArrayList<>(); - int diff = itemEntry.getValue() - cap; + int diff = itemCounts.get(itemType) - cap; for (CompoundTag item : items) { if (item == null) { continue; From 2302f9aafb4462a16f16cd15ea0584b2dec4ba9a Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Thu, 15 Oct 2026 18:13:53 +0200 Subject: [PATCH 16/32] 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 17/32] 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 18/32] 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 19/32] 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 20/32] 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 21/32] 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 22/32] 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 23/32] 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 24/32] 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 25/32] 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 26/32] 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 27/32] 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 28/32] 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 29/32] 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 30/32] 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 31/32] 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 32/32] 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);