diff --git a/.gitignore b/.gitignore index ba7d8990..6b5c70a1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,14 @@ !gradle/wrapper/gradle-wrapper.jar steamwar.properties +# Kotlin +.kotlin + # IntelliJ IDEA *.iml +.idea +!.idea/copyright +!.idea/runConfigurations # VSCode bin/ 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; diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NoClipCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NoClipCommand.java index 91a79f5e..9994e3d4 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NoClipCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NoClipCommand.java @@ -22,123 +22,71 @@ package de.steamwar.bausystem.features.util; import com.comphenix.tinyprotocol.TinyProtocol; import com.mojang.authlib.GameProfile; import de.steamwar.bausystem.BauSystem; -import de.steamwar.bausystem.features.tpslimit.TPSUtils; import de.steamwar.bausystem.utils.BauMemberUpdateEvent; import de.steamwar.command.SWCommand; import de.steamwar.core.ProtocolWrapper; import de.steamwar.core.SWPlayer; import de.steamwar.linkage.Linked; -import net.minecraft.network.protocol.game.*; -import net.minecraft.world.entity.player.Abilities; -import net.minecraft.world.level.GameType; -import org.bukkit.Bukkit; +import io.papermc.paper.event.player.PlayerFailMoveEvent; +import net.minecraft.network.protocol.game.ClientboundGameEventPacket; import org.bukkit.GameMode; -import org.bukkit.craftbukkit.entity.CraftPlayer; -import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.block.BlockCanBuildEvent; import org.bukkit.event.player.PlayerGameModeChangeEvent; -import org.bukkit.event.player.PlayerTeleportEvent; import org.bukkit.event.player.PlayerToggleFlightEvent; -import java.util.function.BiFunction; - @Linked public class NoClipCommand extends SWCommand implements Listener { public static class NoClipData implements SWPlayer.Component { - private long lastTick = -1; - @Override - public void onUnmount(SWPlayer player) { - player.getPlayer().setGameMode(GameMode.CREATIVE); + public void onUnmount(SWPlayer swPlayer) { + Player player = swPlayer.getPlayer(); + showClientGameMode(player, GameMode.CREATIVE, GameMode.CREATIVE); } } public NoClipCommand() { super("noclip", "nc"); - - BiFunction first = (player, o) -> { - NoClipData noClipData = SWPlayer.of(player).getComponent(NoClipData.class).orElse(null); - if (noClipData == null) return o; - if (noClipData.lastTick == TPSUtils.currentTick.get()) return o; - setInternalGameMode(player, GameMode.SPECTATOR); - noClipData.lastTick = TPSUtils.currentTick.get(); - return o; - }; - TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.Pos.class, first); - TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.PosRot.class, first); - - BiFunction second = (player, o) -> { - NoClipData noClipData = SWPlayer.of(player).getComponent(NoClipData.class).orElse(null); - if (noClipData == null) return o; - setInternalGameMode(player, GameMode.CREATIVE); - noClipData.lastTick = TPSUtils.currentTick.get(); - return o; - }; - TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, second); - TinyProtocol.instance.addFilter(ServerboundPlayerActionPacket.class, second); - TinyProtocol.instance.addFilter(ServerboundContainerClickPacket.class, second); - - BiFunction third = (player, o) -> { - if (SWPlayer.of(player).hasComponent(NoClipData.class)) { - int index = o.slotNum(); - if (index >= 36 && index <= 44) { - index -= 36; - } else if (index > 44) { - index -= 5; - } else if (index <= 8) { - index = index - 8 + 36; - } - player.getInventory().setItem(index, CraftItemStack.asBukkitCopy(o.itemStack())); - if (index < 9) player.getInventory().setHeldItemSlot(index); - player.updateInventory(); - } - return o; - }; - TinyProtocol.instance.addFilter(ServerboundSetCreativeModeSlotPacket.class, third); } - private void setInternalGameMode(Player player, GameMode gameMode) { - ((CraftPlayer) player).getHandle().gameMode.gameModeForPlayer = GameType.byId(gameMode.getValue()); - } - - @Register(help = true) + @Register public void genericCommand(@Validator Player player) { SWPlayer swPlayer = SWPlayer.of(player); if (swPlayer.hasComponent(NoClipData.class)) { swPlayer.removeComponent(NoClipData.class); } else { - player.setGameMode(GameMode.SPECTATOR); - Abilities abilities = (((CraftPlayer) player).getHandle()).getAbilities(); - abilities.mayBuild = true; - abilities.mayfly = true; - abilities.instabuild = true; - + player.setGameMode(GameMode.CREATIVE); + player.setAllowFlight(true); + player.setFlying(true); swPlayer.setComponent(new NoClipData()); BauSystem.MESSAGE.send("OTHER_NOCLIP_SLOT_INFO", player); - TinyProtocol.instance.sendPacket(player, new ClientboundGameEventPacket(ClientboundGameEventPacket.CHANGE_GAME_MODE, 1F)); - pseudoGameMode(player, GameMode.SPECTATOR); + showClientGameMode(player, GameMode.CREATIVE, GameMode.SPECTATOR); } } @EventHandler - public void onBauMemberUpdate(BauMemberUpdateEvent event) { - event.getNewSpectator().forEach(player -> { - SWPlayer.of(player).removeComponent(NoClipData.class); - }); + public void onPlayerFailMove(PlayerFailMoveEvent event) { + if (event.getFailReason() != PlayerFailMoveEvent.FailReason.CLIPPED_INTO_BLOCK) return; + if (!SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) return; + event.setAllowed(true); } - @EventHandler(ignoreCancelled = true) + @EventHandler + public void onBauMemberUpdate(BauMemberUpdateEvent event) { + event.getNewSpectator().forEach(player -> SWPlayer.of(player).removeComponent(NoClipData.class)); + } + + @EventHandler public void onPlayerGameModeChange(PlayerGameModeChangeEvent event) { if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) { event.setCancelled(true); } } - @EventHandler(ignoreCancelled = true) + @EventHandler public void onBlock(BlockCanBuildEvent event) { if (event.getPlayer() == null) return; if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) { @@ -146,7 +94,7 @@ public class NoClipCommand extends SWCommand implements Listener { } } - @EventHandler(ignoreCancelled = true) + @EventHandler public void onPlayerToggleFlight(PlayerToggleFlightEvent event) { if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) { event.setCancelled(true); @@ -154,20 +102,13 @@ public class NoClipCommand extends SWCommand implements Listener { } } - @EventHandler(ignoreCancelled = true) - public void onPlayerTeleport(PlayerTeleportEvent event) { - if (event.getCause() != PlayerTeleportEvent.TeleportCause.SPECTATE) { - return; - } - if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) { - event.setCancelled(true); - Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { - event.getPlayer().setSpectatorTarget(null); - }, 1L); - } + /** + * @param player player + * @param localMode GameMode for breaking and placing + * @param listedMode GameMode for noPhysics + */ + private static void showClientGameMode(Player player, GameMode localMode, GameMode listedMode) { + TinyProtocol.instance.sendPacket(player, new ClientboundGameEventPacket(ClientboundGameEventPacket.CHANGE_GAME_MODE, localMode.getValue())); + TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.GAMEMODE, new GameProfile(player.getUniqueId(), player.getName()), listedMode)); } - - private static void pseudoGameMode(Player player, GameMode gameMode) { - TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.GAMEMODE, new GameProfile(player.getUniqueId(), player.getName()), gameMode)); - } -} \ No newline at end of file +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/PistonCalculator.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/PistonCalculator.java index ec191942..8b9b4a62 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/PistonCalculator.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/PistonCalculator.java @@ -23,15 +23,17 @@ import de.steamwar.bausystem.BauSystem; import de.steamwar.bausystem.Permission; import de.steamwar.entity.*; import de.steamwar.linkage.Linked; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.piston.PistonStructureResolver; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; -import org.bukkit.block.PistonMoveReaction; -import org.bukkit.block.TileState; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.type.Piston; +import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.entity.Display; import org.bukkit.entity.Player; import org.bukkit.entity.TextDisplay; @@ -92,41 +94,48 @@ public class PistonCalculator implements Listener { @EventHandler public void onBlockPistonExtend(BlockPistonExtendEvent event) { if (pistOrders.isEmpty()) return; - Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { - movePistOrders(event.getDirection(), event.getBlocks()); - pistOrders.values().forEach(PistOrder::calculate); - }, 3); + removeMovedPistOrders(event.getBlocks()); + scheduleRecalculate(); } @EventHandler public void onBlockPistonRetract(BlockPistonRetractEvent event) { if (pistOrders.isEmpty()) return; - Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { - movePistOrders(event.getDirection(), event.getBlocks()); - pistOrders.values().forEach(PistOrder::calculate); - }, 3); + removeMovedPistOrders(event.getBlocks()); + scheduleRecalculate(); } - private void movePistOrders(BlockFace direction, List blocks) { - Set orders = new HashSet<>(); + private void removeMovedPistOrders(List blocks) { blocks.forEach(block -> { - PistOrder pistOrder = pistOrders.get(block.getLocation()); - if (pistOrder == null) return; - pistOrders.remove(block.getLocation()); - pistOrder.piston = pistOrder.piston.getRelative(direction); - orders.add(pistOrder); - }); - orders.forEach(pistOrder -> { - pistOrders.put(pistOrder.piston.getLocation(), pistOrder); + PistOrder pistOrder = pistOrders.remove(block.getLocation()); + if (pistOrder != null) pistOrder.server.close(); }); } + private static final long RECALCULATE_INTERVAL_MS = 200; + private boolean recalculationScheduled = false; + private long lastRecalculation = 0; + + private void scheduleRecalculate() { + if (recalculationScheduled) return; + recalculationScheduled = true; + Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), this::recalculateIfDue, 1); + } + + private void recalculateIfDue() { + if (System.currentTimeMillis() - lastRecalculation < RECALCULATE_INTERVAL_MS) { + Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), this::recalculateIfDue, 1); + return; + } + recalculationScheduled = false; + lastRecalculation = System.currentTimeMillis(); + pistOrders.values().forEach(PistOrder::calculate); + } + @EventHandler public void onBlockPlace(BlockPlaceEvent event) { if (pistOrders.isEmpty()) return; - Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { - pistOrders.values().forEach(PistOrder::calculate); - }, 1); + scheduleRecalculate(); } @EventHandler @@ -136,9 +145,7 @@ public class PistonCalculator implements Listener { PistOrder pistOrder = pistOrders.remove(event.getBlock().getLocation()); pistOrder.server.close(); } - Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { - pistOrders.values().forEach(PistOrder::calculate); - }, 1); + scheduleRecalculate(); } @EventHandler @@ -164,13 +171,13 @@ public class PistonCalculator implements Listener { private final Map pistOrders = new HashMap<>(); private final class PistOrder { - private Block piston; + private final Block piston; private REntityServer server = new REntityServer(); private boolean pulling = false; private List movedBlocks = new ArrayList<>(); private List brokenBlocks = new ArrayList<>(); - private Set immovableBlocks = new HashSet<>(); + private boolean blocked = false; public PistOrder(Block piston) { this.piston = piston; @@ -179,7 +186,7 @@ public class PistonCalculator implements Listener { public void calculate() { movedBlocks.clear(); brokenBlocks.clear(); - immovableBlocks.clear(); + blocked = false; if (piston.isEmpty()) { Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { @@ -199,7 +206,7 @@ public class PistonCalculator implements Listener { pulling = piston.getType() == Material.STICKY_PISTON && (piston.getRelative(pistonData.getFacing()).getType() == Material.AIR || pistonData.isExtended()); - calculate(piston, pistonData.getFacing(), (pulling ? pistonData.getFacing().getOppositeFace() : pistonData.getFacing())); + resolve(pistonData.getFacing()); Collections.reverse(movedBlocks); Collections.reverse(brokenBlocks); @@ -207,86 +214,23 @@ public class PistonCalculator implements Listener { visualize(); } - private void calculate(Block origin, BlockFace facing, BlockFace direction) { - Block calcOrigin = origin; - if (facing != direction) calcOrigin = origin.getRelative(facing, 3); - - List toCalc = new LinkedList<>(); - calcDirection(origin, null, calcOrigin, facing != direction ? origin.getRelative(facing) : null, facing, direction, toCalc); - - while (!toCalc.isEmpty()) { - Block current = toCalc.remove(0); - if (!movedBlocks.contains(current.getLocation())) { - movedBlocks.add(current.getLocation()); - } - - Material type = current.getType(); - if (type != Material.SLIME_BLOCK && type != Material.HONEY_BLOCK) continue; - Material oppositeType = type == Material.SLIME_BLOCK ? Material.HONEY_BLOCK : Material.SLIME_BLOCK; - - for (BlockFace face : FACES) { - Block block = current.getRelative(face); - if (block.getType().isAir()) continue; - if (isImmovable(block) || block.getPistonMoveReaction() == PistonMoveReaction.BREAK) continue; - if (face != direction || face == direction.getOppositeFace()) { - switch (block.getType()) { - case BLACK_GLAZED_TERRACOTTA: - case GRAY_GLAZED_TERRACOTTA: - case BLUE_GLAZED_TERRACOTTA: - case BROWN_GLAZED_TERRACOTTA: - case CYAN_GLAZED_TERRACOTTA: - case GREEN_GLAZED_TERRACOTTA: - case LIGHT_BLUE_GLAZED_TERRACOTTA: - case LIGHT_GRAY_GLAZED_TERRACOTTA: - case LIME_GLAZED_TERRACOTTA: - case MAGENTA_GLAZED_TERRACOTTA: - case ORANGE_GLAZED_TERRACOTTA: - case PINK_GLAZED_TERRACOTTA: - case PURPLE_GLAZED_TERRACOTTA: - case RED_GLAZED_TERRACOTTA: - case WHITE_GLAZED_TERRACOTTA: - case YELLOW_GLAZED_TERRACOTTA: - continue; - default: - break; - } - } - if (block.getType() != oppositeType) { - if (!movedBlocks.contains(block.getLocation())) toCalc.add(block); - calcDirection(null, origin, block, null, facing, direction, toCalc); - } - } + private void resolve(BlockFace facing) { + PistonStructureResolver resolver = new PistonStructureResolver( + ((CraftWorld) piston.getWorld()).getHandle(), + new BlockPos(piston.getX(), piston.getY(), piston.getZ()), + Direction.valueOf(facing.name()), + !pulling + ); + if (!resolver.resolve()) { + blocked = true; + return; } - - movedBlocks.remove(origin.getLocation()); - if (facing != direction) movedBlocks.remove(origin.getRelative(facing, 1).getLocation()); - if (pulling) immovableBlocks.remove(origin.getRelative(facing).getLocation()); + resolver.getToPush().forEach(pos -> movedBlocks.add(toLocation(pos))); + resolver.getToDestroy().forEach(pos -> brokenBlocks.add(toLocation(pos))); } - private void calcDirection(Block origin, Block blockOrigin, Block calcOrigin, Block ignore, BlockFace facing, BlockFace direction, List toCalc) { - for (int i = 1; i < 24; i++) { - Block block = calcOrigin.getRelative(direction, i); - if (block.equals(ignore)) return; - if (block.getPistonMoveReaction() == PistonMoveReaction.BREAK) { - if (!brokenBlocks.contains(block.getLocation())) { - brokenBlocks.add(block.getLocation()); - } - return; - } - if (isImmovable(block)) { - immovableBlocks.add(block.getLocation()); - return; - } - if (block.getType().isAir()) return; - if (facing == direction && block.equals(blockOrigin)) { - immovableBlocks.add(block.getLocation()); - return; - } - if (facing != direction && (block.equals(origin) || block.getRelative(facing.getOppositeFace()).equals(origin))) { - return; - } - if (!movedBlocks.contains(block.getLocation())) toCalc.add(block); - } + private Location toLocation(BlockPos pos) { + return new Location(piston.getWorld(), pos.getX(), pos.getY(), pos.getZ()); } private void visualize() { @@ -321,21 +265,11 @@ public class PistonCalculator implements Listener { textDisplay.setBrightness(new Display.Brightness(15, 15)); } - for (Location location : immovableBlocks) { - CWireframe wireframe = new CWireframe(server); - wireframe.setPos1(location); - wireframe.setPos2(location); - wireframe.setWidth(1 / 32f); - wireframe.setBlock(Material.RED_CONCRETE.createBlockData()); - } - CWireframe wireframe = new CWireframe(server); wireframe.setPos1(piston.getLocation()); wireframe.setPos2(piston.getLocation()); - if (!immovableBlocks.isEmpty()) { + if (blocked) { wireframe.setBlock(Material.RED_CONCRETE.createBlockData()); - } else if (movedBlocks.size() > 12) { - wireframe.setBlock(Material.YELLOW_CONCRETE.createBlockData()); } else { wireframe.setBlock(Material.LIME_CONCRETE.createBlockData()); } @@ -348,7 +282,11 @@ public class PistonCalculator implements Listener { } else { text.append("§ePush\n"); } - text.append("§f").append(movedBlocks.size()).append(" §eBlocks"); + if (blocked) { + text.append("§cBlocked"); + } else { + text.append("§f").append(movedBlocks.size()).append(" §eBlocks"); + } textDisplay.setText(text.toString()); textDisplay.setBillboard(Display.Billboard.CENTER); textDisplay.setAlignment(TextDisplay.TextAlignment.CENTER); @@ -358,26 +296,4 @@ public class PistonCalculator implements Listener { textDisplay.setBrightness(new Display.Brightness(15, 15)); } } - - private boolean isImmovable(Block block) { - if (block.isEmpty() || block.isLiquid()) { - return false; - } - BlockData blockData = block.getBlockData(); - if (blockData instanceof Piston) { - return ((Piston) blockData).isExtended(); - } - if (block.getState() instanceof TileState) { - return true; - } - PistonMoveReaction reaction = block.getPistonMoveReaction(); - if (reaction == PistonMoveReaction.IGNORE) return true; - if (reaction == PistonMoveReaction.BLOCK) return true; - switch (block.getType()) { - case OBSIDIAN: - return true; - default: - return false; - } - } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauMemberUpdate.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauMemberUpdate.java index d3416e08..d8827c4b 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauMemberUpdate.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauMemberUpdate.java @@ -28,6 +28,7 @@ import de.steamwar.bausystem.utils.bossbar.BossBarService; import de.steamwar.linkage.Linked; import de.steamwar.network.packets.PacketHandler; import de.steamwar.network.packets.server.BaumemberUpdatePacket; +import de.steamwar.sql.BauweltMember; import io.papermc.paper.event.player.PlayerPickBlockEvent; import org.bukkit.Bukkit; import org.bukkit.boss.BarColor; @@ -57,6 +58,8 @@ public class BauMemberUpdate extends PacketHandler implements Listener { } public static void baumemberUpdate() { + // Packet handlers run in no guaranteed order, so don't rely on CoreNetworkHandler having cleared the cache yet. + BauweltMember.clear(); Set newSpectator = new HashSet<>(); Set newBuilder = new HashSet<>(); Bukkit.getOnlinePlayers().forEach(player -> { diff --git a/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/CommonCore/SQL/src/de/steamwar/sql/BauweltMember.kt b/CommonCore/SQL/src/de/steamwar/sql/BauweltMember.kt index 8c94f52b..71c63d5c 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/BauweltMember.kt +++ b/CommonCore/SQL/src/de/steamwar/sql/BauweltMember.kt @@ -30,6 +30,7 @@ import org.jetbrains.exposed.v1.dao.CompositeEntity import org.jetbrains.exposed.v1.dao.CompositeEntityClass import org.jetbrains.exposed.v1.jdbc.insertIgnore import java.util.* +import java.util.concurrent.ConcurrentHashMap object BauweltMemberTable : CompositeIdTable("BauweltMember") { val bauweltId = reference("BauweltID", SteamwarUserTable).index() @@ -48,10 +49,12 @@ object BauweltMemberTable : CompositeIdTable("BauweltMember") { class BauweltMember(id: EntityID) : CompositeEntity(id) { companion object : CompositeEntityClass(BauweltMemberTable) { - private val cache = mutableMapOf() + // Keyed by (bauweltId, memberId). Optional.empty() caches "is not a member", which is the + // common case for spectators and would otherwise hit the DB on every permission check. + private val cache = ConcurrentHashMap, Optional>() private fun cache(member: BauweltMember) = - cache.put(member.memberID, member) + cache.put(member.bauweltID to member.memberID, Optional.of(member)) @JvmStatic fun clear() = @@ -75,20 +78,20 @@ class BauweltMember(id: EntityID) : CompositeEntity(id) { it[worldEdit] = false it[world] = false } - } + }.also { cache.remove(ownerId.value to newMemberId.value) } @JvmStatic @Deprecated("Use getBauMember(bauwelt: Int, member: Int)") fun getBauMember(bauwelt: UUID, member: UUID) = - useDb { - find { (bauweltId eq SteamwarUser.get(bauwelt)!!.id) and (BauweltMemberTable.memberId eq SteamwarUser.get(member)!!.id) }.firstOrNull()?.also { cache(it) } - } + getBauMember(SteamwarUser.get(bauwelt)!!.id.value, SteamwarUser.get(member)!!.id.value) @JvmStatic - fun getBauMember(bauwelt: Int, member: Int) = - useDb { - find { (bauweltId eq bauwelt) and (BauweltMemberTable.memberId eq member) }.firstOrNull()?.also { cache(it) } - } + fun getBauMember(bauwelt: Int, member: Int): BauweltMember? = + cache.computeIfAbsent(bauwelt to member) { + Optional.ofNullable(useDb { + find { (bauweltId eq bauwelt) and (BauweltMemberTable.memberId eq member) }.firstOrNull() + }) + }.orElse(null) @JvmStatic @Deprecated("Use getMembers(bauwelt: Int)") @@ -140,8 +143,11 @@ class BauweltMember(id: EntityID) : CompositeEntity(id) { fun isWorld() = world - fun remove() = + fun remove() { + val key = bauweltID to memberID useDb { delete() } + cache.remove(key) + } } \ No newline at end of file diff --git a/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java b/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java index 2292ed86..7c6d369a 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java +++ b/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java @@ -617,6 +617,15 @@ public final class GameModeConfig { */ public final List SubTypes; + /** + * List of publics that should be used. Mainly useful for an Event Mode + * where a subset of the regular publics should be used without duplicating + * those in the Database. + * + * @implSpec {@code []} by default, thus the GameModes publics are used. + */ + public final List Publics; + /** * Shortcut of the schematic type * @@ -725,6 +734,7 @@ public final class GameModeConfig { Inset = new InsetConfig(loader.with("Inset")); Type = null; SubTypesStrings = loader.getStringList("SubTypes"); + Publics = loader.getIntList("Publics"); SubTypes = new ArrayList<>(); Shortcut = loader.getString("Shortcut", ""); Material = loader.getMaterial("Material", "STONE_BUTTON"); diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/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/ai/DummyAI.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/DummyAI.java index f4a5bfb0..f74ad6b9 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/DummyAI.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/DummyAI.java @@ -20,19 +20,14 @@ package de.steamwar.fightsystem.ai; import com.sk89q.worldedit.extent.clipboard.Clipboard; -import de.steamwar.fightsystem.Config; +import de.steamwar.fightsystem.fight.FightSchematic; import de.steamwar.fightsystem.fight.FightTeam; import de.steamwar.fightsystem.states.FightState; import de.steamwar.sql.SchematicNode; import de.steamwar.sql.SteamwarUser; -import java.util.List; -import java.util.Random; - public class DummyAI extends AI { - private static final Random random = new Random(); - public DummyAI(FightTeam team) { super(team, SteamwarUser.get("public")); @@ -41,8 +36,7 @@ public class DummyAI extends AI { @Override public SchematicNode chooseSchematic() { - List publics = SchematicNode.getAllSchematicsOfType(0, Config.GameModeConfig.Schematic.Type.toDB()); - return publics.get(random.nextInt(publics.size())); + return FightSchematic.getPublicSchematic().orElseThrow(); } @Override diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/commands/GUI.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/commands/GUI.java index a65886b0..c70fbeca 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/commands/GUI.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/commands/GUI.java @@ -255,7 +255,15 @@ public class GUI { private static void schemDialog(Player p, SchematicType type, boolean publicSchems, boolean unchecked) { SchematicSelector selector = new SchematicSelector(p, Config.test() ? SchematicSelector.selectSchematic() : SchematicSelector.selectSchematicType(unchecked ? type.checkType() : type), node -> { schemSelect(p, node); - }); + }) { + @Override + protected List getSchematicList(SchematicNode parent) { + if (!publicSchems || parent != null) return super.getSchematicList(parent); + List publics = Config.GameModeConfig.Schematic.Publics; + if (publics.isEmpty()) return super.getSchematicList(null); + return publics.stream().map(SchematicNode::byId).collect(Collectors.toList()); + } + }; selector.setPublicMode(publicSchems ? SchematicSelector.PublicMode.PUBLIC_ONLY : SchematicSelector.PublicMode.PRIVATE_ONLY); selector.open(); } diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightSchematic.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightSchematic.java index c8174349..014b9181 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightSchematic.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightSchematic.java @@ -46,6 +46,7 @@ import org.bukkit.util.Vector; import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Random; import java.util.logging.Level; @@ -109,31 +110,47 @@ public class FightSchematic extends StateDependent { clipboard = null; } + private static final Random random = new Random(); + + public static Optional getPublicSchematic() { + List publicsOverridden = Config.GameModeConfig.Schematic.Publics; + if (!publicsOverridden.isEmpty()) { + int publicId = publicsOverridden.get(random.nextInt(publicsOverridden.size())); + SchematicNode schematicNode = SchematicNode.getSchematicNode(publicId); + if (schematicNode != null) { + return Optional.of(schematicNode); + } + } + + List publics = SchematicNode.getAllSchematicsOfType(0, Config.GameModeConfig.Schematic.Type.toDB()); + if (publics.isEmpty()) { + for (SchematicType type : Config.GameModeConfig.Schematic.SubTypes) { + publics = SchematicNode.getAllSchematicsOfType(0, type.toDB()); + if (!publics.isEmpty()) { + break; + } + } + if (publics.isEmpty()) { + return Optional.empty(); + } + } + return Optional.of(publics.get(random.nextInt(publics.size()))); + } + @Override public void enable() { if (FightState.getFightState() == FightState.SPECTATE) return; if (clipboard == null) { - List publics = SchematicNode.getAllSchematicsOfType(0, Config.GameModeConfig.Schematic.Type.toDB()); - if (publics.isEmpty()) { - for (SchematicType type : Config.GameModeConfig.Schematic.SubTypes) { - publics = SchematicNode.getAllSchematicsOfType(0, type.toDB()); - if (!publics.isEmpty()) { - break; - } - } - if (publics.isEmpty()) { - return; - } - } - - setSchematic(publics.get(new Random().nextInt(publics.size()))); + Optional schematicNode = getPublicSchematic(); + if (schematicNode.isEmpty()) return; + setSchematic(schematicNode.get()); } if (ArenaMode.AntiReplay.contains(Config.mode)) { boolean changeRotation = false; if (Config.GameModeConfig.WinConditions.contains(Winconditions.RANDOM_ROTATE)) { - changeRotation = new Random().nextBoolean(); + changeRotation = random.nextBoolean(); usedRotate = rotate ^ changeRotation; } if (team.isBlue()) { 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/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/CLine.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/CLine.java index 1f877cce..a7628c40 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/CLine.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/CLine.java @@ -19,21 +19,23 @@ package de.steamwar.entity; +import lombok.AccessLevel; import lombok.Getter; import org.bukkit.Location; import org.bukkit.block.data.BlockData; import org.bukkit.entity.Display; import org.bukkit.entity.Player; -import org.bukkit.util.Consumer; import org.bukkit.util.Transformation; import org.bukkit.util.Vector; import org.joml.Quaternionf; import org.joml.Vector3f; import java.util.Objects; +import java.util.function.Consumer; /** * Can be used for Axis Aligned Lines of near infinite length. + * The line is a single display, which is shown to players based on their view distance. */ @Getter public class CLine extends CEntity { @@ -42,6 +44,8 @@ public class CLine extends CEntity { private static final float offset = 1 / 1024f; private static final Vector offsetVec = new Vector(offset, offset, offset); + @Getter(AccessLevel.NONE) + private LineDisplay line; private Location from; private Location to; private float width = DEFAULT_WIDTH; @@ -99,9 +103,7 @@ public class CLine extends CEntity { if (hide == this.hide) return; this.hide = hide; if (hide) { - if (startLine != null) startLine.hide(true); - if (middleLine != null) middleLine.hide(true); - if (endLine != null) endLine.hide(true); + if (line != null) line.hide(true); } else { tick(); } @@ -109,148 +111,26 @@ public class CLine extends CEntity { @Override void tick() { - if (from == null || to == null) { - if (startLine != null) startLine.hide(true); - if (middleLine != null) middleLine.hide(true); - if (endLine != null) endLine.hide(true); - return; - } else { - if (startLine != null) startLine.hide(false); - if (middleLine != null) middleLine.hide(false); - if (endLine != null) endLine.hide(false); - } - if (hide) return; - updateStart(); - updateMiddle(); - updateEnd(); - } - - private RBlockDisplay startLine; - - private void updateStart() { - Vector vec = to.clone().subtract(from).toVector(); - if (vec.length() > 35) vec.normalize().multiply(35); - - if (startLine == null) { - startLine = new RBlockDisplay(server, new Location(null, 0, 0, 0)); - startLine.setBrightness(new Display.Brightness(15, 15)); - startLine.setViewRange(560); - startLine.setBlock(blockData); - entities.add(startLine); - } else { - startLine.hide(false); - } - - startLine.move(from.clone().subtract(offsetVec)); - startLine.setTransform(new Transformation(new Vector3f(0, 0, 0), new Quaternionf(0, 0, 0, 1), addWidth(vec).toVector3f(), new Quaternionf(0, 0, 0, 1))); - } - - private RBlockDisplay middleLine; - - private void updateMiddle() { - Vector vec = to.clone().subtract(from).toVector(); - if (vec.length() <= 70) { - if (middleLine != null) middleLine.hide(true); + if (from == null || to == null || hide) { + if (line != null) line.hide(true); return; } - if (vec.length() > 280) vec.normalize().multiply(280); - else vec = vec.clone().normalize().multiply(vec.length() - 60); - if (middleLine == null) { - middleLine = new RBlockDisplay(server, new Location(null, 0, 0, 0)); - middleLine.setBrightness(new Display.Brightness(15, 15)); - middleLine.setViewRange(560); - middleLine.setBlock(blockData); - entities.add(middleLine); + if (line == null) { + line = new LineDisplay(server); + line.setBrightness(new Display.Brightness(15, 15)); + line.setViewRange(560); + line.setBlock(blockData); + entities.add(line); } else { - middleLine.hide(false); + line.hide(false); } - Player player = server.getPlayers().stream().findFirst().orElse(null); - if (player == null) return; - - Vector tempVector = vec.clone().normalize().multiply(30); - Location from = this.from.clone().add(tempVector); - Location to = this.to.clone().subtract(tempVector); - - Vector lineVec = to.clone().subtract(from).toVector(); - Vector playerVec = player.getLocation().toVector().subtract(from.toVector()); - double lineVecDotItself = lineVec.dot(lineVec); - Vector projectionVec = lineVec.clone().multiply(lineVec.dot(playerVec)).divide(new Vector(lineVecDotItself, lineVecDotItself, lineVecDotItself)); - - Vector moveVec = from.toVector().add(projectionVec); - if (moveVec.getX() < from.getX()) { - moveVec.setX(from.getX()); - } - if (moveVec.getX() > to.getX()) { - moveVec.setX(to.getX()); - } - if (moveVec.getY() < from.getY()) { - moveVec.setY(from.getY()); - } - if (moveVec.getY() > to.getY()) { - moveVec.setY(to.getY()); - } - if (moveVec.getZ() < from.getZ()) { - moveVec.setZ(from.getZ()); - } - if (moveVec.getZ() > to.getZ()) { - moveVec.setZ(to.getZ()); - } - - Vector translation = vec.clone().divide(new Vector(2, 2, 2)); - translation.setX(-translation.getX()); - translation.setY(-translation.getY()); - translation.setZ(-translation.getZ()); - - Vector first = moveVec.clone().add(translation).subtract(from.toVector()); - if (first.getX() < 0) { - translation.setX(translation.getX() - first.getX()); - } - if (first.getY() < 0) { - translation.setY(translation.getY() - first.getY()); - } - if (first.getZ() < 0) { - translation.setZ(translation.getZ() - first.getZ()); - } - - Vector second = to.toVector().subtract(moveVec.clone().subtract(translation)); - if (second.getX() < 0) { - translation.setX(translation.getX() + second.getX()); - } - if (second.getY() < 0) { - translation.setY(translation.getY() + second.getY()); - } - if (second.getZ() < 0) { - translation.setZ(translation.getZ() + second.getZ()); - } - - middleLine.move(moveVec.toLocation(player.getWorld()).subtract(offsetVec)); - middleLine.setTransform(new Transformation(translation.toVector3f(), new Quaternionf(0, 0, 0, 1), addWidth(vec).toVector3f(), new Quaternionf(0, 0, 0, 1))); - } - - private RBlockDisplay endLine; - - private void updateEnd() { Vector vec = to.clone().subtract(from).toVector(); - if (vec.length() <= 35) { - if (endLine != null) endLine.hide(true); - return; - } - if (vec.length() > 35) vec.normalize().multiply(35); - - if (endLine == null) { - endLine = new RBlockDisplay(server, new Location(null, 0, 0, 0)); - endLine.setBrightness(new Display.Brightness(15, 15)); - endLine.setViewRange(560); - endLine.setBlock(blockData); - entities.add(endLine); - } else { - endLine.hide(false); - } - - endLine.move(to.clone().subtract(offsetVec)); - endLine.setTransform(new Transformation(vec.toVector3f().negate(), new Quaternionf(0, 0, 0, 1), addWidth(vec).toVector3f(), new Quaternionf(0, 0, 0, 1))); + line.setSpan(from, to); + line.move(from.clone().subtract(offsetVec)); + line.setTransform(new Transformation(new Vector3f(0, 0, 0), new Quaternionf(0, 0, 0, 1), addWidth(vec).toVector3f(), new Quaternionf(0, 0, 0, 1))); + server.updateViewers(line); } private Vector addWidth(Vector vector) { @@ -267,4 +147,31 @@ public class CLine extends CEntity { vector.add(offsetVec).add(offsetVec); return vector; } + + /** + * The line is one display that is visible for a player as soon as any chunk it passes through is in the view distance, + * independent of the chunk its origin is in. + */ + private static class LineDisplay extends RBlockDisplay { + + private int minChunkX, maxChunkX, minChunkZ, maxChunkZ; + + private LineDisplay(REntityServer server) { + super(server, new Location(null, 0, 0, 0)); + } + + private void setSpan(Location from, Location to) { + minChunkX = Math.floorDiv((int) Math.floor(Math.min(from.getX(), to.getX())), 16); + maxChunkX = Math.floorDiv((int) Math.floor(Math.max(from.getX(), to.getX())), 16); + minChunkZ = Math.floorDiv((int) Math.floor(Math.min(from.getZ(), to.getZ())), 16); + maxChunkZ = Math.floorDiv((int) Math.floor(Math.max(from.getZ(), to.getZ())), 16); + } + + @Override + protected boolean canView(Player player, int playerChunkX, int playerChunkZ, int entityChunkX, int entityChunkZ) { + int distanceX = Math.max(0, Math.max(minChunkX - playerChunkX, playerChunkX - maxChunkX)); + int distanceZ = Math.max(0, Math.max(minChunkZ - playerChunkZ, playerChunkZ - maxChunkZ)); + return inViewDistance(player, Math.max(distanceX, distanceZ)); + } + } } diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RDisplay.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RDisplay.java index d96d085c..28d9744a 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RDisplay.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RDisplay.java @@ -26,6 +26,7 @@ import net.minecraft.network.syncher.EntityDataSerializers; import org.bukkit.Color; import org.bukkit.Location; import org.bukkit.entity.Display; +import org.bukkit.entity.Player; import org.bukkit.entity.EntityType; import org.bukkit.util.Transformation; import org.joml.Quaternionf; @@ -135,6 +136,15 @@ public abstract class RDisplay extends REntity { public void setViewRange(float viewRange) { this.viewRange = viewRange; sendPacket(updatePacketSink, this::getViewRange); + server.updateViewers(this); + } + + /** + * A view range of 1 equals 64 blocks. This is only a comparison, so huge values like {@link Float#MAX_VALUE} are cheap. + */ + @Override + protected boolean canView(Player player, int playerChunkX, int playerChunkZ, int entityChunkX, int entityChunkZ) { + return chunkDistance(playerChunkX, playerChunkZ, entityChunkX, entityChunkZ) <= Math.ceil(viewRange * 64.0 / 16.0); } private static final EntityDataAccessor viewRangeWatcher = new EntityDataAccessor<>(17, EntityDataSerializers.FLOAT); diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java index a5be3c41..8b6697ed 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java @@ -39,6 +39,7 @@ import org.bukkit.Location; import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.craftbukkit.util.CraftNamespacedKey; import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import java.util.*; @@ -56,6 +57,10 @@ public class REntity { private static final Random random = new Random(); protected final REntityServer server; + /** + * Players this entity is currently spawned for, maintained by the {@link REntityServer}. + */ + final Set viewers = new HashSet<>(); private final EntityType entityType; @Getter protected final int entityId; @@ -89,7 +94,7 @@ public class REntity { private final int objectData; @Getter private String displayName; - protected final Map itemSlots; + protected final Map itemSlots; public REntity(REntityServer server, EntityType entityType, Location location) { this(server, entityType, location, 0); @@ -120,6 +125,28 @@ public class REntity { this.objectData = objectData; } + /** + * Whether the given player should see this entity. + * Defaults to the view distance of the player. + * + * @param playerChunkX the chunk the player is in + * @param entityChunkX the chunk the entity is in, the entity itself might not be moved there yet + */ + protected boolean canView(Player player, int playerChunkX, int playerChunkZ, int entityChunkX, int entityChunkZ) { + return inViewDistance(player, chunkDistance(playerChunkX, playerChunkZ, entityChunkX, entityChunkZ)); + } + + /** + * @return the larger of the chunk distances on the x and z axis + */ + protected static int chunkDistance(int chunkX1, int chunkZ1, int chunkX2, int chunkZ2) { + return Math.max(Math.abs(chunkX1 - chunkX2), Math.abs(chunkZ1 - chunkZ2)); + } + + protected boolean inViewDistance(Player player, int chunkDistance) { + return chunkDistance <= server.viewRadius(player); + } + public void hide(boolean hide) { if (hidden == hide) return; @@ -180,7 +207,7 @@ public class REntity { } public void setVelocity(double dX, double dY, double dZ) { - server.updateEntity(this, new ClientboundSetEntityMotionPacket(entityId, new Vec3(calcVelocity(dX), calcVelocity(dY), calcVelocity(dZ)))); + server.updateEntity(this, new ClientboundSetEntityMotionPacket(entityId, new Vec3(dX, dY, dZ))); } public void showDamage() { @@ -220,7 +247,7 @@ public class REntity { .add(nameVisibleWatcher, displayName != null).build()); } - public void setItem(Object slot, ItemStack stack) { + public void setItem(org.bukkit.inventory.EquipmentSlot slot, ItemStack stack) { itemSlots.put(slot, stack); server.updateEntity(this, getEquipmentPacket(slot, stack)); } @@ -340,24 +367,22 @@ public class REntity { } private Object getTeleportPacket() { - PositionMoveRotation rot = new PositionMoveRotation(new Vec3(x, y, z), Vec3.ZERO, pitch, yaw); - return new ClientboundTeleportEntityPacket(entityId, rot, Collections.emptySet(), false); + PositionMoveRotation rot = new PositionMoveRotation(new Vec3(x, y, z), Vec3.ZERO, yaw * 360F / 256F, pitch * 360F / 256F); + return new ClientboundEntityPositionSyncPacket(entityId, rot, false); } private Object getMoveLookPacket(double diffX, double diffY, double diffZ, boolean rotEq) { short x = (short) (diffX * 4096); short y = (short) (diffY * 4096); short z = (short) (diffZ * 4096); - byte yaw = (byte) (this.yaw * 256 / 360); - byte pitch = (byte) (this.pitch * 256 / 360); if (diffX == 0 && diffY == 0 && diffZ == 0) { if (rotEq) return null; - return new ClientboundMoveEntityPacket.Rot(entityId, pitch, yaw, false); + return new ClientboundMoveEntityPacket.Rot(entityId, yaw, pitch, false); } else if (rotEq) { return new ClientboundMoveEntityPacket.Pos(entityId, x, y, z, false); } else { - return new ClientboundMoveEntityPacket.PosRot(entityId, x, y, z, pitch, yaw, false); + return new ClientboundMoveEntityPacket.PosRot(entityId, x, y, z, yaw, pitch, false); } } @@ -368,18 +393,25 @@ public class REntity { return packet; } - protected Object getEquipmentPacket(Object slot, ItemStack stack) { - return new ClientboundSetEquipmentPacket(entityId, Collections.singletonList(Pair.of((EquipmentSlot) slot, CraftItemStack.asNMSCopy(stack)))); + protected Object getEquipmentPacket(org.bukkit.inventory.EquipmentSlot slot, ItemStack stack) { + return new ClientboundSetEquipmentPacket(entityId, Collections.singletonList(Pair.of(toNmsSlot(slot), CraftItemStack.asNMSCopy(stack)))); + } + + private static EquipmentSlot toNmsSlot(org.bukkit.inventory.EquipmentSlot slot) { + return switch (slot) { + case HEAD -> EquipmentSlot.HEAD; + case CHEST -> EquipmentSlot.CHEST; + case LEGS -> EquipmentSlot.LEGS; + case FEET -> EquipmentSlot.FEET; + case OFF_HAND -> EquipmentSlot.OFFHAND; + default -> EquipmentSlot.MAINHAND; + }; } private byte rotToByte(float rot) { return (byte) ((int) (rot * 256.0F / 360.0F)); } - private int calcVelocity(double value) { - return (int) (Math.max(-3.9, Math.min(value, 3.9)) * 8000); - } - private static Optional formatDisplayName(String displayName) { return displayName != null ? Optional.of(MutableComponent.create(PlainTextContents.create(displayName))) : Optional.empty(); } diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java index 7a48f553..005dbf42 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java @@ -42,14 +42,8 @@ import java.util.stream.Collectors; public class REntityServer implements Listener { - private static final HashSet emptyEntities = new HashSet<>(0); - private static final HashSet emptyPlayers = new HashSet<>(0); - private final ConcurrentHashMap entityMap = new ConcurrentHashMap<>(); - private final HashMap> entities = new HashMap<>(); - private final HashMap> players = new HashMap<>(); private final HashMap lastLocation = new HashMap<>(); - private final HashMap viewDistance = new HashMap<>(); private final Set playersThatClicked = Collections.synchronizedSet(new HashSet<>()); @@ -78,20 +72,22 @@ public class REntityServer implements Listener { public void addPlayer(Player player) { Location location = player.getLocation(); lastLocation.put(player, location); - viewDistance.put(player, viewRadius(player)); - forChunkInView(player, location, (x, z) -> addPlayerToChunk(player, x, z)); + updateViewer(player, posToChunk(location.getX()), posToChunk(location.getZ())); } public void removePlayer(Player player) { - if (!viewDistance.containsKey(player)) { + if (lastLocation.remove(player) == null) { return; } - forChunkInView(player, lastLocation.remove(player), (x, z) -> removePlayerFromChunk(player, x, z)); - viewDistance.remove(player); + for (REntity entity : entityMap.values()) { + if (entity.viewers.remove(player)) { + hideFrom(entity, player); + } + } } public List getPlayers() { - return new ArrayList<>(viewDistance.keySet()); + return new ArrayList<>(lastLocation.keySet()); } public void close() { @@ -104,50 +100,77 @@ public class REntityServer implements Listener { void addEntity(REntity entity) { entityMap.put(entity.entityId, entity); - addEntityToChunk(entity); + updateViewers(entity); entity.list(packet -> updateEntity(entity, packet)); entity.spawn(packet -> updateEntity(entity, packet)); } void preEntityMove(REntity entity, double toX, double toZ) { - long fromId = entityToId(entity); - long toId = posToId(toX, toZ); - if (fromId == toId) return; + if (posToChunk(entity.x()) == posToChunk(toX) && posToChunk(entity.z()) == posToChunk(toZ)) return; - if (!entity.isHidden()) { - onMissing(players.get(fromId), players.get(toId), entity::despawn); - } - onMissing(players.get(fromId), players.get(toId), entity::delist); - removeEntityFromChunk(entity); + int chunkX = posToChunk(toX); + int chunkZ = posToChunk(toZ); + entity.viewers.removeIf(player -> { + if (inView(entity, player, chunkX, chunkZ)) return false; + hideFrom(entity, player); + return true; + }); } void postEntityMove(REntity entity, double fromX, double fromZ) { - long fromId = posToId(fromX, fromZ); - long toId = entityToId(entity); - if (fromId == toId) return; + if (posToChunk(fromX) == posToChunk(entity.x()) && posToChunk(fromZ) == posToChunk(entity.z())) return; - addEntityToChunk(entity); - onMissing(players.get(toId), players.get(fromId), entity::list); - if (!entity.isHidden()) { - onMissing(players.get(toId), players.get(fromId), entity::spawn); - } + addMissingViewers(entity); } void updateEntity(REntity entity, Object packet) { if (entity.isHidden()) return; - for (Player player : players.getOrDefault(entityToId(entity), emptyPlayers)) { + for (Player player : entity.viewers) { TinyProtocol.instance.sendPacket(player, packet); } } + /** + * Re-evaluates which players see the entity, e.g. after its view range changed. + */ + void updateViewers(REntity entity) { + if (!entityMap.containsKey(entity.entityId)) return; + + int chunkX = posToChunk(entity.x()); + int chunkZ = posToChunk(entity.z()); + entity.viewers.removeIf(player -> { + if (inView(entity, player, chunkX, chunkZ)) return false; + hideFrom(entity, player); + return true; + }); + addMissingViewers(entity); + } + + /** + * Re-sends list and spawn packets of an already added entity to all current viewers, + * e.g. when information that is part of the tab list entry changed after the entity was added. + */ + void refreshEntity(REntity entity) { + if (!entityMap.containsKey(entity.entityId)) return; + + for (Player player : entity.viewers) { + Consumer sink = packet -> TinyProtocol.instance.sendPacket(player, packet); + if (!entity.isHidden()) entity.despawn(sink); + entity.delist(sink); + entity.list(sink); + if (!entity.isHidden()) entity.spawn(sink); + } + } + void removeEntity(REntity entity) { entity.despawn(packet -> updateEntity(entity, packet)); - removeEntityFromChunk(entity); entity.delist(packet -> updateEntity(entity, packet)); + entity.viewers.clear(); entityMap.remove(entity.entityId); } + public List getEntities() { return new ArrayList<>(entityMap.values()); } @@ -156,18 +179,6 @@ public class REntityServer implements Listener { return entityMap.values().stream().filter(clazz::isInstance).map(clazz::cast).collect(Collectors.toList()); } - private void addEntityToChunk(REntity entity) { - entities.computeIfAbsent(entityToId(entity), i -> new HashSet<>()).add(entity); - } - - private void removeEntityFromChunk(REntity entity) { - long id = entityToId(entity); - HashSet entitiesInChunk = entities.get(id); - if (entitiesInChunk == null) return; - entitiesInChunk.remove(entity); - if (entitiesInChunk.isEmpty()) entities.remove(id); - } - @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) public void onMove(PlayerMoveEvent e) { Player player = e.getPlayer(); @@ -175,90 +186,62 @@ public class REntityServer implements Listener { Location to = e.getTo(); if (from == null || to == null) return; - int fromX = posToChunk(from.getX()); - int fromZ = posToChunk(from.getZ()); int toX = posToChunk(to.getX()); int toZ = posToChunk(to.getZ()); - if (fromX == toX && fromZ == toZ) return; + if (posToChunk(from.getX()) == toX && posToChunk(from.getZ()) == toZ) return; lastLocation.put(player, to); - - int toViewDistance = viewRadius(player); - forChunkInView(player, from, (x, z) -> { - if (Math.abs(x - toX) > toViewDistance || Math.abs(z - toZ) > toViewDistance) { - removePlayerFromChunk(player, x, z); - } - }); - - int fromViewDistance = this.viewDistance.put(player, toViewDistance); - forChunkInView(player, to, (x, z) -> { - if (Math.abs(x - fromX) > fromViewDistance || Math.abs(z - fromZ) > fromViewDistance) { - addPlayerToChunk(player, x, z); - } - }); + updateViewer(player, toX, toZ); } @EventHandler public void onQuit(PlayerQuitEvent e) { Player player = e.getPlayer(); - Location location = lastLocation.remove(player); - if (location == null) return; + if (lastLocation.remove(player) == null) return; - forChunkInView(player, location, (x, z) -> { - long id = chunkToId(x, z); - Set playersInChunk = players.get(id); - playersInChunk.remove(player); - if (playersInChunk.isEmpty()) players.remove(id); - }); - viewDistance.remove(player); + for (REntity entity : entityMap.values()) { + entity.viewers.remove(player); + } } - private void onMissing(Set of, Set in, Consumer> packetProvider) { - if (of == null) return; - - for (Player player : of) { - if (in == null || !in.contains(player)) { - packetProvider.accept(packet -> TinyProtocol.instance.sendPacket(player, packet)); + private void updateViewer(Player player, int chunkX, int chunkZ) { + for (REntity entity : entityMap.values()) { + boolean inView = inView(entity, player, chunkX, chunkZ); + if (inView) { + if (entity.viewers.add(player)) showTo(entity, player); + } else if (entity.viewers.remove(player)) { + hideFrom(entity, player); } } } - private void forChunkInView(Player player, Location location, BiConsumer func) { - int chunkX = posToChunk(location.getX()); - int chunkZ = posToChunk(location.getZ()); - int viewDistance = this.viewDistance.get(player); - - for (int x = chunkX - viewDistance; x <= chunkX + viewDistance; x++) { - for (int z = chunkZ - viewDistance; z <= chunkZ + viewDistance; z++) { - func.accept(x, z); + private void addMissingViewers(REntity entity) { + int chunkX = posToChunk(entity.x()); + int chunkZ = posToChunk(entity.z()); + for (Player player : lastLocation.keySet()) { + if (inView(entity, player, chunkX, chunkZ) && entity.viewers.add(player)) { + showTo(entity, player); } } } - private void addPlayerToChunk(Player player, int x, int z) { - long id = chunkToId(x, z); - players.computeIfAbsent(id, i -> new HashSet<>()).add(player); - for (REntity entity : entities.getOrDefault(id, emptyEntities)) { - entity.list(packet -> TinyProtocol.instance.sendPacket(player, packet)); - if (!entity.isHidden()) { - entity.spawn(packet -> TinyProtocol.instance.sendPacket(player, packet)); - } + private boolean inView(REntity entity, Player player, int entityChunkX, int entityChunkZ) { + Location location = lastLocation.get(player); + return entity.canView(player, posToChunk(location.getX()), posToChunk(location.getZ()), entityChunkX, entityChunkZ); + } + + private void showTo(REntity entity, Player player) { + entity.list(packet -> TinyProtocol.instance.sendPacket(player, packet)); + if (!entity.isHidden()) { + entity.spawn(packet -> TinyProtocol.instance.sendPacket(player, packet)); } } - private void removePlayerFromChunk(Player player, int x, int z) { - long id = chunkToId(x, z); - - Set playersInChunk = players.get(id); - playersInChunk.remove(player); - if (playersInChunk.isEmpty()) players.remove(id); - - for (REntity entity : entities.getOrDefault(id, emptyEntities)) { - if (!entity.isHidden()) { - entity.despawn(packet -> TinyProtocol.instance.sendPacket(player, packet)); - } - entity.delist(packet -> TinyProtocol.instance.sendPacket(player, packet)); + private void hideFrom(REntity entity, Player player) { + if (!entity.isHidden()) { + entity.despawn(packet -> TinyProtocol.instance.sendPacket(player, packet)); } + entity.delist(packet -> TinyProtocol.instance.sendPacket(player, packet)); } public void tick() { @@ -271,19 +254,10 @@ public class REntityServer implements Listener { return (int) (coord / 16) - (coord < 0 ? 1 : 0); } - private int viewRadius(Player player) { + /** + * Default view radius in chunks for entities without a view range of their own. + */ + int viewRadius(Player player) { return player.getClientViewDistance() / 2; } - - private long entityToId(REntity entity) { - return posToId(entity.x(), entity.z()); - } - - private long posToId(double x, double z) { - return chunkToId(posToChunk(x), posToChunk(z)); - } - - private long chunkToId(int x, int z) { - return ((long) x << 32) + z; - } } diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RPlayer.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RPlayer.java index cfb6304d..e13343dc 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RPlayer.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RPlayer.java @@ -76,10 +76,20 @@ public class RPlayer extends REntity implements RInteractableEntity { } private GameProfile saved; + private boolean awaitingSkin = false; @Override void list(Consumer packetSink) { saved = getGameProfile(); + if (saved.getProperties().isEmpty() && !awaitingSkin) { + // The skin is requested asynchronously, so it is usually not available yet when the entity is created. + // Without a refresh, players that already see this entity would keep the default skin. + awaitingSkin = true; + CoreNetworkHandler.onSkinReceived(actualUUID, () -> { + awaitingSkin = false; + server.refreshEntity(this); + }); + } packetSink.accept(ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.ADD, saved, GameMode.CREATIVE)); } @@ -88,7 +98,7 @@ public class RPlayer extends REntity implements RInteractableEntity { packetSink.accept(getNamedSpawnPacket()); entityDataPacket().add(skinPartsDataWatcher, (byte) 0x7F).send(packetSink); - for (Map.Entry entry : itemSlots.entrySet()) { + for (Map.Entry entry : itemSlots.entrySet()) { packetSink.accept(getEquipmentPacket(entry.getKey(), entry.getValue())); } diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/SchematicSelector.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/SchematicSelector.java index 5d649766..59df352e 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/SchematicSelector.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/SchematicSelector.java @@ -467,7 +467,7 @@ public class SchematicSelector { return nodes; } - private List getSchematicList(SchematicNode parent) { + protected List getSchematicList(SchematicNode parent) { List nodes = new ArrayList<>(); switch (target.target) { case DIRECTORY: diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/network/CoreNetworkHandler.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/network/CoreNetworkHandler.java index bc5d471e..a5fe8cf5 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/network/CoreNetworkHandler.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/network/CoreNetworkHandler.java @@ -21,6 +21,7 @@ package de.steamwar.network; import com.mojang.authlib.properties.Property; import de.steamwar.core.BountifulWrapper; +import de.steamwar.core.Core; import de.steamwar.network.handlers.InventoryHandler; import de.steamwar.network.packets.PacketHandler; import de.steamwar.network.packets.common.PlayerSkinResponsePacket; @@ -31,8 +32,11 @@ import org.bukkit.Bukkit; import org.bukkit.entity.Player; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; public class CoreNetworkHandler extends PacketHandler { @@ -85,9 +89,23 @@ public class CoreNetworkHandler extends PacketHandler { } }; + private static final Map> SKIN_LISTENERS = new ConcurrentHashMap<>(); + + /** + * Runs the given callback on the main thread once the skin of the given player has been received. + */ + public static void onSkinReceived(UUID uuid, Runnable callback) { + SKIN_LISTENERS.computeIfAbsent(uuid, __ -> new CopyOnWriteArrayList<>()).add(callback); + } + @Handler public void handlePlayerSkinResponse(PlayerSkinResponsePacket packet) { if (!SKIN_DATA_PROMISES.containsKey(packet.getUuid())) return; SKIN_DATA_PROMISES.put(packet.getUuid(), new Property("textures", packet.getSkin(), packet.getSignature())); + + Bukkit.getScheduler().runTask(Core.getInstance(), () -> { + List listeners = SKIN_LISTENERS.remove(packet.getUuid()); + if (listeners != null) listeners.forEach(Runnable::run); + }); } } 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/Persistent/src/de/steamwar/persistent/Subserver.java b/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java index 711bab09..f2a237aa 100644 --- a/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java +++ b/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java @@ -22,6 +22,7 @@ package de.steamwar.persistent; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; +import com.velocitypowered.api.scheduler.ScheduledTask; import lombok.Getter; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -32,7 +33,10 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.logging.Level; @@ -98,7 +102,13 @@ public class Subserver { @Getter private boolean started; - private final List cachedPlayers = new LinkedList<>(); + private static final int MAX_PROGRESS = 10; + private static final Pattern PLUGIN_COUNT = Pattern.compile("Bukkit plugins \\((\\d+)\\)"); + + private final List cachedPlayers = new CopyOnWriteArrayList<>(); + private volatile int progress = 0; + private int pluginCount = 0; + private int enabledPlugins = 0; @Getter private final Map tablistNames = new HashMap<>(); @@ -127,7 +137,7 @@ public class Subserver { public void sendPlayer(Player p) { if (!started) { - p.sendActionBar(generateBar(0)); + p.sendActionBar(generateBar(progress)); cachedPlayers.add(p); } else { p.createConnectionRequest(registeredServer).connect(); @@ -246,31 +256,32 @@ public class Subserver { private void run() { Exception ex = null; try { - if (checkpoint) { - start(process.getErrorStream(), line -> line.contains("Restore finished successfully.")); - start(process.getInputStream(), line -> line.contains("Checkpoint restored")); - Thread.sleep(100); - } else { - sendProgress(0); - start(process.getInputStream(), line -> { - if (line.contains("Loading libraries, please wait")) { - sendProgress(2); - } else if (line.contains("Starting Minecraft server on")) { - sendProgress(5); - } else if (line.contains("Preparing start region")) { - sendProgress(8); - } - return line.contains("Done ("); - }); - } + ScheduledTask refresh = Persistent.getInstance().getProxy().getScheduler() + .buildTask(Persistent.getInstance(), () -> sendProgress(progress)) + .repeat(2, TimeUnit.SECONDS).schedule(); - if (!started) return; + try { + if (checkpoint) { + start(process.getErrorStream(), line -> line.contains("Restore finished successfully.")); + start(process.getInputStream(), line -> line.contains("Checkpoint restored")); + Thread.sleep(100); + } else { + start(process.getInputStream(), line -> { + updateProgress(line); + return line.contains("Done ("); + }); + } - sendProgress(10); - for (Player cachedPlayer : cachedPlayers) { - sendPlayer(cachedPlayer); + if (!started) return; + + setProgress(MAX_PROGRESS); + for (Player cachedPlayer : cachedPlayers) { + sendPlayer(cachedPlayer); + } + cachedPlayers.clear(); + } finally { + refresh.cancel(); } - cachedPlayers.clear(); process.waitFor(); } catch (IOException e) { @@ -285,6 +296,39 @@ public class Subserver { } } + private void updateProgress(String line) { + if (line.startsWith("Loading Paper")) { + setProgress(1); + } else if (line.startsWith("Bukkit plugins (")) { + Matcher matcher = PLUGIN_COUNT.matcher(line); + if (matcher.find()) pluginCount = Integer.parseInt(matcher.group(1)); + } else if (line.startsWith("Starting minecraft server version")) { + setProgress(2); + } else if (line.contains("Starting Minecraft server on")) { + setProgress(3); + } else if (line.contains("libraries... please wait")) { + setProgress(4); + } else if (line.startsWith("Loading server plugin")) { + setProgress(5); + } else if (line.startsWith("Preparing level")) { + setProgress(6); + } else if (line.startsWith("Preparing start region")) { + setProgress(7); + } else if (line.startsWith("Enabling ")) { + enabledPlugins++; + // 7 -> 9 over all plugins, 9 is reached with the last plugin + setProgress(7 + (pluginCount > 0 ? Math.min(2, 2 * enabledPlugins / pluginCount) : 0)); + } else if (line.startsWith("Done preparing level")) { + setProgress(9); + } + } + + private void setProgress(int newProgress) { + if (newProgress <= progress) return; + progress = newProgress; + sendProgress(newProgress); + } + private Component generateBar(int progress) { return Component.text("⬛".repeat(Math.max(0, progress))).color(NamedTextColor.YELLOW) .append(Component.text("⬛".repeat(Math.max(0, 10 - progress))).color(NamedTextColor.DARK_GRAY)); diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index 6afbaf7e..8e47001e 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] @@ -574,12 +573,60 @@ CHAT_BC_USAGE = §8/§7bc §8[§emessage§8] CHAT_NO_RECEIVER = §cNobody receives your message CHAT_EMPTY = §cDon\'t write meaningless empty messages. -CHAT_SERVERTEAM = §8STC §e{0}§8» §f{2} -CHAT_DISCORD_SERVERTEAM = §8STC §e{0}§8» §f{2} -CHAT_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_DISCORD_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_TEAM = §8TC §e{0}§8» §f{2} -CHAT_MSG = §e{0}§8»§e{1} §7{2} +CHAT_SERVERTEAM_SENDER = §e{0} +CHAT_SERVERTEAM = §8» §f{2} +CHAT_GLOBAL_SENDER = {4}{5}{6}{0} +CHAT_GLOBAL = §8» {7}{2} +CHAT_TEAM_SENDER = §e{0} +CHAT_TEAM = §8» §f{2} +CHAT_MSG_SENDER = §e{0} +CHAT_MSG_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} +CHAT_FIGHT_SPECTATOR_SENDER = §7{1} +CHAT_FIGHT_SPECTATOR = §8» §7{0} +CHAT_FIGHT_REFEREE_SENDER = §e{1} +CHAT_FIGHT_REFEREE = §8» §e{0} +CHAT_FIGHT_TEAM = §8» §f{0} +CHAT_ICON = {0}{1} +CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Prefix: §r{1}{2} +CHAT_HOVER_TEAM_CLICK = §7Team: §e{0}\n§7Prefix: §r{1}{2}\n§8Click to insert §7/team info +CHAT_HOVER_LOCAL = {0}\n§7Server: §e{1} +CHAT_ICON_GLOBAL = 🌍 +CHAT_ICON_LOCAL = ⌂ +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 = §eFight +CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} +CHAT_CHANNEL_TEAM = §eTeamChat +CHAT_CHANNEL_SERVERTEAM = §eServerTeamChat +CHAT_CHANNEL_MSG = §eMSG §7{0} +CHAT_CHANNEL_ALERT = §cAlert +CHAT_CHANNEL_LOCAL = §eLocal +CHAT_CHANNEL_DEFAULT = §ais your default channel +CHAT_CHANNEL_NOT_SWITCHABLE = §cYou cannot write into this channel by default. +CHAT_NO_WRITE = §cYou cannot write into this channel. +CHAT_FIGHT_NO_TEAM = §cYou are not in a fight team. +GLOBAL_USAGE = §8/§7global §8[§emessage§8] +CHAT_USAGE = §8/§7chat §8[§echannel§8] [§ejoin§8|§eleave§8] +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. +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..6f23220f 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -399,7 +399,6 @@ 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] @@ -539,12 +538,60 @@ CHAT_BC_USAGE = §8/§7bc §8[§eNachricht§8] CHAT_NO_RECEIVER = §cNiemand empfängt deine Nachricht CHAT_EMPTY = §cSchreibe keine inhaltslosen Nachrichten. -CHAT_SERVERTEAM = §8STC §e{0}§8» §f{2} -CHAT_DISCORD_SERVERTEAM = §8STC §e{0}§8» §f{2} -CHAT_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_DISCORD_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_TEAM = §8TC §e{0}§8» §f{2} -CHAT_MSG = §e{0}§8»§e{1} §7{2} +CHAT_SERVERTEAM_SENDER = §e{0} +CHAT_SERVERTEAM = §8» §f{2} +CHAT_GLOBAL_SENDER = {4}{5}{6}{0} +CHAT_GLOBAL = §8» {7}{2} +CHAT_TEAM_SENDER = §e{0} +CHAT_TEAM = §8» §f{2} +CHAT_MSG_SENDER = §e{0} +CHAT_MSG_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} +CHAT_FIGHT_SPECTATOR_SENDER = §7{1} +CHAT_FIGHT_SPECTATOR = §8» §7{0} +CHAT_FIGHT_REFEREE_SENDER = §e{1} +CHAT_FIGHT_REFEREE = §8» §e{0} +CHAT_FIGHT_TEAM = §8» §f{0} +CHAT_ICON = {0}{1} +CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Kürzel: §r{1}{2} +CHAT_HOVER_TEAM_CLICK = §7Team: §e{0}\n§7Kürzel: §r{1}{2}\n§8Klicke, um §7/team info §8einzufügen +CHAT_HOVER_LOCAL = {0}\n§7Server: §e{1} +CHAT_ICON_GLOBAL = 🌍 +CHAT_ICON_LOCAL = ⌂ +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 = §eFight +CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} +CHAT_CHANNEL_TEAM = §eTeamChat +CHAT_CHANNEL_SERVERTEAM = §eServerTeamChat +CHAT_CHANNEL_MSG = §eMSG §7{0} +CHAT_CHANNEL_ALERT = §cAlert +CHAT_CHANNEL_LOCAL = §eLokal +CHAT_CHANNEL_DEFAULT = §aist dein Standardkanal +CHAT_CHANNEL_NOT_SWITCHABLE = §cIn diesen Kanal kannst du nicht standardmäßig schreiben. +CHAT_NO_WRITE = §cDu kannst nicht in diesen Kanal schreiben. +CHAT_FIGHT_NO_TEAM = §cDu bist in keinem Kampfteam. +GLOBAL_USAGE = §8/§7global §8[§eNachricht§8] +CHAT_USAGE = §8/§7chat §8[§eKanal§8] [§ejoin§8|§eleave§8] +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. +CHAT_CHANNEL_LEAVE_DEFAULT = §cDu kannst den Kanal, in den du schreibst, nicht verlassen. Wechsle zuerst in einen anderen Kanal. #CheckListner CHECK_UNCHECKED = §7Du hast noch §e{0} §7ungeprüfte Schematic§8(§7s§8)! diff --git a/VelocityCore/src/de/steamwar/messages/Chatter.java b/VelocityCore/src/de/steamwar/messages/Chatter.java index 5cc74f23..fc34a4f7 100644 --- a/VelocityCore/src/de/steamwar/messages/Chatter.java +++ b/VelocityCore/src/de/steamwar/messages/Chatter.java @@ -23,8 +23,6 @@ import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.event.ResultedEvent; import com.velocitypowered.api.event.connection.LoginEvent; import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.ServerConnection; -import de.steamwar.persistent.Subserver; import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.UserPerm; import de.steamwar.velocitycore.VelocityCore; @@ -61,13 +59,6 @@ public interface Chatter { return new ChatterGroup(allStream()); } - static ChatterGroup globalChat() { - return new ChatterGroup(Stream.concat(Stream.of(Chatter.console()), allPlayers().filter(player -> { - Subserver subserver = Subserver.getSubserver(player); - return !(Subserver.isArena(subserver) && subserver.getServer() == player.getCurrentServer().map(ServerConnection::getServerInfo).orElse(null)); - }).map(Chatter::of))); - } - static ChatterGroup serverteam() { return new ChatterGroup(allStream() .filter(player -> player.user().hasPerm(UserPerm.TEAM)) diff --git a/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java b/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java index 5790e53e..0c2c0aa8 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java +++ b/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java @@ -31,8 +31,10 @@ import net.kyori.adventure.text.event.ClickEvent; import java.sql.Timestamp; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Queue; +import java.util.Set; import java.util.concurrent.TimeUnit; import static de.steamwar.persistent.Storage.eventServer; @@ -43,8 +45,15 @@ public class EventStarter { @Getter private static final Map spectatePorts = new HashMap<>(); - public static void addSpectateServer(int port, String command) { + private static final Set spectateServers = new HashSet<>(); + + public static void addSpectateServer(int port, String command, String serverName) { spectatePorts.put(port, command); + spectateServers.add(serverName); + } + + public static boolean isSpectateServer(String serverName) { + return spectateServers.contains(serverName); } public EventStarter() { diff --git a/VelocityCore/src/de/steamwar/velocitycore/ServerStarter.java b/VelocityCore/src/de/steamwar/velocitycore/ServerStarter.java index a4dc58fc..eab63787 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/ServerStarter.java +++ b/VelocityCore/src/de/steamwar/velocitycore/ServerStarter.java @@ -303,7 +303,7 @@ public class ServerStarter { worldSetup.run(); arguments.put("logPath", worldName); - File checkpointDir = new File("/tmp/" + System.getProperty("user.name") + ".checkpoints/" + directory.getName() + "/" + worldName); + File checkpointDir = checkpointDir(directory, worldName); if (checkpoint) { arguments.put("checkpoint", checkpointDir.getPath()); } @@ -326,6 +326,10 @@ public class ServerStarter { }).schedule(); } + public static File checkpointDir(File serverDirectory, String worldName) { + return new File("/tmp/" + System.getProperty("user.name") + ".checkpoints/" + serverDirectory.getName() + "/" + worldName); + } + private void regularStart(String serverName, int port) { postStart(constructor.construct(serverName, port, node.startServer( version, directory, worldDir, worldName, port, arguments.entrySet().stream().map(entry -> entry.getKey() + "=" + entry.getValue()).toArray(String[]::new) diff --git a/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java b/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java index 6240f0c0..1701a10d 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java +++ b/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java @@ -236,7 +236,7 @@ public class VelocityCore implements ReloadablePlugin { String cmd = cmds.remove(0); if (server.getSpectatePort() != 0) { - EventStarter.addSpectateServer(server.getSpectatePort(), cmd); + EventStarter.addSpectateServer(server.getSpectatePort(), cmd, entry.getKey()); } new ServerSwitchCommand(cmd, entry.getKey(), server.getSpectatePort() != 0, cmds.toArray(new String[0])); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java new file mode 100644 index 00000000..a86964fb --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java @@ -0,0 +1,71 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.UserPerm; +import net.kyori.adventure.text.Component; + +import java.util.Arrays; +import java.util.stream.Stream; + +public class AlertChannel extends ChatChannel { + + @Override + public boolean switchable() { + return false; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_ALERT"); + } + + @Override + public Stream readers() { + return Arrays.stream(Chatter.broadcast().getChatters()); + } + + @Override + public boolean canWrite(Chatter sender) { + if (!sender.user().hasPerm(UserPerm.MODERATION)) { + sender.system("CHAT_NO_WRITE"); + return false; + } + return true; + } + + @Override + protected Component format(Chatter receiver, Chatter sender, String message) { + return receiver.parse(true, new Message("ALERT", message.replace('&', '§'))); + } + + /** + * Alerts are system messages: they are shown even with hidden chat and bypass the chat filter. + */ + @Override + public void send(Chatter sender, String message, boolean mirror) { + if (!canWrite(sender)) return; + + readers().forEach(reader -> reader.sendMessage(format(reader, sender, message))); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java new file mode 100644 index 00000000..4bc9d919 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java @@ -0,0 +1,146 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Stream; + +public abstract class ChatChannel { + + /** + * @return the id under which this channel is stored as default channel of a player, null if it cannot be stored + */ + public String persistId() { + return null; + } + + public boolean switchable() { + return true; + } + + /** + * @return the channel name shown in channel related system messages + */ + public abstract Message tag(Chatter receiver); + + /** + * @return the 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. + */ + protected Message icon(Chatter receiver) { + 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(); + + /** + * Checks if the sender is allowed to write into this channel and informs the sender otherwise. + */ + public abstract boolean canWrite(Chatter sender); + + protected abstract Component format(Chatter receiver, Chatter sender, String message); + + /** + * Called after a message was delivered to all readers. + * + * @param mirror false if the message originates from the mirror of this channel and should not be sent back + * @return true if the message was delivered to a receiver outside the game + */ + protected boolean delivered(Chatter sender, String message, boolean mirror) { + return false; + } + + public void audit(SteamwarUser sender, String message) { + AuditLog.createChat(AuditLog.SERVER_NAME_VELOCITY, null, sender, message); + } + + public void send(Chatter sender, String message) { + send(sender, message, true); + } + + public void send(Chatter sender, String message, boolean mirror) { + if (!canWrite(sender)) return; + + SteamwarUser user = sender.user(); + String coloredMessage = user.hasPerm(UserPerm.COLOR_CHAT) ? message.replace('&', '§') : message; + if (ChatFilter.filtered(sender, coloredMessage)) return; + + Map receivers = new LinkedHashMap<>(); + readers().forEach(reader -> receivers.putIfAbsent(reader.user().getId(), reader)); + receivers.putIfAbsent(user.getId(), sender); + + boolean noReceiver = true; + for (Chatter receiver : receivers.values()) { + if (!receiver.chatShown()) continue; + + Component msg = 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(); + } + receiver.sendMessage(msg); + + if (receiver.user().getId() != user.getId()) noReceiver = false; + } + + if (!delivered(sender, coloredMessage, mirror) && noReceiver) { + sender.system("CHAT_NO_RECEIVER"); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java new file mode 100644 index 00000000..51c2841c --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java @@ -0,0 +1,76 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.Punishment; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.commands.PunishmentCommand; + +import java.util.concurrent.TimeUnit; + +public class ChatFilter { + + private ChatFilter() {} + + public static boolean filtered(Chatter sender, String message) { + if (!sender.chatShown()) { + sender.system("CHAT_RECEIVE"); + return true; + } + + if (message.replace("§[a-f0-9klmno]", "").trim().isEmpty()) { + sender.system("CHAT_EMPTY"); + return true; + } + + SteamwarUser user = sender.user(); + if (!user.hasPerm(UserPerm.TEAM) && (message.contains("http:") || message.contains("https:") || message.contains("www."))) { + sender.system("CHAT_NO_LINKS"); + return true; + } + + if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.Mute)) return true; + + if (message.contains("LIXFEL")) { + specialAlert(sender, "Lixfel", "CHAT_LIXFEL_", 3, 6, 11, 12, 15); + } + if (message.contains("YOYONOW")) { + specialAlert(sender, "YoyoNow", "CHAT_YOYONOW_", 3, 6, 11, 12); + } + if (message.contains("CHAOSCAOT")) { + specialAlert(sender, "Chaoscaot", "CHAT_CHAOSCAOT_", 3, 6, 11, 12, 15, 17); + } + + return false; + } + + private static void specialAlert(Chatter sender, String name, String baseMessage, int... delay) { + sender.system("CHAT_LIXFEL_ACTION_BAR"); + for (int i = 0; i < delay.length; i++) { + int finalI = i; + VelocityCore.schedule(() -> sender.prefixless("CHAT_SPECIAL_ALERT", name, sender.user(), new Message(baseMessage + (finalI + 1)))).delay(delay[i], TimeUnit.SECONDS).schedule(); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java new file mode 100644 index 00000000..1921bec8 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java @@ -0,0 +1,179 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.network.packets.server.PingPacket; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.Team; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.network.NetworkSender; +import de.steamwar.velocitycore.util.SteamwarPrefix; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.TextReplacementConfig; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; + +import java.util.Arrays; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class ChatFormat { + + private static final Pattern MENTION = Pattern.compile("@(\\w{1,16})"); + + // Formats that show the team prefix in front of the sender name + private static final Set TEAM_PREFIX_FORMATS = Set.of("CHAT_GLOBAL"); + + private ChatFormat() {} + + /** + * Formats a message with the rank and team prefixes of the sender. The sender part ({@code _SENDER}) + * shows the team of the sender on hover. + */ + public static Component player(String format, Chatter receiver, Chatter sender, Chatter msgReceiver, String message) { + SteamwarUser user = sender.user(); + UserPerm.Prefix prefix = user.prefix(); + + // The team prefix is its own component, so that clicking it suggests /team info + Component teamPrefix = Component.empty(); + if (TEAM_PREFIX_FORMATS.contains(format)) { + if (useSwPrefix(user)) { + teamPrefix = Chatter.SERIALIZER.deserialize("§eS§8W") + .hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM_CLICK", "SteamWar", "§e", "S§8W")))) + .clickEvent(ClickEvent.suggestCommand("/team info SW")) + .append(Component.text(" ")); + } else if (user.getTeam() != 0) { + Team team = Team.byId(user.getTeam()); + teamPrefix = clickableTeam(receiver, team, Chatter.SERIALIZER.deserialize("§" + team.getTeamColor() + team.getTeamKuerzel())).append(Component.text(" ")); + } + } + + String chatColorCode = user.hasPerm(UserPerm.TEAM) ? "§f" : "§7"; + Object[] params = { + user, + msgReceiver == null ? receiver : msgReceiver, + highlightMentions(message, chatColorCode, receiver), + "", + "", + prefix == UserPerm.emptyPrefix ? "§f" : prefix.getColorCode(), + prefix.getChatPrefix().length() == 0 ? "§f" : prefix.getChatPrefix() + " ", + chatColorCode}; + + Component senderPart = withTeamHover(receiver, user, receiver.parse(new Message(format + "_SENDER", params))); + return Component.text().append(teamPrefix).append(senderPart).append(receiver.parse(new Message(format, params))).build(); + } + + /** + * Formats a fight chat message. The fight team name is only hoverable in event fights, where it is the actual team. + * + * @param coloredTeamName the fight team of the sender, null if the sender is not in a fight team + */ + public static Component fight(Chatter receiver, RegisteredServer server, Chatter sender, String coloredTeamName, String senderFormat, String format, String message) { + TextComponent.Builder line = Component.text(); + String nameColor = ""; + if (coloredTeamName != null) { + Component team = Chatter.SERIALIZER.deserialize(coloredTeamName); + Team eventTeam = ChatManager.eventTeam(server, coloredTeamName); + if (eventTeam != null) { + team = clickableTeam(receiver, eventTeam, team); + } + line.append(team).append(Component.text(" ")); + nameColor = coloredTeamName.length() >= 2 && coloredTeamName.charAt(0) == '§' ? coloredTeamName.substring(0, 2) : ""; + } + + line.append(withTeamHover(receiver, sender.user(), receiver.parse(new Message(senderFormat, nameColor, sender.user())))); + line.append(receiver.parse(new Message(format, message))); + return line.build(); + } + + /** + * Shows the team of the user when hovering the component, the SteamWar team for server team members using the SW prefix. + */ + public static Component withTeamHover(Chatter receiver, SteamwarUser user, Component component) { + // Players of the public team (id 0) get no hover + if (user.getTeam() == 0) return component; + if (useSwPrefix(user)) { + return component.hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM", "SteamWar", "§e", "S§8W")))); + } + + return component.hoverEvent(HoverEvent.showText(teamHover(receiver, Team.byId(user.getTeam())))); + } + + /** + * Shows the team on hover and suggests {@code /team info} for it on click. + */ + private static Component clickableTeam(Chatter receiver, Team team, Component component) { + return component + .hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM_CLICK", team.getTeamName(), "§" + team.getTeamColor(), team.getTeamKuerzel())))) + .clickEvent(ClickEvent.suggestCommand("/team info " + team.getTeamKuerzel())); + } + + private static Component teamHover(Chatter receiver, Team team) { + return receiver.parse(new Message("CHAT_HOVER_TEAM", team.getTeamName(), "§" + team.getTeamColor(), team.getTeamKuerzel())); + } + + 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); + } + + /** + * 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; + + 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..f65be1b3 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -0,0 +1,514 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.player.ServerPostConnectEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.linkage.Linked; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.persistent.Subserver; +import de.steamwar.sql.Event; +import de.steamwar.sql.Referee; +import de.steamwar.sql.Team; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserConfig; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.EventStarter; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.listeners.BasicListener; +import de.steamwar.velocitycore.tablist.TablistManager; +import lombok.Getter; +import net.kyori.adventure.text.Component; + +import 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 String ARENAS_KEY = "chat.arenas"; + private static final long REFEREE_CACHE_MILLIS = 60_000; + + @Getter + private static final GlobalChannel global = new GlobalChannel(); + @Getter + private static final ServerTeamChannel serverTeam = new ServerTeamChannel(); + @Getter + private static final AlertChannel alert = new AlertChannel(); + + private static final Map locals = new ConcurrentHashMap<>(); + private static final Map fightTeams = new ConcurrentHashMap<>(); + private static final Map teams = new ConcurrentHashMap<>(); + private static final Set directs = ConcurrentHashMap.newKeySet(); + + private static final Map defaults = new ConcurrentHashMap<>(); + private static final Map persisted = new ConcurrentHashMap<>(); + private static final Set globalOnArena = ConcurrentHashMap.newKeySet(); + // 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<>(); + private static final Map> leftArenas = new ConcurrentHashMap<>(); + private static final Map> hidden = new ConcurrentHashMap<>(); + + private static int refereeEvent = -1; + private static long refereesLoaded = 0; + private static Set referees = Collections.emptySet(); + + public static LocalChannel local(RegisteredServer server) { + locals.keySet().removeIf(ChatManager::unregistered); + return locals.computeIfAbsent(server, LocalChannel::new); + } + + public static FightTeamChannel fightTeam(RegisteredServer server, boolean blue) { + fightTeams.keySet().removeIf(ChatManager::unregistered); + return fightTeams.computeIfAbsent(server, s -> new FightTeamChannel[]{new FightTeamChannel(s, true), new FightTeamChannel(s, false)})[blue ? 0 : 1]; + } + + public static TeamChannel team(int team) { + return teams.computeIfAbsent(team, TeamChannel::new); + } + + public static DirectChannel direct(UUID first, UUID second) { + for (DirectChannel channel : directs) { + if (channel.contains(first) && channel.partner(first).equals(second)) { + return channel; + } + } + + DirectChannel channel = new DirectChannel(first, second); + directs.add(channel); + return channel; + } + + private static boolean unregistered(RegisteredServer server) { + return VelocityCore.getProxy().getServer(server.getServerInfo().getName()).orElse(null) != server; + } + + /** + * @return true for arenas and event spectate servers, which use a fight chat instead of the global chat + */ + public static boolean isArena(RegisteredServer server) { + return Subserver.isArena(Subserver.getSubserver(server.getServerInfo())) || EventStarter.isSpectateServer(server.getServerInfo().getName()); + } + + /** + * @return all servers using a fight chat (arenas and event spectate servers) + */ + public static Stream fightServers() { + return VelocityCore.getProxy().getAllServers().stream().filter(ChatManager::isArena); + } + + /** + * @return the local channel of the server the player is on, null while the player is on no server + */ + public static LocalChannel currentLocal(Player player) { + return currentServer(player).map(ChatManager::local).orElse(null); + } + + static boolean onServer(Chatter chatter, RegisteredServer server) { + Player player = chatter.getPlayer(); + return player != null && server.getPlayersConnected().contains(player); + } + + /** + * Sends the message into the channel or makes it the default channel of the player if there is no message. + */ + public static void sendOrSwitch(Chatter sender, ChatChannel channel, String[] message, String usage) { + if (message.length > 0) { + channel.send(sender, String.join(" ", message)); + } else if (sender instanceof PlayerChatter player) { + switchTo(player, channel); + } else { + sender.system(usage); + } + } + + private static Optional currentServer(Player player) { + return player.getCurrentServer().map(ServerConnection::getServer); + } + + static boolean readsGlobal(Player player) { + UUID uuid = player.getUniqueId(); + return !currentServer(player).map(ChatManager::isArena).orElse(false) || globalOnArena.contains(uuid) || globalOnServer.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 (globalOnServer.add(player.user().getUUID())) { + player.system("CHAT_GLOBAL_AFTER_WRITING", global.tag(player)); + } + } + + /** + * @return the moderators reading the fight chats of the server without being on it + */ + static Stream spies(RegisteredServer server) { + 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 + */ + static boolean isReferee(RegisteredServer server, int userId) { + Subserver subserver = Subserver.getSubserver(server.getServerInfo()); + if (subserver == null || !EventStarter.getEventServer().containsValue(subserver)) return false; + + Event event = Event.get(); + return event != null && referees(event.getEventID()).contains(userId); + } + + private static synchronized Set referees(int eventId) { + if (eventId != refereeEvent || System.currentTimeMillis() - refereesLoaded > REFEREE_CACHE_MILLIS) { + referees = Referee.get(eventId); + refereeEvent = eventId; + refereesLoaded = System.currentTimeMillis(); + } + return referees; + } + + private static Set hidden(UUID uuid) { + return hidden.computeIfAbsent(uuid, u -> { + Set channels = ConcurrentHashMap.newKeySet(); + String config = UserConfig.getConfig(u, HIDDEN_KEY); + if (config != null && !config.isEmpty()) { + channels.addAll(Arrays.asList(config.split(","))); + } + return channels; + }); + } + + /** + * @return false if the player left the channel with the given persist id as reader + */ + static boolean reads(Chatter chatter, String persistId) { + Player player = chatter.getPlayer(); + return player == null || !hidden(player.getUniqueId()).contains(persistId); + } + + private static void saveHidden(UUID uuid) { + Set channels = hidden(uuid); + UserConfig.updatePlayerConfig(uuid, HIDDEN_KEY, channels.isEmpty() ? null : String.join(",", channels)); + } + + /** + * @return true if the channel is the fight chat of an arena the player is not on, which moderators can read without joining the arena + */ + private static boolean isForeignArena(PlayerChatter player, ChatChannel channel) { + return channel instanceof LocalChannel local && local.isArena() && !onServer(player, local.getServer()); + } + + public static void join(PlayerChatter player, ChatChannel channel) { + UUID uuid = player.user().getUUID(); + if (isForeignArena(player, channel)) { + 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; + } + + 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)); + } + + 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(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; + } + + String id = channel.persistId(); + if (id.equals(getDefault(player).persistId())) { + player.system("CHAT_CHANNEL_LEAVE_DEFAULT"); + return; + } + + // 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; + } + + if (hidden(uuid).add(id)) { + saveHidden(uuid); + } + player.system("CHAT_CHANNEL_LEFT", channel.tag(player)); + } + + public static FightInfoPacket getFightInfo(RegisteredServer server) { + return TablistManager.getFightInfo(server); + } + + /** + * @return the team of an event fight on the server whose colored name is shown, null if the server runs no event fight + */ + static Team eventTeam(RegisteredServer server, String coloredTeamName) { + Subserver subserver = Subserver.getSubserver(server.getServerInfo()); + if (subserver == null) return null; + + String kuerzel = coloredTeamName.replaceAll("§.", ""); + for (Map.Entry entry : new ArrayList<>(EventStarter.getEventServer().entrySet())) { + if (entry.getValue() != subserver) continue; + + Team team = Team.byId(entry.getKey()); + if (team != null && team.getTeamKuerzel().equals(kuerzel)) return team; + } + return null; + } + + /** + * Updates the fight teams of an arena. Players who just joined a fight team write into the fight chat by default. + */ + public static void updateFightInfo(RegisteredServer server, FightInfoPacket previous, FightInfoPacket packet) { + + Set joined = new HashSet<>(packet.getBluePlayers()); + joined.addAll(packet.getRedPlayers()); + if (previous != null) { + previous.getBluePlayers().forEach(joined::remove); + previous.getRedPlayers().forEach(joined::remove); + } + + for (Player player : server.getPlayersConnected()) { + if (joined.contains(SteamwarUser.get(player.getUniqueId()).getId())) { + setDefault(Chatter.of(player), local(server)); + } + } + } + + /** + * @return the channel the receiver writes into by default; non-player chatters (console, Discord) use the global chat + */ + public static ChatChannel getDefault(Chatter chatter) { + Player player = chatter.getPlayer(); + if (player == null) return global; + + return defaults.computeIfAbsent(player.getUniqueId(), uuid -> initialDefault(player, currentServer(player).orElse(null))); + } + + private static ChatChannel initialDefault(Player player, RegisteredServer server) { + if (server != null && isArena(server)) return local(server); + return resolvePersisted(player, server); + } + + private static ChatChannel resolvePersisted(Player player, RegisteredServer server) { + String id = persisted.computeIfAbsent(player.getUniqueId(), uuid -> Optional.ofNullable(UserConfig.getConfig(uuid, CONFIG_KEY)).orElse(global.persistId())); + SteamwarUser user = SteamwarUser.get(player.getUniqueId()); + return switch (id) { + case "local" -> server != null && !isArena(server) ? local(server) : global; + case "team" -> user.getTeam() != 0 ? team(user.getTeam()) : global; + case "serverteam" -> user.hasPerm(UserPerm.TEAM) ? serverTeam : global; + default -> global; + }; + } + + /** + * Makes the channel the default channel of the player because the player chose it. + */ + public static void switchTo(PlayerChatter player, ChatChannel channel) { + if (!channel.switchable() || isForeignArena(player, channel)) { + player.system("CHAT_CHANNEL_NOT_SWITCHABLE"); + return; + } + if (!channel.canWrite(player)) return; + + UUID uuid = player.user().getUUID(); + if (channel == global && currentServer(player.getPlayer()).map(ChatManager::isArena).orElse(false)) { + globalOnArena.add(uuid); + } else if (channel != global) { + globalOnArena.remove(uuid); + } + + String id = channel.persistId(); + if (id != null && !id.equals(persisted.get(uuid))) { + persisted.put(uuid, id); + UserConfig.updatePlayerConfig(uuid, CONFIG_KEY, id); + } + if (id != null && hidden(uuid).remove(id)) { + saveHidden(uuid); + } + + defaults.put(uuid, channel); + notifyDefault(player, channel); + } + + private static void setDefault(PlayerChatter player, ChatChannel channel) { + UUID uuid = player.user().getUUID(); + if (channel != global) globalOnArena.remove(uuid); + + ChatChannel previous = defaults.put(uuid, channel); + if (announced.add(uuid)) { + notifyDefault(player, channel); + } else if (previous != channel && !player.parseToPlain(previous.tag(player)).equals(player.parseToPlain(channel.tag(player)))) { + notifyDefault(player, channel); + } + } + + /** + * Tells the player which channel they write into, showing the channel icon with its name on hover. + */ + public static void notifyDefault(PlayerChatter player, ChatChannel channel) { + player.sendMessage(Component.text() + .append(channel.iconComponent(player, "§a")) + .append(Component.text(" ")) + .append(player.parse(new Message("CHAT_CHANNEL_DEFAULT"))) + .build()); + } + + /** + * Handles a chat message the player typed without a command. + */ + public static void chat(PlayerChatter sender, String message) { + ChatChannel channel = getDefault(sender); + if (channel instanceof TeamChannel team && team.getTeam() != sender.user().getTeam()) { + channel = resolvePersisted(sender.getPlayer(), currentServer(sender.getPlayer()).orElse(null)); + setDefault(sender, channel); + } + + if (message.startsWith("+")) { + channel = quickChannel(sender, channel); + message = message.substring(1); + } + + channel.send(sender, message); + channel.audit(sender.user(), message); + } + + /** + * The channel a message prefixed with '+' is sent to: the local chat when writing into the global chat, + * the fight team chat when writing into the local/fight chat. + */ + private static ChatChannel quickChannel(PlayerChatter sender, ChatChannel current) { + RegisteredServer server = currentServer(sender.getPlayer()).orElse(null); + if (server == null) return current; + + if (current instanceof LocalChannel local) { + FightTeamChannel blue = fightTeam(server, true); + if (blue.isMember(sender)) return blue; + + FightTeamChannel red = fightTeam(server, false); + if (red.isMember(sender)) return red; + + // Spectators on fight servers stay in the fight chat + return local.isArena() ? local : global; + } + + return local(server); + } + + @Subscribe + public void onServerPostConnect(ServerPostConnectEvent event) { + Player player = event.getPlayer(); + RegisteredServer server = currentServer(player).orElse(null); + if (server == null) return; + + UUID uuid = player.getUniqueId(); + globalOnArena.remove(uuid); + globalOnServer.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); + globalOnServer.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/chat/DirectChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java new file mode 100644 index 00000000..5b89b8ba --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java @@ -0,0 +1,128 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.IgnoreSystem; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.velocitycore.VelocityCore; +import net.kyori.adventure.text.Component; + +import java.util.UUID; +import java.util.stream.Stream; + +import static de.steamwar.persistent.Storage.lastChats; + +/** + * A temporary channel between two players, closed as soon as one of them goes offline. + */ +public class DirectChannel extends ChatChannel { + + private final UUID first; + private final UUID second; + + DirectChannel(UUID first, UUID second) { + this.first = first; + this.second = second; + } + + public boolean contains(UUID uuid) { + return first.equals(uuid) || second.equals(uuid); + } + + public UUID partner(UUID uuid) { + return first.equals(uuid) ? second : first; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_MSG", SteamwarUser.get(partner(receiver.user().getUUID()))); + } + + @Override + public boolean switchable() { + return false; + } + + @Override + protected Message icon(Chatter receiver) { + return new Message("CHAT_ICON_MSG"); + } + + @Override + protected String suggestion(Chatter receiver) { + return "/msg " + SteamwarUser.get(partner(receiver.user().getUUID())).getUserName() + " "; + } + + @Override + public Stream readers() { + return Stream.of(first, second) + .map(uuid -> VelocityCore.getProxy().getPlayer(uuid).orElse(null)) + .filter(player -> player != null) + .map(Chatter::of); + } + + @Override + public boolean canWrite(Chatter sender) { + UUID partner = partner(sender.user().getUUID()); + if (VelocityCore.getProxy().getPlayer(partner).isEmpty()) { + sender.system("MSG_OFFLINE"); + return false; + } + + if (IgnoreSystem.isIgnored(partner, sender.user().getUUID())) { + sender.system("MSG_IGNORED"); + return false; + } + return true; + } + + @Override + protected Component format(Chatter receiver, Chatter sender, String message) { + 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 + 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..49b75cfa --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java @@ -0,0 +1,118 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import lombok.Getter; +import net.kyori.adventure.text.Component; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +/** + * The team chat of the blue or red team of a fight. + */ +public class FightTeamChannel extends ChatChannel { + + @Getter + private final RegisteredServer server; + private final boolean blue; + + FightTeamChannel(RegisteredServer server, boolean blue) { + this.server = server; + this.blue = blue; + } + + @Override + public boolean switchable() { + return false; + } + + private List members() { + FightInfoPacket info = ChatManager.getFightInfo(server); + if (info == null) return Collections.emptyList(); + return blue ? info.getBluePlayers() : info.getRedPlayers(); + } + + private String teamName() { + FightInfoPacket info = ChatManager.getFightInfo(server); + if (info == null) return ""; + return blue ? info.getBlueName() : info.getRedName(); + } + + public boolean isMember(Chatter chatter) { + return members().contains(chatter.user().getId()); + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_FIGHT_TEAM", server.getServerInfo().getName(), teamName()); + } + + @Override + protected Message icon(Chatter receiver) { + String teamName = teamName(); + String color = teamName.length() >= 2 && teamName.charAt(0) == '§' ? teamName.substring(0, 2) : "§7"; + if (ChatManager.onServer(receiver, server)) { + return new Message("CHAT_ICON_FIGHT_TEAM", color); + } + return new Message("CHAT_ICON_FIGHT_TEAM_FOREIGN", color, server.getServerInfo().getName()); + } + + @Override + 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(); + return Stream.concat( + server.getPlayersConnected().stream().map(Chatter::of).filter(chatter -> members.contains(chatter.user().getId())), + ChatManager.spies(server)); + } + + @Override + public boolean canWrite(Chatter sender) { + if (!isMember(sender) || !ChatManager.onServer(sender, server)) { + sender.system("CHAT_FIGHT_NO_TEAM"); + return false; + } + return true; + } + + @Override + protected Component format(Chatter receiver, Chatter sender, String message) { + return ChatFormat.fight(receiver, server, sender, teamName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_TEAM", ChatFormat.highlightMentions(message, "§f", receiver)); + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createChat(server.getServerInfo().getName(), null, sender, "+" + message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java new file mode 100644 index 00000000..67730da7 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java @@ -0,0 +1,74 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.velocitycore.discord.DiscordBot; +import net.kyori.adventure.text.Component; + +import java.util.stream.Stream; + +public class GlobalChannel extends ChatChannel { + + @Override + public String persistId() { + return "global"; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_GLOBAL"); + } + + @Override + protected Message icon(Chatter receiver) { + return new Message("CHAT_ICON_GLOBAL"); + } + + @Override + 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()))); + } + + @Override + public boolean canWrite(Chatter sender) { + return true; + } + + @Override + protected Component format(Chatter receiver, Chatter sender, String message) { + return ChatFormat.player("CHAT_GLOBAL", receiver, sender, null, message); + } + + @Override + protected boolean delivered(Chatter sender, String message, boolean mirror) { + if (mirror) { + DiscordBot.withBot(bot -> bot.getIngameChat().sendMessage(format(bot.getIngameChat(), sender, message))); + } + return true; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java new file mode 100644 index 00000000..e4d6c131 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java @@ -0,0 +1,141 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.network.packets.server.ChatRecordPacket; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.network.NetworkSender; +import lombok.Getter; +import net.kyori.adventure.text.Component; + +import java.util.stream.Stream; + +/** + * The chat of all players on one server. On arenas this is the fight chat. + */ +public class LocalChannel extends ChatChannel { + + @Getter + private final RegisteredServer server; + + LocalChannel(RegisteredServer server) { + this.server = server; + } + + public boolean isArena() { + return ChatManager.isArena(server); + } + + @Override + public String persistId() { + return "local"; + } + + @Override + public Message tag(Chatter receiver) { + if (isArena()) { + return new Message("CHAT_CHANNEL_ARENA"); + } + 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"); + + // Moderators reading an arena they are not on need to know which arena it is + if (ChatManager.onServer(receiver, server)) { + return new Message("CHAT_ICON_FIGHT"); + } + return new Message("CHAT_ICON_FIGHT_FOREIGN", server.getServerInfo().getName()); + } + + @Override + 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); + if (isArena()) { + players = Stream.concat(players, ChatManager.spies(server)); + } + return players; + } + + @Override + public boolean canWrite(Chatter sender) { + // Moderators can write into fight chats they are not on (/chat msg) + if (!ChatManager.onServer(sender, server) && !(isArena() && sender.user().hasPerm(UserPerm.MODERATION))) { + sender.system("CHAT_NO_WRITE"); + return false; + } + return true; + } + + @Override + protected Component format(Chatter receiver, Chatter sender, String message) { + if (!isArena()) { + return ChatFormat.player("CHAT_GLOBAL", receiver, sender, null, message); + } + + String highlighted = ChatFormat.highlightMentions(message, "§7", receiver); + FightInfoPacket info = ChatManager.getFightInfo(server); + int id = sender.user().getId(); + if (info != null && info.getBluePlayers().contains(id)) { + return ChatFormat.fight(receiver, server, sender, info.getBlueName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_PARTICIPANT", highlighted); + } else if (info != null && info.getRedPlayers().contains(id)) { + return ChatFormat.fight(receiver, server, sender, info.getRedName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_PARTICIPANT", highlighted); + } else if (ChatManager.isReferee(server, id)) { + return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_REFEREE_SENDER", "CHAT_FIGHT_REFEREE", highlighted); + } + return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_SPECTATOR_SENDER", "CHAT_FIGHT_SPECTATOR", highlighted); + } + + @Override + protected boolean delivered(Chatter sender, String message, boolean mirror) { + if (isArena()) { + // Arenas never display chat from Velocity, they only record it for replays + NetworkSender.send(server, new ChatRecordPacket(sender.user().getId(), message)); + } + return false; + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createChat(server.getServerInfo().getName(), null, sender, message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java new file mode 100644 index 00000000..1e23d787 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java @@ -0,0 +1,87 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.discord.DiscordBot; +import net.kyori.adventure.text.Component; + +import java.util.Arrays; +import java.util.stream.Stream; + +public class ServerTeamChannel extends ChatChannel { + + @Override + public String persistId() { + return "serverteam"; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_SERVERTEAM"); + } + + @Override + protected Message icon(Chatter receiver) { + return new Message("CHAT_ICON_SERVERTEAM"); + } + + @Override + protected String suggestion(Chatter receiver) { + return "/stc "; + } + + @Override + public Stream readers() { + return Arrays.stream(Chatter.serverteam().getChatters()).filter(chatter -> ChatManager.reads(chatter, persistId())); + } + + @Override + public boolean canWrite(Chatter sender) { + if (!sender.user().hasPerm(UserPerm.TEAM)) { + sender.system("CHAT_NO_WRITE"); + return false; + } + return true; + } + + @Override + protected Component format(Chatter receiver, Chatter sender, String message) { + return ChatFormat.player("CHAT_SERVERTEAM", receiver, sender, null, message); + } + + @Override + protected boolean delivered(Chatter sender, String message, boolean mirror) { + if (mirror) { + DiscordBot.withBot(bot -> bot.getServerTeamChat().sendMessage(ChatFormat.player("CHAT_GLOBAL", bot.getServerTeamChat(), sender, null, message))); + } + return true; + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createCommand(AuditLog.SERVER_NAME_VELOCITY, null, sender, "/stc " + message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java new file mode 100644 index 00000000..4603c3d5 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java @@ -0,0 +1,90 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.velocitycore.commands.StreamingCommand; +import lombok.Getter; +import net.kyori.adventure.text.Component; + +import java.util.stream.Stream; + +public class TeamChannel extends ChatChannel { + + @Getter + private final int team; + + TeamChannel(int team) { + this.team = team; + } + + @Override + public String persistId() { + return "team"; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_TEAM"); + } + + @Override + protected Message icon(Chatter receiver) { + return new Message("CHAT_ICON_TEAM"); + } + + @Override + protected String suggestion(Chatter receiver) { + return "/tc "; + } + + @Override + public Stream readers() { + return Chatter.allPlayers() + .map(Chatter::of) + .filter(chatter -> chatter.user().getTeam() == team) + .filter(StreamingCommand::isNotStreaming) + .filter(chatter -> ChatManager.reads(chatter, persistId())) + .map(Chatter.class::cast); + } + + @Override + public boolean canWrite(Chatter sender) { + if (sender.user().getTeam() != team) { + sender.system("TC_NO_TEAM"); + return false; + } + return true; + } + + @Override + protected Component format(Chatter receiver, Chatter sender, String message) { + return ChatFormat.player("CHAT_TEAM", receiver, sender, null, message); + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createSensitiveCommand(AuditLog.SERVER_NAME_VELOCITY, null, sender, "/tc " + message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java index ea846789..552691d2 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java @@ -23,6 +23,7 @@ import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.chat.ChatManager; import de.steamwar.velocitycore.discord.DiscordBot; @Linked @@ -40,7 +41,7 @@ public class AlertCommand extends SWCommand { } String s = String.join(" ", message); - Chatter.broadcast().system("ALERT", s.replace('&', '§')); + ChatManager.getAlert().send(sender, s); if ("-discord".equals(sendToDiscord)) { DiscordBot.withBot(bot -> bot.getAnnouncementChannel().send(s)); diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java index 048e3125..67eadd80 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java @@ -19,6 +19,7 @@ package de.steamwar.velocitycore.commands; +import com.velocitypowered.api.proxy.server.RegisteredServer; import de.steamwar.command.PreviousArguments; import de.steamwar.command.SWCommand; import de.steamwar.command.TypeMapper; @@ -46,6 +47,7 @@ import de.steamwar.velocitycore.util.BauLock; import java.io.File; import java.util.Collection; +import java.util.UUID; import java.util.function.Consumer; @Linked @@ -72,6 +74,7 @@ public class BauCommand extends SWCommand { @Register(value = "addmember", description = "BAU_ADDMEMBER_USAGE") public void addmember(Chatter sender, @Validator("addMemberTarget") SteamwarUser target) { BauweltMember.addMember(sender.user().getUUID(), target.getUUID()); + sendBaumemberUpdate(sender.user().getUUID()); sender.system("BAU_ADDMEMBER_ADDED"); Chatter.of(target.getUUID()).system("BAU_ADDMEMBER_ADDED_TARGET", sender); } @@ -161,10 +164,7 @@ public class BauCommand extends SWCommand { withMember(owner, user, target -> { setter.accept(target); - Bauserver bauserver = Bauserver.get(owner.user().getUUID()); - if (bauserver != null) { - bauserver.getRegisteredServer().getPlayersConnected().stream().findAny().ifPresent(player -> NetworkSender.send(player, new BaumemberUpdatePacket())); - } + sendBaumemberUpdate(owner.user().getUUID()); Chatter.of(user.getUUID()).system("BAU_MEMBER_SET_TARGET", owner, new Message(permName)); owner.system("BAU_MEMBER_SET", new Message(permName)); @@ -184,9 +184,7 @@ public class BauCommand extends SWCommand { } }); - if (bauserver != null) { - bauserver.getRegisteredServer().getPlayersConnected().stream().findAny().ifPresent(player -> NetworkSender.send(player, new BaumemberUpdatePacket())); - } + sendBaumemberUpdate(owner.user().getUUID()); try { deletePlayerData(owner, user); @@ -261,6 +259,7 @@ public class BauCommand extends SWCommand { } SubserverSystem.deleteFolder(VelocityCore.local, world); + SubserverSystem.deleteFolder(VelocityCore.local, ServerStarter.checkpointDir(version.getServerDirectory("Bau"), String.valueOf(sender.user().getId())).getPath()); sender.system("BAU_DELETE_DELETED"); }).schedule(); @@ -288,6 +287,26 @@ public class BauCommand extends SWCommand { BauLock.setLocked(sender, BauLockState.OPEN); } + /** + * Tells the owner's Bauserver and the owner's own dev server to drop their cached {@link BauweltMember}s, + * since those run in separate processes and would otherwise keep serving stale permissions. + * Plugin messages need a connected player, so servers without players are skipped. + */ + private static void sendBaumemberUpdate(UUID owner) { + Bauserver bauserver = Bauserver.get(owner); + if (bauserver != null) { + sendBaumemberUpdate(bauserver.getRegisteredServer()); + } + RegisteredServer devServer = DevCommand.getDevServers().get(SteamwarUser.get(owner).getUserName().toLowerCase()); + if (devServer != null) { + sendBaumemberUpdate(devServer); + } + } + + private static void sendBaumemberUpdate(RegisteredServer server) { + server.getPlayersConnected().stream().findAny().ifPresent(player -> NetworkSender.send(player, new BaumemberUpdatePacket())); + } + private static void withMember(Chatter owner, SteamwarUser member, Consumer function) { if (member == null) { owner.system("UNKNOWN_PLAYER"); 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..106042f4 --- /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 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; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.chat.ChatChannel; +import de.steamwar.velocitycore.chat.ChatManager; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@Linked +public class ChatCommand extends SWCommand { + + public ChatCommand() { + super("chat"); + } + + @Register(description = "CHAT_USAGE") + public void current(PlayerChatter sender) { + ChatManager.notifyDefault(sender, ChatManager.getDefault(sender)); + } + + @Register + public void switchTo(PlayerChatter sender, ChatChannel channel) { + ChatManager.switchTo(sender, channel); + } + + @Register + public void joinOrLeave(PlayerChatter sender, ChatChannel channel, @StaticValue(value = {"leave", "join"}, allowISE = true) boolean join) { + if (join) { + ChatManager.join(sender, channel); + } else { + ChatManager.leave(sender, channel); + } + } + + @Register(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)); + } + + @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<>() { + @Override + public ChatChannel map(Chatter sender, PreviousArguments previousArguments, String s) { + return switch (s.toLowerCase()) { + case "global" -> ChatManager.getGlobal(); + case "local" -> sender.getPlayer() == null ? null : ChatManager.currentLocal(sender.getPlayer()); + case "team", "tc" -> sender.user().getTeam() != 0 ? ChatManager.team(sender.user().getTeam()) : null; + case "serverteam", "stc" -> sender.user().hasPerm(UserPerm.TEAM) ? ChatManager.getServerTeam() : null; + default -> arena(sender, s); + }; + } + + @Override + public boolean validate(Chatter sender, ChatChannel value, MessageSender messageSender) { + return !messageSender.send(value == null, "CHAT_CHANNEL_UNKNOWN"); + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + List channels = new ArrayList<>(List.of("global", "local")); + if (sender.user().getTeam() != 0) channels.add("team"); + if (sender.user().hasPerm(UserPerm.TEAM)) channels.add("serverteam"); + if (sender.user().hasPerm(UserPerm.MODERATION)) { + ChatManager.fightServers().map(server -> server.getServerInfo().getName()).forEach(channels::add); + } + return channels; + } + }; + } + + /** + * Moderators can address the fight chat of every fight server by its server name. + */ + private static ChatChannel arena(Chatter sender, String name) { + if (!sender.user().hasPerm(UserPerm.MODERATION)) return null; + + return ChatManager.fightServers() + .filter(server -> server.getServerInfo().getName().equalsIgnoreCase(name)) + .findFirst() + .map(ChatManager::local) + .orElse(null); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/CheckCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/CheckCommand.java index 289f22dc..b130a2d7 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/CheckCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/CheckCommand.java @@ -156,8 +156,7 @@ public class CheckCommand extends SWCommand { } SchematicNode schem = SchematicNode.getSchematicNode(Integer.parseInt(schemID)); - GameModeConfig gameModeConfig = ArenaMode.getBySchemType(schem.getSchemtype()); - if (!gameModeConfig.Checkers.isEmpty() && !gameModeConfig.Checkers.contains(sender.user().getId())) { + if (!mayCheck(sender.user(), schem)) { sender.system("CHECK_SCHEMATIC_EVENT"); return; } @@ -313,60 +312,6 @@ public class CheckCommand extends SWCommand { } private void accept() { - // TODO: This Code is only for the WGS and because YoyoNow is not available this can be removed or changed after the WGS! - if (schematic.getSchemtype().toDB().equals("cwargearseason26")) { - int userId = schematic.getOwner(); - SteamwarUser user = SteamwarUser.byId(userId); - int teamId = user.getTeam(); - - SchematicNode teamFolder = SchematicNode.getSchematicNodeInNode(172325) - .stream() - .filter(schematicNode -> schematicNode.getName().startsWith(teamId + "_")) - .findFirst() - .orElse(null); - if (teamFolder == null) { - internalAccept(); - return; - } - - // Remove any added players from the schematic in the folder - for (SchematicNode schematicNode : SchematicNode.getSchematicNodeInNode(teamFolder.getNodeId())) { - for (NodeMember nodeMember : NodeMember.getNodeMembers(schematicNode.getNodeId())) { - nodeMember.delete(); - } - } - - // Copy Schem into team folder of -1 user - String name = DateTimeFormatter.ofPattern("yyyy.MM.dd_HH:mm:ss").format(schematic.getLastUpdate().toLocalDateTime()); - NodeData data = NodeData.getLatest(schematic); - SchematicNode node = SchematicNode.createSchematic(-1, name, teamFolder.getNodeId()); - NodeData.saveFromStream(node, data.schemData(false), data.getNodeFormat()); - - // Accept the team folder schematic and set other to Normal as well as adding the original owner on the schematic - node.setSchemtype(GameModeConfig.getBySchematicType(schematic.getSchemtype()).Schematic.Type); - NodeMember.createNodeMember(node.getNodeId(), schematic.getOwner()); - for (NodeMember nodeMember : NodeMember.getNodeMembers(schematic.getId())) { - NodeMember.createNodeMember(node.getNodeId(), nodeMember.getMember()); - } - - // Conclude by setting send in schematic to normal and broadcast - concludeCheckSession("freigegeben", SchematicType.Normal, () -> { - Chatter owner = Chatter.of(SteamwarUser.byId(schematic.getOwner()).getUUID()); - owner.withPlayerOrOffline( - player -> owner.system("CHECK_ACCEPTED", schematic.getSchemtype().name(), schematic.getName()), - () -> DiscordAlert.send(owner, Color.GREEN, new Message("DC_TITLE_SCHEMINFO"), new Message("DC_SCHEM_ACCEPT", schematic.getName()), true) - ); - notifyTeam(new Message("CHECK_ACCEPTED_TEAM", schematic.getName(), owner.user().getUserName())); - - return owner.getPlayer() != null; - }); - return; - } - - internalAccept(); - } - - private void internalAccept() { concludeCheckSession("freigegeben", GameModeConfig.getBySchematicType(schematic.getSchemtype()).Schematic.Type, () -> { Chatter owner = Chatter.of(SteamwarUser.byId(schematic.getOwner()).getUUID()); owner.withPlayerOrOffline( 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..96f7eb2b --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java @@ -0,0 +1,43 @@ +/* + * 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) { + ChatManager.sendOrSwitch(sender, ChatManager.getGlobal(), message, "GLOBAL_USAGE"); + if (message.length > 0 && sender instanceof PlayerChatter player) { + ChatManager.readGlobalAfterWriting(player); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java index 40f48fbf..b52a014f 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java @@ -22,7 +22,8 @@ package de.steamwar.velocitycore.commands; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.PlayerChatter; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatChannel; +import de.steamwar.velocitycore.chat.ChatManager; @Linked public class LocalCommand extends SWCommand { @@ -31,8 +32,11 @@ public class LocalCommand extends SWCommand { super("local", "bc", "bauchat"); } - @Register - public void genericCommand(PlayerChatter player, String... message) { - ChatListener.localChat(player, String.join(" ", message)); + @Register(description = "CHAT_BC_USAGE") + public void genericCommand(PlayerChatter sender, String... message) { + ChatChannel channel = ChatManager.currentLocal(sender.getPlayer()); + if (channel == null) return; + + ChatManager.sendOrSwitch(sender, channel, message, "CHAT_BC_USAGE"); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java index 622aa2dc..4db12170 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java @@ -22,12 +22,9 @@ package de.steamwar.velocitycore.commands; import com.velocitypowered.api.proxy.Player; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; -import de.steamwar.messages.Chatter; -import de.steamwar.messages.ChatterGroup; import de.steamwar.messages.PlayerChatter; -import de.steamwar.sql.IgnoreSystem; import de.steamwar.velocitycore.VelocityCore; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatManager; import static de.steamwar.persistent.Storage.lastChats; @@ -49,15 +46,7 @@ public class MsgCommand extends SWCommand { return; } - if (IgnoreSystem.isIgnored(target.getUniqueId(), sender.user().getUUID())) { - sender.system("MSG_IGNORED"); - return; - } - - Chatter receiver = Chatter.of(target); - ChatListener.sendChat(sender, new ChatterGroup(sender, receiver), "CHAT_MSG", receiver, String.join(" ", args)); - lastChats.put(sender.getPlayer(), target); - lastChats.put(target, sender.getPlayer()); + ChatManager.direct(sender.user().getUUID(), target.getUniqueId()).send(sender, String.join(" ", args)); } public static void remove(Player player) { diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ReplayCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ReplayCommand.java index ae276519..3f5802c5 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/ReplayCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ReplayCommand.java @@ -57,9 +57,6 @@ public class ReplayCommand extends SWCommand { @Register public void genericCommand(PlayerChatter sender, @OptionalValue("") String optionalMap) { if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.NoFightServer)) return; - if (!sender.user().hasPerm(UserPerm.TEAM)) return; - - new SWStreamInv<>(sender, new Message("REPLAY_TITLE"), (click, fight) -> { startReplay(sender, click.isShiftClick(), optionalMap, fight); @@ -68,7 +65,6 @@ public class ReplayCommand extends SWCommand { private void startReplay(PlayerChatter sender, boolean isAdmin, String optionalMap, Fight fight) { if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.NoFightServer)) return; - if (!sender.user().hasPerm(UserPerm.TEAM)) return; GameModeConfig mode = ArenaMode.getBySchemType(fight.getSchemType()); ServerStarter starter = new ServerStarter().replay(fight.getFightID()).blueLeader(sender.getPlayer()); diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java index 0b04183e..024c118d 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java @@ -23,7 +23,7 @@ import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; import de.steamwar.sql.UserPerm; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatManager; @Linked public class ServerTeamchatCommand extends SWCommand { @@ -33,7 +33,7 @@ public class ServerTeamchatCommand extends SWCommand { } @Register(description = "STC_USAGE") - public void genericCommand(Chatter sender, @ErrorMessage(value = "STC_USAGE", allowEAs = false) String... message) { - ChatListener.sendChat(sender, Chatter.serverteam(), "CHAT_SERVERTEAM", null, String.join(" ", message)); + public void genericCommand(Chatter sender, String... message) { + ChatManager.sendOrSwitch(sender, ChatManager.getServerTeam(), message, "STC_USAGE"); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/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"); } } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java index e9b1d4d2..39de5457 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java @@ -22,9 +22,7 @@ package de.steamwar.velocitycore.commands; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; -import de.steamwar.messages.ChatterGroup; -import de.steamwar.sql.SteamwarUser; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatManager; @Linked public class TeamchatCommand extends SWCommand { @@ -34,14 +32,13 @@ public class TeamchatCommand extends SWCommand { } @Register(description = "TC_USAGE") - public void genericCommand(Chatter sender, @ErrorMessage(value = "TC_USAGE", allowEAs = false) String... args) { - SteamwarUser user = sender.user(); - - if (user.getTeam() == 0) { + public void genericCommand(Chatter sender, String... args) { + int team = sender.user().getTeam(); + if (team == 0) { sender.system("TC_NO_TEAM"); return; } - ChatListener.sendChat(sender, new ChatterGroup(Chatter.allStream().filter(p -> p.user().getTeam() == user.getTeam()).filter(StreamingCommand::isNotStreaming)), "CHAT_TEAM", null, String.join(" ", args)); + ChatManager.sendOrSwitch(sender, ChatManager.team(team), args, "TC_USAGE"); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java b/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java index 7e7dba1e..4b557bb7 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java @@ -21,6 +21,7 @@ package de.steamwar.velocitycore.discord; import de.steamwar.command.SWCommand; import de.steamwar.messages.Chatter; +import de.steamwar.velocitycore.chat.ChatManager; import de.steamwar.sql.Event; import de.steamwar.velocitycore.VelocityCore; import de.steamwar.velocitycore.discord.channels.*; @@ -181,9 +182,9 @@ public class DiscordBot { // There is a hard limit of 30 messages per minute to send as a webhook, thus with 8 webhooks we can send // 240 messages per minute. Which means 4 every second. I looked at the WGS fights and there were around // ~70 in a short burst and then rather long no new message. - ingameChat = new DiscordChatRoom(config.channel("ingame"), "CHAT_DISCORD_GLOBAL", Chatter::globalChat, 8); + ingameChat = new DiscordChatRoom(config.channel("ingame"), ChatManager.getGlobal(), 8); // 60 messages per minute should be enough for the server team! - serverTeamChat = new DiscordChatRoom(config.channel("serverteam"), "CHAT_DISCORD_SERVERTEAM", Chatter::serverteam, 2); + serverTeamChat = new DiscordChatRoom(config.channel("serverteam"), ChatManager.getServerTeam(), 2); VelocityCore.schedule(() -> { try { diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java index ef02ce11..6c69a516 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java @@ -20,22 +20,17 @@ package de.steamwar.velocitycore.discord.channels; import de.steamwar.messages.Chatter; -import de.steamwar.messages.ChatterGroup; import de.steamwar.sql.Punishment; import de.steamwar.sql.SteamwarUser; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatChannel; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import java.util.function.Supplier; - public class DiscordChatRoom extends DiscordChannel { - private final String format; - private final Supplier target; + private final ChatChannel target; - public DiscordChatRoom(String channel, String format, Supplier target, int maxNumberOfWebhooks) { + public DiscordChatRoom(String channel, ChatChannel target, int maxNumberOfWebhooks) { super(channel, maxNumberOfWebhooks); - this.format = format; this.target = target; } @@ -47,6 +42,6 @@ public class DiscordChatRoom extends DiscordChannel { return; } - ChatListener.sendChat(Chatter.of(user.getUUID(), true), target.get(), format, null, event.getMessage().getContentDisplay().replace('§', '&').replace('\n', ' ')); + target.send(Chatter.of(user.getUUID(), true), event.getMessage().getContentDisplay().replace('§', '&').replace('\n', ' '), false); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java index ad522fee..1da67ed6 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java @@ -27,30 +27,18 @@ import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.event.player.TabCompleteEvent; import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.ServerConnection; -import com.velocitypowered.api.proxy.server.ServerInfo; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; -import de.steamwar.messages.ChatterGroup; -import de.steamwar.messages.Message; -import de.steamwar.messages.PlayerChatter; -import de.steamwar.network.packets.server.PingPacket; -import de.steamwar.persistent.Subserver; import de.steamwar.sql.*; import de.steamwar.velocitycore.ArenaMode; import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.chat.ChatManager; import de.steamwar.velocitycore.commands.PunishmentCommand; -import de.steamwar.velocitycore.discord.DiscordBot; -import de.steamwar.velocitycore.network.NetworkSender; -import de.steamwar.velocitycore.util.SteamwarPrefix; -import java.util.Arrays; import java.util.List; import java.util.Set; -import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Collectors; @Linked public class ChatListener extends BasicListener { @@ -138,21 +126,7 @@ public class ChatListener extends BasicListener { return; } - Subserver subserver = Subserver.getSubserver(player); - String serverName = AuditLog.SERVER_NAME_VELOCITY; - if (Subserver.isArena(subserver) && subserver.getServer() == player.getCurrentServer().orElseThrow().getServerInfo()) { - serverName = subserver.getServer().getName(); - localChat(Chatter.of(player), message); - } else if (message.startsWith("+")) { - serverName = player.getCurrentServer() - .map(ServerConnection::getServerInfo) - .map(ServerInfo::getName) - .orElse(serverName); - localChat(Chatter.of(player), message.substring(1)); - } else { - sendChat(Chatter.of(player), Chatter.globalChat(), "CHAT_GLOBAL", null, message); - } - AuditLog.createChat(serverName, null, user, message); + ChatManager.chat(Chatter.of(player), message); } private static boolean isMistypedCommand(Player player, String message) { @@ -172,115 +146,6 @@ public class ChatListener extends BasicListener { return isCommand; } - public static void sendChat(Chatter sender, ChatterGroup receivers, String format, Chatter msgReceiver, String message) { - SteamwarUser user = sender.user(); - final String coloredMessage = user.hasPerm(UserPerm.COLOR_CHAT) ? message.replace('&', '§') : message; - if (chatFilter(sender, coloredMessage)) return; - - boolean useSwPrefix = useSwPrefix(user); - - boolean noReceiver = true; - for (Chatter player : receivers.getChatters()) { - if (player.chatShown()) { - chatToReciever(player, msgReceiver, user, format, coloredMessage, useSwPrefix); - if (sender.user().getId() != player.user().getId()) noReceiver = false; - } - - } - - if (format.equals("CHAT_GLOBAL")) { - DiscordBot.withBot(bot -> chatToReciever(bot.getIngameChat(), msgReceiver, user, format, coloredMessage, useSwPrefix)); - } else if (format.equals("CHAT_SERVERTEAM")) { - DiscordBot.withBot(bot -> chatToReciever(bot.getServerTeamChat(), msgReceiver, user, "CHAT_GLOBAL", coloredMessage, useSwPrefix)); - } else if (noReceiver) { - sender.system("CHAT_NO_RECEIVER"); - } - } - - public static void localChat(PlayerChatter sender, String message) { - if (message.isEmpty()) { - sender.system("CHAT_BC_USAGE"); - return; - } - - if (ChatListener.filteredCommand(sender, message)) return; - - if (!message.startsWith("/") && chatFilter(sender, message)) return; - - sender.getPlayer().spoofChatInput(message); - } - - private static boolean chatFilter(Chatter sender, String message) { - if (!sender.chatShown()) { - sender.system("CHAT_RECEIVE"); - return true; - } - - if (message.replace("§[a-f0-9klmno]", "").trim().isEmpty()) { - sender.system("CHAT_EMPTY"); - return true; - } - - SteamwarUser user = sender.user(); - if (!user.hasPerm(UserPerm.TEAM) && (message.contains("http:") || message.contains("https:") || message.contains("www."))) { - sender.system("CHAT_NO_LINKS"); - return true; - } - - if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.Mute)) return true; - - if (message.contains("LIXFEL")) { - specialAlert(sender, "Lixfel", "CHAT_LIXFEL_", 3, 6, 11, 12, 15); - } - if (message.contains("YOYONOW")) { - specialAlert(sender, "YoyoNow", "CHAT_YOYONOW_", 3, 6, 11, 12); - } - if (message.contains("CHAOSCAOT")) { - specialAlert(sender, "Chaoscaot", "CHAT_CHAOSCAOT_", 3, 6, 11, 12, 15, 17); - } - - return false; - } - - private static void chatToReciever(Chatter receiver, Chatter msgReceiver, SteamwarUser sender, String format, String message, boolean useSwPrefix) { - UserPerm.Prefix prefix = sender.prefix(); - - String teamPrefix = ""; - if (sender.hasPerm(UserPerm.TEAM) && useSwPrefix) { - teamPrefix = "§eS§8W "; - } else if (sender.getTeam() != 0) { - Team team = Team.byId(sender.getTeam()); - teamPrefix = "§" + team.getTeamColor() + team.getTeamKuerzel() + " "; - } - - String chatColorCode = sender.hasPerm(UserPerm.TEAM) ? "§f" : "§7"; - receiver.prefixless(format, - sender, - msgReceiver == null ? receiver : msgReceiver, - highlightMentions(message, chatColorCode, receiver), - teamPrefix, - "", - prefix == UserPerm.emptyPrefix ? "§f" : prefix.getColorCode(), - prefix.getChatPrefix().length() == 0 ? "§f" : prefix.getChatPrefix() + " ", - chatColorCode); - } - - private static boolean useSwPrefix(SteamwarUser user) { - if (!user.hasPerm(UserPerm.TEAM)) { - return false; - } - - UserPerm.Prefix prefix = user.prefix(); - - boolean hasTeamPrefix = user.getTeam() != 0; - boolean hasSwPrefix = prefix != UserPerm.emptyPrefix; - - if (!hasSwPrefix) return false; - if (!hasTeamPrefix) return true; - - return SteamwarPrefix.usesSWPrefix(user); - } - private static boolean filteredCommand(Chatter sender, String message) { String command = message.split(" ", 2)[0]; if (command.startsWith("/") && command.contains(":")) { @@ -290,27 +155,6 @@ public class ChatListener extends BasicListener { return false; } - private static void specialAlert(Chatter sender, String name, String baseMessage, int... delay) { - sender.system("CHAT_LIXFEL_ACTION_BAR"); - for (int i = 0; i < delay.length; i++) { - int finalI = i; - VelocityCore.schedule(() -> sender.prefixless("CHAT_MSG", name, sender.user(), new Message(baseMessage + (finalI + 1)))).delay(delay[i], TimeUnit.SECONDS).schedule(); - } - } - - private static String highlightMentions(String message, String returnColor, Chatter player) { - if (!message.contains("@")) return message; - - String mark = "@" + player.user().getUserName(); - return Arrays.stream(message.split(" ")).map(cur -> { - if (cur.equalsIgnoreCase(mark) && player.getPlayer() != null) { - NetworkSender.send(player.getPlayer(), new PingPacket(player.user().getId())); - return "§e" + cur + returnColor; - } - return cur; - }).collect(Collectors.joining(" ")); - } - @Subscribe public void onTabCompleteResponseEvent(TabCompleteEvent e) { List suggestions = e.getSuggestions(); diff --git a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java index 704acfec..b7ad0d19 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java +++ b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java @@ -24,6 +24,7 @@ import com.velocitypowered.api.proxy.server.RegisteredServer; import de.steamwar.linkage.Linked; import de.steamwar.network.packets.PacketHandler; import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.velocitycore.chat.ChatManager; import de.steamwar.velocitycore.network.NetworkSender; import de.steamwar.velocitycore.network.ServerMetaInfo; import de.steamwar.velocitycore.tablist.TablistManager; @@ -50,7 +51,8 @@ public class FightInfoHandler extends PacketHandler { FightInfoPacket lobbyPacket = packet.withServerName(server.getServerInfo().getName()); - TablistManager.newFightInfo(server, packet); + FightInfoPacket previous = TablistManager.newFightInfo(server, packet); + ChatManager.updateFightInfo(server, previous, packet); lobbys.removeIf(s -> s.getPlayersConnected().isEmpty()); lobbys.forEach(lobby -> NetworkSender.send(lobby, lobbyPacket)); diff --git a/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java index 7d2fc7ae..9631720d 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java @@ -45,9 +45,17 @@ public class TablistManager extends BasicListener implements Disable { private static final Map fightInfos = new HashMap<>(); - public static synchronized void newFightInfo(RegisteredServer info, FightInfoPacket packet) { - fightInfos.put(info, packet); + /** + * @return the previous fight info of the server, null if there was none + */ + public static synchronized FightInfoPacket newFightInfo(RegisteredServer info, FightInfoPacket packet) { + FightInfoPacket previous = fightInfos.put(info, packet); fightInfos.keySet().removeIf(serverInfo -> serverInfo.getPlayersConnected().isEmpty()); + return previous; + } + + public static synchronized FightInfoPacket getFightInfo(RegisteredServer server) { + return fightInfos.get(server); } private final Map tablists = new HashMap<>();