forked from SteamWar/SteamWar
Merge branch 'main' into FightSystem/WaterRemover
This commit is contained in:
@@ -7,8 +7,14 @@
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
steamwar.properties
|
||||
|
||||
# Kotlin
|
||||
.kotlin
|
||||
|
||||
# IntelliJ IDEA
|
||||
*.iml
|
||||
.idea
|
||||
!.idea/copyright
|
||||
!.idea/runConfigurations
|
||||
|
||||
# VSCode
|
||||
bin/
|
||||
|
||||
+3
@@ -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;
|
||||
|
||||
+31
-90
@@ -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<Player, ServerboundMovePlayerPacket, Object> 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<Player, Object, Object> 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<Player, ServerboundSetCreativeModeSlotPacket, Object> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+57
-141
@@ -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<Block> blocks) {
|
||||
Set<PistOrder> orders = new HashSet<>();
|
||||
private void removeMovedPistOrders(List<Block> 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<Location, PistOrder> 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<Location> movedBlocks = new ArrayList<>();
|
||||
private List<Location> brokenBlocks = new ArrayList<>();
|
||||
private Set<Location> 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<Block> 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<Block> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Player> newSpectator = new HashSet<>();
|
||||
Set<Player> newBuilder = new HashSet<>();
|
||||
Bukkit.getOnlinePlayers().forEach(player -> {
|
||||
|
||||
+18
-15
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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<CompositeID>) : CompositeEntity(id) {
|
||||
companion object : CompositeEntityClass<BauweltMember>(BauweltMemberTable) {
|
||||
private val cache = mutableMapOf<Int, BauweltMember>()
|
||||
// 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<Pair<Int, Int>, Optional<BauweltMember>>()
|
||||
|
||||
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<CompositeID>) : 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<CompositeID>) : CompositeEntity(id) {
|
||||
fun isWorld() =
|
||||
world
|
||||
|
||||
fun remove() =
|
||||
fun remove() {
|
||||
val key = bauweltID to memberID
|
||||
useDb {
|
||||
delete()
|
||||
}
|
||||
cache.remove(key)
|
||||
}
|
||||
}
|
||||
@@ -617,6 +617,15 @@ public final class GameModeConfig<M, W> {
|
||||
*/
|
||||
public final List<SchematicType> 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<Integer> Publics;
|
||||
|
||||
/**
|
||||
* Shortcut of the schematic type
|
||||
*
|
||||
@@ -725,6 +734,7 @@ public final class GameModeConfig<M, W> {
|
||||
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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<SchematicNode> publics = SchematicNode.getAllSchematicsOfType(0, Config.GameModeConfig.Schematic.Type.toDB());
|
||||
return publics.get(random.nextInt(publics.size()));
|
||||
return FightSchematic.getPublicSchematic().orElseThrow();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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<SchematicNode> getSchematicList(SchematicNode parent) {
|
||||
if (!publicSchems || parent != null) return super.getSchematicList(parent);
|
||||
List<Integer> 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();
|
||||
}
|
||||
|
||||
+32
-15
@@ -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<SchematicNode> getPublicSchematic() {
|
||||
List<Integer> 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<SchematicNode> 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<SchematicNode> 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> 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()) {
|
||||
|
||||
@@ -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<Player> f) {
|
||||
players.forEach((uuid, fightPlayer) -> {
|
||||
Player player = Bukkit.getPlayer(uuid);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
LOCAL_CHAT = §eLocal §r{0}§8»§7 {1}
|
||||
|
||||
COMMAND_SYSTEM_ERROR = §cError executing the command!
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
LOCAL_CHAT = §eLokal §r{0}§8»§7 {1}
|
||||
|
||||
COMMAND_SYSTEM_ERROR = §cFehler beim Ausführen des Befehls!
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Float> viewRangeWatcher = new EntityDataAccessor<>(17, EntityDataSerializers.FLOAT);
|
||||
|
||||
@@ -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<Player> 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<Object, ItemStack> itemSlots;
|
||||
protected final Map<org.bukkit.inventory.EquipmentSlot, ItemStack> 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<Component> formatDisplayName(String displayName) {
|
||||
return displayName != null ? Optional.of(MutableComponent.create(PlainTextContents.create(displayName))) : Optional.empty();
|
||||
}
|
||||
|
||||
@@ -42,14 +42,8 @@ import java.util.stream.Collectors;
|
||||
|
||||
public class REntityServer implements Listener {
|
||||
|
||||
private static final HashSet<REntity> emptyEntities = new HashSet<>(0);
|
||||
private static final HashSet<Player> emptyPlayers = new HashSet<>(0);
|
||||
|
||||
private final ConcurrentHashMap<Integer, REntity> entityMap = new ConcurrentHashMap<>();
|
||||
private final HashMap<Long, HashSet<REntity>> entities = new HashMap<>();
|
||||
private final HashMap<Long, Set<Player>> players = new HashMap<>();
|
||||
private final HashMap<Player, Location> lastLocation = new HashMap<>();
|
||||
private final HashMap<Player, Integer> viewDistance = new HashMap<>();
|
||||
|
||||
private final Set<Player> 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<Player> 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<Object> 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<REntity> 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<REntity> 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<Player> 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<Player> of, Set<Player> in, Consumer<Consumer<Object>> 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<Integer, Integer> 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<Player> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,10 +76,20 @@ public class RPlayer extends REntity implements RInteractableEntity<RPlayer> {
|
||||
}
|
||||
|
||||
private GameProfile saved;
|
||||
private boolean awaitingSkin = false;
|
||||
|
||||
@Override
|
||||
void list(Consumer<Object> 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<RPlayer> {
|
||||
packetSink.accept(getNamedSpawnPacket());
|
||||
entityDataPacket().add(skinPartsDataWatcher, (byte) 0x7F).send(packetSink);
|
||||
|
||||
for (Map.Entry<Object, ItemStack> entry : itemSlots.entrySet()) {
|
||||
for (Map.Entry<org.bukkit.inventory.EquipmentSlot, ItemStack> entry : itemSlots.entrySet()) {
|
||||
packetSink.accept(getEquipmentPacket(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
|
||||
|
||||
@@ -467,7 +467,7 @@ public class SchematicSelector {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
private List<SchematicNode> getSchematicList(SchematicNode parent) {
|
||||
protected List<SchematicNode> getSchematicList(SchematicNode parent) {
|
||||
List<SchematicNode> nodes = new ArrayList<>();
|
||||
switch (target.target) {
|
||||
case DIRECTORY:
|
||||
|
||||
@@ -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<UUID, List<Runnable>> 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<Runnable> listeners = SKIN_LISTENERS.remove(packet.getUuid());
|
||||
if (listeners != null) listeners.forEach(Runnable::run);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Player> cachedPlayers = new LinkedList<>();
|
||||
private static final int MAX_PROGRESS = 10;
|
||||
private static final Pattern PLUGIN_COUNT = Pattern.compile("Bukkit plugins \\((\\d+)\\)");
|
||||
|
||||
private final List<Player> cachedPlayers = new CopyOnWriteArrayList<>();
|
||||
private volatile int progress = 0;
|
||||
private int pluginCount = 0;
|
||||
private int enabledPlugins = 0;
|
||||
@Getter
|
||||
private final Map<Player, String> 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));
|
||||
|
||||
@@ -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)!
|
||||
|
||||
@@ -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)!
|
||||
|
||||
@@ -23,8 +23,6 @@ import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.event.ResultedEvent;
|
||||
import com.velocitypowered.api.event.connection.LoginEvent;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import de.steamwar.persistent.Subserver;
|
||||
import de.steamwar.sql.SteamwarUser;
|
||||
import de.steamwar.sql.UserPerm;
|
||||
import de.steamwar.velocitycore.VelocityCore;
|
||||
@@ -61,13 +59,6 @@ public interface Chatter {
|
||||
return new ChatterGroup(allStream());
|
||||
}
|
||||
|
||||
static ChatterGroup globalChat() {
|
||||
return new ChatterGroup(Stream.concat(Stream.of(Chatter.console()), allPlayers().filter(player -> {
|
||||
Subserver subserver = Subserver.getSubserver(player);
|
||||
return !(Subserver.isArena(subserver) && subserver.getServer() == player.getCurrentServer().map(ServerConnection::getServerInfo).orElse(null));
|
||||
}).map(Chatter::of)));
|
||||
}
|
||||
|
||||
static ChatterGroup serverteam() {
|
||||
return new ChatterGroup(allStream()
|
||||
.filter(player -> player.user().hasPerm(UserPerm.TEAM))
|
||||
|
||||
@@ -31,8 +31,10 @@ import net.kyori.adventure.text.event.ClickEvent;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static de.steamwar.persistent.Storage.eventServer;
|
||||
@@ -43,8 +45,15 @@ public class EventStarter {
|
||||
@Getter
|
||||
private static final Map<Integer, String> spectatePorts = new HashMap<>();
|
||||
|
||||
public static void addSpectateServer(int port, String command) {
|
||||
private static final Set<String> spectateServers = new HashSet<>();
|
||||
|
||||
public static void addSpectateServer(int port, String command, String serverName) {
|
||||
spectatePorts.put(port, command);
|
||||
spectateServers.add(serverName);
|
||||
}
|
||||
|
||||
public static boolean isSpectateServer(String serverName) {
|
||||
return spectateServers.contains(serverName);
|
||||
}
|
||||
|
||||
public EventStarter() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -236,7 +236,7 @@ public class VelocityCore implements ReloadablePlugin {
|
||||
String cmd = cmds.remove(0);
|
||||
|
||||
if (server.getSpectatePort() != 0) {
|
||||
EventStarter.addSpectateServer(server.getSpectatePort(), cmd);
|
||||
EventStarter.addSpectateServer(server.getSpectatePort(), cmd, entry.getKey());
|
||||
}
|
||||
|
||||
new ServerSwitchCommand(cmd, entry.getKey(), server.getSpectatePort() != 0, cmds.toArray(new String[0]));
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2026 SteamWar.de-Serverteam
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
package de.steamwar.velocitycore.chat;
|
||||
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.messages.Message;
|
||||
import de.steamwar.sql.UserPerm;
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class AlertChannel extends ChatChannel {
|
||||
|
||||
@Override
|
||||
public boolean switchable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message tag(Chatter receiver) {
|
||||
return new Message("CHAT_CHANNEL_ALERT");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Chatter> readers() {
|
||||
return Arrays.stream(Chatter.broadcast().getChatters());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(Chatter sender) {
|
||||
if (!sender.user().hasPerm(UserPerm.MODERATION)) {
|
||||
sender.system("CHAT_NO_WRITE");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Component format(Chatter receiver, Chatter sender, String message) {
|
||||
return receiver.parse(true, new Message("ALERT", message.replace('&', '§')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Alerts are system messages: they are shown even with hidden chat and bypass the chat filter.
|
||||
*/
|
||||
@Override
|
||||
public void send(Chatter sender, String message, boolean mirror) {
|
||||
if (!canWrite(sender)) return;
|
||||
|
||||
readers().forEach(reader -> reader.sendMessage(format(reader, sender, message)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package de.steamwar.velocitycore.chat;
|
||||
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.messages.Message;
|
||||
import de.steamwar.sql.AuditLog;
|
||||
import de.steamwar.sql.SteamwarUser;
|
||||
import de.steamwar.sql.UserPerm;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.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<Chatter> readers();
|
||||
|
||||
/**
|
||||
* Checks if the sender is allowed to write into this channel and informs the sender otherwise.
|
||||
*/
|
||||
public abstract boolean canWrite(Chatter sender);
|
||||
|
||||
protected abstract Component format(Chatter receiver, Chatter sender, String message);
|
||||
|
||||
/**
|
||||
* Called after a message was delivered to all readers.
|
||||
*
|
||||
* @param mirror false if the message originates from the mirror of this channel and should not be sent back
|
||||
* @return true if the message was delivered to a receiver outside the game
|
||||
*/
|
||||
protected boolean delivered(Chatter sender, String message, boolean mirror) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void audit(SteamwarUser sender, String message) {
|
||||
AuditLog.createChat(AuditLog.SERVER_NAME_VELOCITY, null, sender, message);
|
||||
}
|
||||
|
||||
public void send(Chatter sender, String message) {
|
||||
send(sender, message, true);
|
||||
}
|
||||
|
||||
public void send(Chatter sender, String message, boolean mirror) {
|
||||
if (!canWrite(sender)) return;
|
||||
|
||||
SteamwarUser user = sender.user();
|
||||
String coloredMessage = user.hasPerm(UserPerm.COLOR_CHAT) ? message.replace('&', '§') : message;
|
||||
if (ChatFilter.filtered(sender, coloredMessage)) return;
|
||||
|
||||
Map<Integer, Chatter> receivers = new LinkedHashMap<>();
|
||||
readers().forEach(reader -> receivers.putIfAbsent(reader.user().getId(), reader));
|
||||
receivers.putIfAbsent(user.getId(), sender);
|
||||
|
||||
boolean noReceiver = true;
|
||||
for (Chatter receiver : receivers.values()) {
|
||||
if (!receiver.chatShown()) continue;
|
||||
|
||||
Component msg = 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2026 SteamWar.de-Serverteam
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
package de.steamwar.velocitycore.chat;
|
||||
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.messages.Message;
|
||||
import de.steamwar.sql.Punishment;
|
||||
import de.steamwar.sql.SteamwarUser;
|
||||
import de.steamwar.sql.UserPerm;
|
||||
import de.steamwar.velocitycore.VelocityCore;
|
||||
import de.steamwar.velocitycore.commands.PunishmentCommand;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ChatFilter {
|
||||
|
||||
private ChatFilter() {}
|
||||
|
||||
public static boolean filtered(Chatter sender, String message) {
|
||||
if (!sender.chatShown()) {
|
||||
sender.system("CHAT_RECEIVE");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.replace("§[a-f0-9klmno]", "").trim().isEmpty()) {
|
||||
sender.system("CHAT_EMPTY");
|
||||
return true;
|
||||
}
|
||||
|
||||
SteamwarUser user = sender.user();
|
||||
if (!user.hasPerm(UserPerm.TEAM) && (message.contains("http:") || message.contains("https:") || message.contains("www."))) {
|
||||
sender.system("CHAT_NO_LINKS");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.Mute)) return true;
|
||||
|
||||
if (message.contains("LIXFEL")) {
|
||||
specialAlert(sender, "Lixfel", "CHAT_LIXFEL_", 3, 6, 11, 12, 15);
|
||||
}
|
||||
if (message.contains("YOYONOW")) {
|
||||
specialAlert(sender, "YoyoNow", "CHAT_YOYONOW_", 3, 6, 11, 12);
|
||||
}
|
||||
if (message.contains("CHAOSCAOT")) {
|
||||
specialAlert(sender, "Chaoscaot", "CHAT_CHAOSCAOT_", 3, 6, 11, 12, 15, 17);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void specialAlert(Chatter sender, String name, String baseMessage, int... delay) {
|
||||
sender.system("CHAT_LIXFEL_ACTION_BAR");
|
||||
for (int i = 0; i < delay.length; i++) {
|
||||
int finalI = i;
|
||||
VelocityCore.schedule(() -> sender.prefixless("CHAT_SPECIAL_ALERT", name, sender.user(), new Message(baseMessage + (finalI + 1)))).delay(delay[i], TimeUnit.SECONDS).schedule();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
package de.steamwar.velocitycore.chat;
|
||||
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.messages.Message;
|
||||
import de.steamwar.network.packets.server.PingPacket;
|
||||
import de.steamwar.sql.SteamwarUser;
|
||||
import de.steamwar.sql.Team;
|
||||
import de.steamwar.sql.UserPerm;
|
||||
import de.steamwar.velocitycore.network.NetworkSender;
|
||||
import de.steamwar.velocitycore.util.SteamwarPrefix;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.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<String> 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 <format>_SENDER})
|
||||
* shows the team of the sender on hover.
|
||||
*/
|
||||
public static Component player(String format, Chatter receiver, Chatter sender, Chatter msgReceiver, String message) {
|
||||
SteamwarUser user = sender.user();
|
||||
UserPerm.Prefix prefix = user.prefix();
|
||||
|
||||
// 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(" "));
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
package de.steamwar.velocitycore.chat;
|
||||
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.connection.DisconnectEvent;
|
||||
import com.velocitypowered.api.event.player.ServerPostConnectEvent;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import de.steamwar.linkage.Linked;
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.messages.Message;
|
||||
import de.steamwar.messages.PlayerChatter;
|
||||
import de.steamwar.network.packets.common.FightInfoPacket;
|
||||
import de.steamwar.persistent.Subserver;
|
||||
import de.steamwar.sql.Event;
|
||||
import de.steamwar.sql.Referee;
|
||||
import de.steamwar.sql.Team;
|
||||
import de.steamwar.sql.SteamwarUser;
|
||||
import de.steamwar.sql.UserConfig;
|
||||
import de.steamwar.sql.UserPerm;
|
||||
import de.steamwar.velocitycore.EventStarter;
|
||||
import de.steamwar.velocitycore.VelocityCore;
|
||||
import de.steamwar.velocitycore.listeners.BasicListener;
|
||||
import de.steamwar.velocitycore.tablist.TablistManager;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
import 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<RegisteredServer, LocalChannel> locals = new ConcurrentHashMap<>();
|
||||
private static final Map<RegisteredServer, FightTeamChannel[]> fightTeams = new ConcurrentHashMap<>();
|
||||
private static final Map<Integer, TeamChannel> teams = new ConcurrentHashMap<>();
|
||||
private static final Set<DirectChannel> directs = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private static final Map<UUID, ChatChannel> defaults = new ConcurrentHashMap<>();
|
||||
private static final Map<UUID, String> persisted = new ConcurrentHashMap<>();
|
||||
private static final Set<UUID> globalOnArena = ConcurrentHashMap.newKeySet();
|
||||
// Players reading the global chat on their current fight server until they leave it (after /global <text> or /chat global join)
|
||||
private static final Set<UUID> globalOnServer = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<UUID> announced = ConcurrentHashMap.newKeySet();
|
||||
private static final Map<UUID, Set<RegisteredServer>> spies = new ConcurrentHashMap<>();
|
||||
private static final Map<UUID, Boolean> allArenas = new ConcurrentHashMap<>();
|
||||
private static final Map<UUID, Set<RegisteredServer>> leftArenas = new ConcurrentHashMap<>();
|
||||
private static final Map<UUID, Set<String>> hidden = new ConcurrentHashMap<>();
|
||||
|
||||
private static int refereeEvent = -1;
|
||||
private static long refereesLoaded = 0;
|
||||
private static Set<Integer> referees = Collections.emptySet();
|
||||
|
||||
public static LocalChannel local(RegisteredServer server) {
|
||||
locals.keySet().removeIf(ChatManager::unregistered);
|
||||
return locals.computeIfAbsent(server, LocalChannel::new);
|
||||
}
|
||||
|
||||
public static FightTeamChannel fightTeam(RegisteredServer server, boolean blue) {
|
||||
fightTeams.keySet().removeIf(ChatManager::unregistered);
|
||||
return fightTeams.computeIfAbsent(server, s -> new FightTeamChannel[]{new FightTeamChannel(s, true), new FightTeamChannel(s, false)})[blue ? 0 : 1];
|
||||
}
|
||||
|
||||
public static TeamChannel team(int team) {
|
||||
return teams.computeIfAbsent(team, TeamChannel::new);
|
||||
}
|
||||
|
||||
public static DirectChannel direct(UUID first, UUID second) {
|
||||
for (DirectChannel channel : directs) {
|
||||
if (channel.contains(first) && channel.partner(first).equals(second)) {
|
||||
return channel;
|
||||
}
|
||||
}
|
||||
|
||||
DirectChannel channel = new DirectChannel(first, second);
|
||||
directs.add(channel);
|
||||
return channel;
|
||||
}
|
||||
|
||||
private static boolean unregistered(RegisteredServer server) {
|
||||
return VelocityCore.getProxy().getServer(server.getServerInfo().getName()).orElse(null) != server;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true for arenas and event spectate servers, which use a fight chat instead of the global chat
|
||||
*/
|
||||
public static boolean isArena(RegisteredServer server) {
|
||||
return Subserver.isArena(Subserver.getSubserver(server.getServerInfo())) || EventStarter.isSpectateServer(server.getServerInfo().getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return all servers using a fight chat (arenas and event spectate servers)
|
||||
*/
|
||||
public static Stream<RegisteredServer> fightServers() {
|
||||
return VelocityCore.getProxy().getAllServers().stream().filter(ChatManager::isArena);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the local channel of the server the player is on, null while the player is on no server
|
||||
*/
|
||||
public static LocalChannel currentLocal(Player player) {
|
||||
return currentServer(player).map(ChatManager::local).orElse(null);
|
||||
}
|
||||
|
||||
static boolean onServer(Chatter chatter, RegisteredServer server) {
|
||||
Player player = chatter.getPlayer();
|
||||
return player != null && server.getPlayersConnected().contains(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the message into the channel or makes it the default channel of the player if there is no message.
|
||||
*/
|
||||
public static void sendOrSwitch(Chatter sender, ChatChannel channel, String[] message, String usage) {
|
||||
if (message.length > 0) {
|
||||
channel.send(sender, String.join(" ", message));
|
||||
} else if (sender instanceof PlayerChatter player) {
|
||||
switchTo(player, channel);
|
||||
} else {
|
||||
sender.system(usage);
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<RegisteredServer> currentServer(Player player) {
|
||||
return player.getCurrentServer().map(ServerConnection::getServer);
|
||||
}
|
||||
|
||||
static boolean readsGlobal(Player player) {
|
||||
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<Chatter> 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<RegisteredServer> joined = spies.get(uuid);
|
||||
if (joined != null && joined.contains(server)) return true;
|
||||
|
||||
Set<RegisteredServer> 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<Integer> referees(int eventId) {
|
||||
if (eventId != refereeEvent || System.currentTimeMillis() - refereesLoaded > REFEREE_CACHE_MILLIS) {
|
||||
referees = Referee.get(eventId);
|
||||
refereeEvent = eventId;
|
||||
refereesLoaded = System.currentTimeMillis();
|
||||
}
|
||||
return referees;
|
||||
}
|
||||
|
||||
private static Set<String> hidden(UUID uuid) {
|
||||
return hidden.computeIfAbsent(uuid, u -> {
|
||||
Set<String> channels = ConcurrentHashMap.newKeySet();
|
||||
String config = UserConfig.getConfig(u, HIDDEN_KEY);
|
||||
if (config != null && !config.isEmpty()) {
|
||||
channels.addAll(Arrays.asList(config.split(",")));
|
||||
}
|
||||
return channels;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false if the player left the channel with the given persist id as reader
|
||||
*/
|
||||
static boolean reads(Chatter chatter, String persistId) {
|
||||
Player player = chatter.getPlayer();
|
||||
return player == null || !hidden(player.getUniqueId()).contains(persistId);
|
||||
}
|
||||
|
||||
private static void saveHidden(UUID uuid) {
|
||||
Set<String> channels = hidden(uuid);
|
||||
UserConfig.updatePlayerConfig(uuid, HIDDEN_KEY, channels.isEmpty() ? null : String.join(",", channels));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the channel is the fight chat of an arena the player is not on, which moderators can read without joining the arena
|
||||
*/
|
||||
private static boolean isForeignArena(PlayerChatter player, ChatChannel channel) {
|
||||
return channel instanceof LocalChannel local && local.isArena() && !onServer(player, local.getServer());
|
||||
}
|
||||
|
||||
public static void join(PlayerChatter player, ChatChannel channel) {
|
||||
UUID uuid = player.user().getUUID();
|
||||
if (isForeignArena(player, channel)) {
|
||||
RegisteredServer server = ((LocalChannel) channel).getServer();
|
||||
spies.computeIfAbsent(uuid, u -> ConcurrentHashMap.newKeySet()).add(server);
|
||||
Set<RegisteredServer> 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<RegisteredServer> 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<Integer, Subserver> entry : new ArrayList<>(EventStarter.getEventServer().entrySet())) {
|
||||
if (entry.getValue() != subserver) continue;
|
||||
|
||||
Team team = Team.byId(entry.getKey());
|
||||
if (team != null && team.getTeamKuerzel().equals(kuerzel)) return team;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the fight teams of an arena. Players who just joined a fight team write into the fight chat by default.
|
||||
*/
|
||||
public static void updateFightInfo(RegisteredServer server, FightInfoPacket previous, FightInfoPacket packet) {
|
||||
|
||||
Set<Integer> joined = new HashSet<>(packet.getBluePlayers());
|
||||
joined.addAll(packet.getRedPlayers());
|
||||
if (previous != null) {
|
||||
previous.getBluePlayers().forEach(joined::remove);
|
||||
previous.getRedPlayers().forEach(joined::remove);
|
||||
}
|
||||
|
||||
for (Player player : server.getPlayersConnected()) {
|
||||
if (joined.contains(SteamwarUser.get(player.getUniqueId()).getId())) {
|
||||
setDefault(Chatter.of(player), local(server));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the channel the receiver writes into by default; non-player chatters (console, Discord) use the global chat
|
||||
*/
|
||||
public static ChatChannel getDefault(Chatter chatter) {
|
||||
Player player = chatter.getPlayer();
|
||||
if (player == null) return global;
|
||||
|
||||
return defaults.computeIfAbsent(player.getUniqueId(), uuid -> initialDefault(player, currentServer(player).orElse(null)));
|
||||
}
|
||||
|
||||
private static ChatChannel initialDefault(Player player, RegisteredServer server) {
|
||||
if (server != null && isArena(server)) return local(server);
|
||||
return resolvePersisted(player, server);
|
||||
}
|
||||
|
||||
private static ChatChannel resolvePersisted(Player player, RegisteredServer server) {
|
||||
String id = persisted.computeIfAbsent(player.getUniqueId(), uuid -> Optional.ofNullable(UserConfig.getConfig(uuid, CONFIG_KEY)).orElse(global.persistId()));
|
||||
SteamwarUser user = SteamwarUser.get(player.getUniqueId());
|
||||
return switch (id) {
|
||||
case "local" -> server != null && !isArena(server) ? local(server) : global;
|
||||
case "team" -> user.getTeam() != 0 ? team(user.getTeam()) : global;
|
||||
case "serverteam" -> user.hasPerm(UserPerm.TEAM) ? serverTeam : global;
|
||||
default -> global;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the channel the default channel of the player because the player chose it.
|
||||
*/
|
||||
public static void switchTo(PlayerChatter player, ChatChannel channel) {
|
||||
if (!channel.switchable() || isForeignArena(player, channel)) {
|
||||
player.system("CHAT_CHANNEL_NOT_SWITCHABLE");
|
||||
return;
|
||||
}
|
||||
if (!channel.canWrite(player)) return;
|
||||
|
||||
UUID uuid = player.user().getUUID();
|
||||
if (channel == global && currentServer(player.getPlayer()).map(ChatManager::isArena).orElse(false)) {
|
||||
globalOnArena.add(uuid);
|
||||
} else if (channel != global) {
|
||||
globalOnArena.remove(uuid);
|
||||
}
|
||||
|
||||
String id = channel.persistId();
|
||||
if (id != null && !id.equals(persisted.get(uuid))) {
|
||||
persisted.put(uuid, id);
|
||||
UserConfig.updatePlayerConfig(uuid, CONFIG_KEY, id);
|
||||
}
|
||||
if (id != null && hidden(uuid).remove(id)) {
|
||||
saveHidden(uuid);
|
||||
}
|
||||
|
||||
defaults.put(uuid, channel);
|
||||
notifyDefault(player, channel);
|
||||
}
|
||||
|
||||
private static void setDefault(PlayerChatter player, ChatChannel channel) {
|
||||
UUID uuid = player.user().getUUID();
|
||||
if (channel != global) globalOnArena.remove(uuid);
|
||||
|
||||
ChatChannel previous = defaults.put(uuid, channel);
|
||||
if (announced.add(uuid)) {
|
||||
notifyDefault(player, channel);
|
||||
} else if (previous != channel && !player.parseToPlain(previous.tag(player)).equals(player.parseToPlain(channel.tag(player)))) {
|
||||
notifyDefault(player, channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the player which channel they write into, showing the channel icon with its name on hover.
|
||||
*/
|
||||
public static void notifyDefault(PlayerChatter player, ChatChannel channel) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2026 SteamWar.de-Serverteam
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
package de.steamwar.velocitycore.chat;
|
||||
|
||||
import com.velocitypowered.api.proxy.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<Chatter> readers() {
|
||||
return Stream.of(first, second)
|
||||
.map(uuid -> VelocityCore.getProxy().getPlayer(uuid).orElse(null))
|
||||
.filter(player -> player != null)
|
||||
.map(Chatter::of);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(Chatter sender) {
|
||||
UUID partner = partner(sender.user().getUUID());
|
||||
if (VelocityCore.getProxy().getPlayer(partner).isEmpty()) {
|
||||
sender.system("MSG_OFFLINE");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IgnoreSystem.isIgnored(partner, sender.user().getUUID())) {
|
||||
sender.system("MSG_IGNORED");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Component format(Chatter receiver, Chatter sender, String message) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2026 SteamWar.de-Serverteam
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
package de.steamwar.velocitycore.chat;
|
||||
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.messages.Message;
|
||||
import de.steamwar.network.packets.common.FightInfoPacket;
|
||||
import de.steamwar.sql.AuditLog;
|
||||
import de.steamwar.sql.SteamwarUser;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* The team chat of the blue or red team of a fight.
|
||||
*/
|
||||
public class FightTeamChannel extends ChatChannel {
|
||||
|
||||
@Getter
|
||||
private final RegisteredServer server;
|
||||
private final boolean blue;
|
||||
|
||||
FightTeamChannel(RegisteredServer server, boolean blue) {
|
||||
this.server = server;
|
||||
this.blue = blue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean switchable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<Integer> members() {
|
||||
FightInfoPacket info = ChatManager.getFightInfo(server);
|
||||
if (info == null) return Collections.emptyList();
|
||||
return blue ? info.getBluePlayers() : info.getRedPlayers();
|
||||
}
|
||||
|
||||
private String teamName() {
|
||||
FightInfoPacket info = ChatManager.getFightInfo(server);
|
||||
if (info == null) return "";
|
||||
return blue ? info.getBlueName() : info.getRedName();
|
||||
}
|
||||
|
||||
public boolean isMember(Chatter chatter) {
|
||||
return members().contains(chatter.user().getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message tag(Chatter receiver) {
|
||||
return new Message("CHAT_CHANNEL_FIGHT_TEAM", server.getServerInfo().getName(), teamName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Message icon(Chatter receiver) {
|
||||
String teamName = teamName();
|
||||
String color = teamName.length() >= 2 && teamName.charAt(0) == '§' ? teamName.substring(0, 2) : "§7";
|
||||
if (ChatManager.onServer(receiver, server)) {
|
||||
return new Message("CHAT_ICON_FIGHT_TEAM", color);
|
||||
}
|
||||
return new Message("CHAT_ICON_FIGHT_TEAM_FOREIGN", color, server.getServerInfo().getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
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<Chatter> readers() {
|
||||
List<Integer> members = members();
|
||||
return Stream.concat(
|
||||
server.getPlayersConnected().stream().map(Chatter::of).filter(chatter -> members.contains(chatter.user().getId())),
|
||||
ChatManager.spies(server));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(Chatter sender) {
|
||||
if (!isMember(sender) || !ChatManager.onServer(sender, server)) {
|
||||
sender.system("CHAT_FIGHT_NO_TEAM");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Component format(Chatter receiver, Chatter sender, String message) {
|
||||
return ChatFormat.fight(receiver, server, sender, teamName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_TEAM", ChatFormat.highlightMentions(message, "§f", receiver));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void audit(SteamwarUser sender, String message) {
|
||||
AuditLog.createChat(server.getServerInfo().getName(), null, sender, "+" + message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
package de.steamwar.velocitycore.chat;
|
||||
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.messages.Message;
|
||||
import de.steamwar.velocitycore.discord.DiscordBot;
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class GlobalChannel extends ChatChannel {
|
||||
|
||||
@Override
|
||||
public String persistId() {
|
||||
return "global";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message tag(Chatter receiver) {
|
||||
return new Message("CHAT_CHANNEL_GLOBAL");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Message icon(Chatter receiver) {
|
||||
return new Message("CHAT_ICON_GLOBAL");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String suggestion(Chatter receiver) {
|
||||
return "/global ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Chatter> readers() {
|
||||
return Stream.concat(Stream.of(Chatter.console()), Chatter.allPlayers().filter(ChatManager::readsGlobal).map(Chatter::of).filter(chatter -> ChatManager.reads(chatter, persistId())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(Chatter sender) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Component format(Chatter receiver, Chatter sender, String message) {
|
||||
return ChatFormat.player("CHAT_GLOBAL", receiver, sender, null, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean delivered(Chatter sender, String message, boolean mirror) {
|
||||
if (mirror) {
|
||||
DiscordBot.withBot(bot -> bot.getIngameChat().sendMessage(format(bot.getIngameChat(), sender, message)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2026 SteamWar.de-Serverteam
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
package de.steamwar.velocitycore.chat;
|
||||
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.messages.Message;
|
||||
import de.steamwar.network.packets.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<Chatter> readers() {
|
||||
Stream<Chatter> players = server.getPlayersConnected().stream().map(Chatter::of).filter(chatter -> ChatManager.reads(chatter, persistId())).map(Chatter.class::cast);
|
||||
if (isArena()) {
|
||||
players = Stream.concat(players, ChatManager.spies(server));
|
||||
}
|
||||
return players;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(Chatter sender) {
|
||||
// Moderators can write into fight chats they are not on (/chat <arena> msg)
|
||||
if (!ChatManager.onServer(sender, server) && !(isArena() && sender.user().hasPerm(UserPerm.MODERATION))) {
|
||||
sender.system("CHAT_NO_WRITE");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Component format(Chatter receiver, Chatter sender, String message) {
|
||||
if (!isArena()) {
|
||||
return ChatFormat.player("CHAT_GLOBAL", receiver, sender, null, message);
|
||||
}
|
||||
|
||||
String highlighted = ChatFormat.highlightMentions(message, "§7", receiver);
|
||||
FightInfoPacket info = ChatManager.getFightInfo(server);
|
||||
int id = sender.user().getId();
|
||||
if (info != null && info.getBluePlayers().contains(id)) {
|
||||
return ChatFormat.fight(receiver, server, sender, info.getBlueName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_PARTICIPANT", highlighted);
|
||||
} else if (info != null && info.getRedPlayers().contains(id)) {
|
||||
return ChatFormat.fight(receiver, server, sender, info.getRedName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_PARTICIPANT", highlighted);
|
||||
} else if (ChatManager.isReferee(server, id)) {
|
||||
return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_REFEREE_SENDER", "CHAT_FIGHT_REFEREE", highlighted);
|
||||
}
|
||||
return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_SPECTATOR_SENDER", "CHAT_FIGHT_SPECTATOR", highlighted);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean delivered(Chatter sender, String message, boolean mirror) {
|
||||
if (isArena()) {
|
||||
// Arenas never display chat from Velocity, they only record it for replays
|
||||
NetworkSender.send(server, new ChatRecordPacket(sender.user().getId(), message));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void audit(SteamwarUser sender, String message) {
|
||||
AuditLog.createChat(server.getServerInfo().getName(), null, sender, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
package de.steamwar.velocitycore.chat;
|
||||
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.messages.Message;
|
||||
import de.steamwar.sql.AuditLog;
|
||||
import de.steamwar.sql.SteamwarUser;
|
||||
import de.steamwar.sql.UserPerm;
|
||||
import de.steamwar.velocitycore.discord.DiscordBot;
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ServerTeamChannel extends ChatChannel {
|
||||
|
||||
@Override
|
||||
public String persistId() {
|
||||
return "serverteam";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message tag(Chatter receiver) {
|
||||
return new Message("CHAT_CHANNEL_SERVERTEAM");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Message icon(Chatter receiver) {
|
||||
return new Message("CHAT_ICON_SERVERTEAM");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String suggestion(Chatter receiver) {
|
||||
return "/stc ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Chatter> readers() {
|
||||
return Arrays.stream(Chatter.serverteam().getChatters()).filter(chatter -> ChatManager.reads(chatter, persistId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(Chatter sender) {
|
||||
if (!sender.user().hasPerm(UserPerm.TEAM)) {
|
||||
sender.system("CHAT_NO_WRITE");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Component format(Chatter receiver, Chatter sender, String message) {
|
||||
return ChatFormat.player("CHAT_SERVERTEAM", receiver, sender, null, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean delivered(Chatter sender, String message, boolean mirror) {
|
||||
if (mirror) {
|
||||
DiscordBot.withBot(bot -> bot.getServerTeamChat().sendMessage(ChatFormat.player("CHAT_GLOBAL", bot.getServerTeamChat(), sender, null, message)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void audit(SteamwarUser sender, String message) {
|
||||
AuditLog.createCommand(AuditLog.SERVER_NAME_VELOCITY, null, sender, "/stc " + message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
package de.steamwar.velocitycore.chat;
|
||||
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.messages.Message;
|
||||
import de.steamwar.sql.AuditLog;
|
||||
import de.steamwar.sql.SteamwarUser;
|
||||
import de.steamwar.velocitycore.commands.StreamingCommand;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class TeamChannel extends ChatChannel {
|
||||
|
||||
@Getter
|
||||
private final int team;
|
||||
|
||||
TeamChannel(int team) {
|
||||
this.team = team;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String persistId() {
|
||||
return "team";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message tag(Chatter receiver) {
|
||||
return new Message("CHAT_CHANNEL_TEAM");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Message icon(Chatter receiver) {
|
||||
return new Message("CHAT_ICON_TEAM");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String suggestion(Chatter receiver) {
|
||||
return "/tc ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Chatter> readers() {
|
||||
return Chatter.allPlayers()
|
||||
.map(Chatter::of)
|
||||
.filter(chatter -> chatter.user().getTeam() == team)
|
||||
.filter(StreamingCommand::isNotStreaming)
|
||||
.filter(chatter -> ChatManager.reads(chatter, persistId()))
|
||||
.map(Chatter.class::cast);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(Chatter sender) {
|
||||
if (sender.user().getTeam() != team) {
|
||||
sender.system("TC_NO_TEAM");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Component format(Chatter receiver, Chatter sender, String message) {
|
||||
return ChatFormat.player("CHAT_TEAM", receiver, sender, null, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void audit(SteamwarUser sender, String message) {
|
||||
AuditLog.createSensitiveCommand(AuditLog.SERVER_NAME_VELOCITY, null, sender, "/tc " + message);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import de.steamwar.command.SWCommand;
|
||||
import de.steamwar.linkage.Linked;
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.sql.UserPerm;
|
||||
import de.steamwar.velocitycore.chat.ChatManager;
|
||||
import de.steamwar.velocitycore.discord.DiscordBot;
|
||||
|
||||
@Linked
|
||||
@@ -40,7 +41,7 @@ public class AlertCommand extends SWCommand {
|
||||
}
|
||||
|
||||
String s = String.join(" ", message);
|
||||
Chatter.broadcast().system("ALERT", s.replace('&', '§'));
|
||||
ChatManager.getAlert().send(sender, s);
|
||||
|
||||
if ("-discord".equals(sendToDiscord)) {
|
||||
DiscordBot.withBot(bot -> bot.getAnnouncementChannel().send(s));
|
||||
|
||||
@@ -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<BauweltMember> function) {
|
||||
if (member == null) {
|
||||
owner.system("UNKNOWN_PLAYER");
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
package de.steamwar.velocitycore.commands;
|
||||
|
||||
import de.steamwar.command.PreviousArguments;
|
||||
import de.steamwar.command.SWCommand;
|
||||
import de.steamwar.command.TypeMapper;
|
||||
import de.steamwar.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<PlayerChatter> moderatorValidator() {
|
||||
return (sender, value, messageSender) -> !messageSender.send(!value.user().hasPerm(UserPerm.MODERATION), "CHAT_NO_WRITE");
|
||||
}
|
||||
|
||||
@ClassMapper(value = ChatChannel.class, local = true)
|
||||
public TypeMapper<ChatChannel> channelTypeMapper() {
|
||||
return new TypeMapper<>() {
|
||||
@Override
|
||||
public ChatChannel map(Chatter sender, PreviousArguments previousArguments, String s) {
|
||||
return switch (s.toLowerCase()) {
|
||||
case "global" -> ChatManager.getGlobal();
|
||||
case "local" -> sender.getPlayer() == null ? null : ChatManager.currentLocal(sender.getPlayer());
|
||||
case "team", "tc" -> sender.user().getTeam() != 0 ? ChatManager.team(sender.user().getTeam()) : null;
|
||||
case "serverteam", "stc" -> sender.user().hasPerm(UserPerm.TEAM) ? ChatManager.getServerTeam() : null;
|
||||
default -> arena(sender, s);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(Chatter sender, ChatChannel value, MessageSender messageSender) {
|
||||
return !messageSender.send(value == null, "CHAT_CHANNEL_UNKNOWN");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) {
|
||||
List<String> channels = new ArrayList<>(List.of("global", "local"));
|
||||
if (sender.user().getTeam() != 0) channels.add("team");
|
||||
if (sender.user().hasPerm(UserPerm.TEAM)) channels.add("serverteam");
|
||||
if (sender.user().hasPerm(UserPerm.MODERATION)) {
|
||||
ChatManager.fightServers().map(server -> server.getServerInfo().getName()).forEach(channels::add);
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Moderators can address the fight chat of every fight server by its server name.
|
||||
*/
|
||||
private static ChatChannel arena(Chatter sender, String name) {
|
||||
if (!sender.user().hasPerm(UserPerm.MODERATION)) return null;
|
||||
|
||||
return ChatManager.fightServers()
|
||||
.filter(server -> server.getServerInfo().getName().equalsIgnoreCase(name))
|
||||
.findFirst()
|
||||
.map(ChatManager::local)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -156,8 +156,7 @@ public class CheckCommand extends SWCommand {
|
||||
}
|
||||
|
||||
SchematicNode schem = SchematicNode.getSchematicNode(Integer.parseInt(schemID));
|
||||
GameModeConfig<String, String> 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(
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
package de.steamwar.velocitycore.commands;
|
||||
|
||||
import de.steamwar.command.SWCommand;
|
||||
import de.steamwar.linkage.Linked;
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,8 @@ package de.steamwar.velocitycore.commands;
|
||||
import de.steamwar.command.SWCommand;
|
||||
import de.steamwar.linkage.Linked;
|
||||
import de.steamwar.messages.PlayerChatter;
|
||||
import de.steamwar.velocitycore.listeners.ChatListener;
|
||||
import de.steamwar.velocitycore.chat.ChatChannel;
|
||||
import de.steamwar.velocitycore.chat.ChatManager;
|
||||
|
||||
@Linked
|
||||
public class LocalCommand extends SWCommand {
|
||||
@@ -31,8 +32,11 @@ public class LocalCommand extends SWCommand {
|
||||
super("local", "bc", "bauchat");
|
||||
}
|
||||
|
||||
@Register
|
||||
public void genericCommand(PlayerChatter player, String... message) {
|
||||
ChatListener.localChat(player, String.join(" ", message));
|
||||
@Register(description = "CHAT_BC_USAGE")
|
||||
public void genericCommand(PlayerChatter sender, String... message) {
|
||||
ChatChannel channel = ChatManager.currentLocal(sender.getPlayer());
|
||||
if (channel == null) return;
|
||||
|
||||
ChatManager.sendOrSwitch(sender, channel, message, "CHAT_BC_USAGE");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,9 @@ package de.steamwar.velocitycore.commands;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import de.steamwar.command.SWCommand;
|
||||
import de.steamwar.linkage.Linked;
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.messages.ChatterGroup;
|
||||
import de.steamwar.messages.PlayerChatter;
|
||||
import de.steamwar.sql.IgnoreSystem;
|
||||
import de.steamwar.velocitycore.VelocityCore;
|
||||
import de.steamwar.velocitycore.listeners.ChatListener;
|
||||
import de.steamwar.velocitycore.chat.ChatManager;
|
||||
|
||||
import static de.steamwar.persistent.Storage.lastChats;
|
||||
|
||||
@@ -49,15 +46,7 @@ public class MsgCommand extends SWCommand {
|
||||
return;
|
||||
}
|
||||
|
||||
if (IgnoreSystem.isIgnored(target.getUniqueId(), sender.user().getUUID())) {
|
||||
sender.system("MSG_IGNORED");
|
||||
return;
|
||||
}
|
||||
|
||||
Chatter receiver = Chatter.of(target);
|
||||
ChatListener.sendChat(sender, new ChatterGroup(sender, receiver), "CHAT_MSG", receiver, String.join(" ", args));
|
||||
lastChats.put(sender.getPlayer(), target);
|
||||
lastChats.put(target, sender.getPlayer());
|
||||
ChatManager.direct(sender.user().getUUID(), target.getUniqueId()).send(sender, String.join(" ", args));
|
||||
}
|
||||
|
||||
public static void remove(Player player) {
|
||||
|
||||
@@ -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<String, String> mode = ArenaMode.getBySchemType(fight.getSchemType());
|
||||
ServerStarter starter = new ServerStarter().replay(fight.getFightID()).blueLeader(sender.getPlayer());
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,7 @@ package de.steamwar.velocitycore.commands;
|
||||
import de.steamwar.command.SWCommand;
|
||||
import de.steamwar.linkage.Linked;
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.messages.ChatterGroup;
|
||||
import de.steamwar.sql.SteamwarUser;
|
||||
import de.steamwar.velocitycore.listeners.ChatListener;
|
||||
import de.steamwar.velocitycore.chat.ChatManager;
|
||||
|
||||
@Linked
|
||||
public class TeamchatCommand extends SWCommand {
|
||||
@@ -34,14 +32,13 @@ public class TeamchatCommand extends SWCommand {
|
||||
}
|
||||
|
||||
@Register(description = "TC_USAGE")
|
||||
public void genericCommand(Chatter sender, @ErrorMessage(value = "TC_USAGE", allowEAs = false) String... args) {
|
||||
SteamwarUser user = sender.user();
|
||||
|
||||
if (user.getTeam() == 0) {
|
||||
public void genericCommand(Chatter sender, String... args) {
|
||||
int team = sender.user().getTeam();
|
||||
if (team == 0) {
|
||||
sender.system("TC_NO_TEAM");
|
||||
return;
|
||||
}
|
||||
|
||||
ChatListener.sendChat(sender, new ChatterGroup(Chatter.allStream().filter(p -> p.user().getTeam() == user.getTeam()).filter(StreamingCommand::isNotStreaming)), "CHAT_TEAM", null, String.join(" ", args));
|
||||
ChatManager.sendOrSwitch(sender, ChatManager.team(team), args, "TC_USAGE");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ package de.steamwar.velocitycore.discord;
|
||||
|
||||
import de.steamwar.command.SWCommand;
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.velocitycore.chat.ChatManager;
|
||||
import de.steamwar.sql.Event;
|
||||
import de.steamwar.velocitycore.VelocityCore;
|
||||
import de.steamwar.velocitycore.discord.channels.*;
|
||||
@@ -181,9 +182,9 @@ public class DiscordBot {
|
||||
// There is a hard limit of 30 messages per minute to send as a webhook, thus with 8 webhooks we can send
|
||||
// 240 messages per minute. Which means 4 every second. I looked at the WGS fights and there were around
|
||||
// ~70 in a short burst and then rather long no new message.
|
||||
ingameChat = new DiscordChatRoom(config.channel("ingame"), "CHAT_DISCORD_GLOBAL", Chatter::globalChat, 8);
|
||||
ingameChat = new DiscordChatRoom(config.channel("ingame"), ChatManager.getGlobal(), 8);
|
||||
// 60 messages per minute should be enough for the server team!
|
||||
serverTeamChat = new DiscordChatRoom(config.channel("serverteam"), "CHAT_DISCORD_SERVERTEAM", Chatter::serverteam, 2);
|
||||
serverTeamChat = new DiscordChatRoom(config.channel("serverteam"), ChatManager.getServerTeam(), 2);
|
||||
|
||||
VelocityCore.schedule(() -> {
|
||||
try {
|
||||
|
||||
@@ -20,22 +20,17 @@
|
||||
package de.steamwar.velocitycore.discord.channels;
|
||||
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.messages.ChatterGroup;
|
||||
import de.steamwar.sql.Punishment;
|
||||
import de.steamwar.sql.SteamwarUser;
|
||||
import de.steamwar.velocitycore.listeners.ChatListener;
|
||||
import de.steamwar.velocitycore.chat.ChatChannel;
|
||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class DiscordChatRoom extends DiscordChannel {
|
||||
|
||||
private final String format;
|
||||
private final Supplier<ChatterGroup> target;
|
||||
private final ChatChannel target;
|
||||
|
||||
public DiscordChatRoom(String channel, String format, Supplier<ChatterGroup> target, int maxNumberOfWebhooks) {
|
||||
public DiscordChatRoom(String channel, ChatChannel target, int maxNumberOfWebhooks) {
|
||||
super(channel, maxNumberOfWebhooks);
|
||||
this.format = format;
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@@ -47,6 +42,6 @@ public class DiscordChatRoom extends DiscordChannel {
|
||||
return;
|
||||
}
|
||||
|
||||
ChatListener.sendChat(Chatter.of(user.getUUID(), true), target.get(), format, null, event.getMessage().getContentDisplay().replace('§', '&').replace('\n', ' '));
|
||||
target.send(Chatter.of(user.getUUID(), true), event.getMessage().getContentDisplay().replace('§', '&').replace('\n', ' '), false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,30 +27,18 @@ import com.velocitypowered.api.event.player.PlayerChatEvent;
|
||||
import com.velocitypowered.api.event.player.TabCompleteEvent;
|
||||
import com.velocitypowered.api.proxy.ConsoleCommandSource;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import de.steamwar.linkage.Linked;
|
||||
import de.steamwar.messages.Chatter;
|
||||
import de.steamwar.messages.ChatterGroup;
|
||||
import de.steamwar.messages.Message;
|
||||
import de.steamwar.messages.PlayerChatter;
|
||||
import de.steamwar.network.packets.server.PingPacket;
|
||||
import de.steamwar.persistent.Subserver;
|
||||
import de.steamwar.sql.*;
|
||||
import de.steamwar.velocitycore.ArenaMode;
|
||||
import de.steamwar.velocitycore.VelocityCore;
|
||||
import de.steamwar.velocitycore.chat.ChatManager;
|
||||
import de.steamwar.velocitycore.commands.PunishmentCommand;
|
||||
import de.steamwar.velocitycore.discord.DiscordBot;
|
||||
import de.steamwar.velocitycore.network.NetworkSender;
|
||||
import de.steamwar.velocitycore.util.SteamwarPrefix;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Linked
|
||||
public class ChatListener extends BasicListener {
|
||||
@@ -138,21 +126,7 @@ public class ChatListener extends BasicListener {
|
||||
return;
|
||||
}
|
||||
|
||||
Subserver subserver = Subserver.getSubserver(player);
|
||||
String serverName = AuditLog.SERVER_NAME_VELOCITY;
|
||||
if (Subserver.isArena(subserver) && subserver.getServer() == player.getCurrentServer().orElseThrow().getServerInfo()) {
|
||||
serverName = subserver.getServer().getName();
|
||||
localChat(Chatter.of(player), message);
|
||||
} else if (message.startsWith("+")) {
|
||||
serverName = player.getCurrentServer()
|
||||
.map(ServerConnection::getServerInfo)
|
||||
.map(ServerInfo::getName)
|
||||
.orElse(serverName);
|
||||
localChat(Chatter.of(player), message.substring(1));
|
||||
} else {
|
||||
sendChat(Chatter.of(player), Chatter.globalChat(), "CHAT_GLOBAL", null, message);
|
||||
}
|
||||
AuditLog.createChat(serverName, null, user, message);
|
||||
ChatManager.chat(Chatter.of(player), message);
|
||||
}
|
||||
|
||||
private static boolean isMistypedCommand(Player player, String message) {
|
||||
@@ -172,115 +146,6 @@ public class ChatListener extends BasicListener {
|
||||
return isCommand;
|
||||
}
|
||||
|
||||
public static void sendChat(Chatter sender, ChatterGroup receivers, String format, Chatter msgReceiver, String message) {
|
||||
SteamwarUser user = sender.user();
|
||||
final String coloredMessage = user.hasPerm(UserPerm.COLOR_CHAT) ? message.replace('&', '§') : message;
|
||||
if (chatFilter(sender, coloredMessage)) return;
|
||||
|
||||
boolean useSwPrefix = useSwPrefix(user);
|
||||
|
||||
boolean noReceiver = true;
|
||||
for (Chatter player : receivers.getChatters()) {
|
||||
if (player.chatShown()) {
|
||||
chatToReciever(player, msgReceiver, user, format, coloredMessage, useSwPrefix);
|
||||
if (sender.user().getId() != player.user().getId()) noReceiver = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (format.equals("CHAT_GLOBAL")) {
|
||||
DiscordBot.withBot(bot -> chatToReciever(bot.getIngameChat(), msgReceiver, user, format, coloredMessage, useSwPrefix));
|
||||
} else if (format.equals("CHAT_SERVERTEAM")) {
|
||||
DiscordBot.withBot(bot -> chatToReciever(bot.getServerTeamChat(), msgReceiver, user, "CHAT_GLOBAL", coloredMessage, useSwPrefix));
|
||||
} else if (noReceiver) {
|
||||
sender.system("CHAT_NO_RECEIVER");
|
||||
}
|
||||
}
|
||||
|
||||
public static void localChat(PlayerChatter sender, String message) {
|
||||
if (message.isEmpty()) {
|
||||
sender.system("CHAT_BC_USAGE");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ChatListener.filteredCommand(sender, message)) return;
|
||||
|
||||
if (!message.startsWith("/") && chatFilter(sender, message)) return;
|
||||
|
||||
sender.getPlayer().spoofChatInput(message);
|
||||
}
|
||||
|
||||
private static boolean chatFilter(Chatter sender, String message) {
|
||||
if (!sender.chatShown()) {
|
||||
sender.system("CHAT_RECEIVE");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.replace("§[a-f0-9klmno]", "").trim().isEmpty()) {
|
||||
sender.system("CHAT_EMPTY");
|
||||
return true;
|
||||
}
|
||||
|
||||
SteamwarUser user = sender.user();
|
||||
if (!user.hasPerm(UserPerm.TEAM) && (message.contains("http:") || message.contains("https:") || message.contains("www."))) {
|
||||
sender.system("CHAT_NO_LINKS");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.Mute)) return true;
|
||||
|
||||
if (message.contains("LIXFEL")) {
|
||||
specialAlert(sender, "Lixfel", "CHAT_LIXFEL_", 3, 6, 11, 12, 15);
|
||||
}
|
||||
if (message.contains("YOYONOW")) {
|
||||
specialAlert(sender, "YoyoNow", "CHAT_YOYONOW_", 3, 6, 11, 12);
|
||||
}
|
||||
if (message.contains("CHAOSCAOT")) {
|
||||
specialAlert(sender, "Chaoscaot", "CHAT_CHAOSCAOT_", 3, 6, 11, 12, 15, 17);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void chatToReciever(Chatter receiver, Chatter msgReceiver, SteamwarUser sender, String format, String message, boolean useSwPrefix) {
|
||||
UserPerm.Prefix prefix = sender.prefix();
|
||||
|
||||
String teamPrefix = "";
|
||||
if (sender.hasPerm(UserPerm.TEAM) && useSwPrefix) {
|
||||
teamPrefix = "§eS§8W ";
|
||||
} else if (sender.getTeam() != 0) {
|
||||
Team team = Team.byId(sender.getTeam());
|
||||
teamPrefix = "§" + team.getTeamColor() + team.getTeamKuerzel() + " ";
|
||||
}
|
||||
|
||||
String chatColorCode = sender.hasPerm(UserPerm.TEAM) ? "§f" : "§7";
|
||||
receiver.prefixless(format,
|
||||
sender,
|
||||
msgReceiver == null ? receiver : msgReceiver,
|
||||
highlightMentions(message, chatColorCode, receiver),
|
||||
teamPrefix,
|
||||
"",
|
||||
prefix == UserPerm.emptyPrefix ? "§f" : prefix.getColorCode(),
|
||||
prefix.getChatPrefix().length() == 0 ? "§f" : prefix.getChatPrefix() + " ",
|
||||
chatColorCode);
|
||||
}
|
||||
|
||||
private static boolean useSwPrefix(SteamwarUser user) {
|
||||
if (!user.hasPerm(UserPerm.TEAM)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UserPerm.Prefix prefix = user.prefix();
|
||||
|
||||
boolean hasTeamPrefix = user.getTeam() != 0;
|
||||
boolean hasSwPrefix = prefix != UserPerm.emptyPrefix;
|
||||
|
||||
if (!hasSwPrefix) return false;
|
||||
if (!hasTeamPrefix) return true;
|
||||
|
||||
return SteamwarPrefix.usesSWPrefix(user);
|
||||
}
|
||||
|
||||
private static boolean filteredCommand(Chatter sender, String message) {
|
||||
String command = message.split(" ", 2)[0];
|
||||
if (command.startsWith("/") && command.contains(":")) {
|
||||
@@ -290,27 +155,6 @@ public class ChatListener extends BasicListener {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void specialAlert(Chatter sender, String name, String baseMessage, int... delay) {
|
||||
sender.system("CHAT_LIXFEL_ACTION_BAR");
|
||||
for (int i = 0; i < delay.length; i++) {
|
||||
int finalI = i;
|
||||
VelocityCore.schedule(() -> sender.prefixless("CHAT_MSG", name, sender.user(), new Message(baseMessage + (finalI + 1)))).delay(delay[i], TimeUnit.SECONDS).schedule();
|
||||
}
|
||||
}
|
||||
|
||||
private static String highlightMentions(String message, String returnColor, Chatter player) {
|
||||
if (!message.contains("@")) return message;
|
||||
|
||||
String mark = "@" + player.user().getUserName();
|
||||
return Arrays.stream(message.split(" ")).map(cur -> {
|
||||
if (cur.equalsIgnoreCase(mark) && player.getPlayer() != null) {
|
||||
NetworkSender.send(player.getPlayer(), new PingPacket(player.user().getId()));
|
||||
return "§e" + cur + returnColor;
|
||||
}
|
||||
return cur;
|
||||
}).collect(Collectors.joining(" "));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onTabCompleteResponseEvent(TabCompleteEvent e) {
|
||||
List<String> suggestions = e.getSuggestions();
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import de.steamwar.linkage.Linked;
|
||||
import de.steamwar.network.packets.PacketHandler;
|
||||
import de.steamwar.network.packets.common.FightInfoPacket;
|
||||
import de.steamwar.velocitycore.chat.ChatManager;
|
||||
import de.steamwar.velocitycore.network.NetworkSender;
|
||||
import de.steamwar.velocitycore.network.ServerMetaInfo;
|
||||
import de.steamwar.velocitycore.tablist.TablistManager;
|
||||
@@ -50,7 +51,8 @@ public class FightInfoHandler extends PacketHandler {
|
||||
|
||||
FightInfoPacket lobbyPacket = packet.withServerName(server.getServerInfo().getName());
|
||||
|
||||
TablistManager.newFightInfo(server, packet);
|
||||
FightInfoPacket previous = TablistManager.newFightInfo(server, packet);
|
||||
ChatManager.updateFightInfo(server, previous, packet);
|
||||
|
||||
lobbys.removeIf(s -> s.getPlayersConnected().isEmpty());
|
||||
lobbys.forEach(lobby -> NetworkSender.send(lobby, lobbyPacket));
|
||||
|
||||
@@ -45,9 +45,17 @@ public class TablistManager extends BasicListener implements Disable {
|
||||
|
||||
private static final Map<RegisteredServer, FightInfoPacket> fightInfos = new HashMap<>();
|
||||
|
||||
public static synchronized void newFightInfo(RegisteredServer info, FightInfoPacket packet) {
|
||||
fightInfos.put(info, packet);
|
||||
/**
|
||||
* @return the previous fight info of the server, null if there was none
|
||||
*/
|
||||
public static synchronized FightInfoPacket newFightInfo(RegisteredServer info, FightInfoPacket packet) {
|
||||
FightInfoPacket previous = fightInfos.put(info, packet);
|
||||
fightInfos.keySet().removeIf(serverInfo -> serverInfo.getPlayersConnected().isEmpty());
|
||||
return previous;
|
||||
}
|
||||
|
||||
public static synchronized FightInfoPacket getFightInfo(RegisteredServer server) {
|
||||
return fightInfos.get(server);
|
||||
}
|
||||
|
||||
private final Map<Player, Tablist> tablists = new HashMap<>();
|
||||
|
||||
Reference in New Issue
Block a user