diff --git a/client/src/main/java/nl/andrewl/aos2_client/Client.java b/client/src/main/java/nl/andrewl/aos2_client/Client.java index b242a4d..13814e5 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/Client.java +++ b/client/src/main/java/nl/andrewl/aos2_client/Client.java @@ -156,6 +156,14 @@ public class Client implements Runnable { }); } else if (msg instanceof PlayerLeaveMessage leaveMessage) { runLater(() -> players.remove(leaveMessage.id())); + } else if (msg instanceof PlayerTeamUpdateMessage teamUpdateMessage) { + runLater(() -> { + OtherPlayer op = players.get(teamUpdateMessage.playerId()); + Team team = teamUpdateMessage.teamId() == -1 ? null : teams.get(teamUpdateMessage.teamId()); + if (op != null) { + op.setTeam(team); + } + }); } else if (msg instanceof SoundMessage soundMessage) { if (soundManager != null) { soundManager.play( diff --git a/core/src/main/java/nl/andrewl/aos_core/Net.java b/core/src/main/java/nl/andrewl/aos_core/Net.java index 17b34a6..de59e14 100644 --- a/core/src/main/java/nl/andrewl/aos_core/Net.java +++ b/core/src/main/java/nl/andrewl/aos_core/Net.java @@ -26,29 +26,37 @@ public final class Net { private static final Serializer serializer = new Serializer(); static { - serializer.registerType(1, ConnectRequestMessage.class); - serializer.registerType(2, ConnectAcceptMessage.class); - serializer.registerType(3, ConnectRejectMessage.class); - serializer.registerType(4, DatagramInit.class); - serializer.registerType(5, ChunkHashMessage.class); - serializer.registerType(6, ChunkDataMessage.class); - serializer.registerType(7, ChunkUpdateMessage.class); - serializer.registerType(8, ClientInputState.class); - serializer.registerType(9, ClientOrientationState.class); - serializer.registerType(10, PlayerUpdateMessage.class); - serializer.registerType(11, PlayerJoinMessage.class); - serializer.registerType(12, PlayerLeaveMessage.class); + int i = 1; + // Basic protocol messages. + serializer.registerType(i++, ConnectRequestMessage.class); + serializer.registerType(i++, ConnectAcceptMessage.class); + serializer.registerType(i++, ConnectRejectMessage.class); + serializer.registerType(i++, DatagramInit.class); + + // World messages. + serializer.registerType(i++, ChunkHashMessage.class); + serializer.registerType(i++, ChunkDataMessage.class); + serializer.registerType(i++, ChunkUpdateMessage.class); + serializer.registerType(i++, ProjectileMessage.class); + + // Player/client messages. + serializer.registerType(i++, ClientInputState.class); + serializer.registerType(i++, ClientOrientationState.class); + serializer.registerType(i++, ClientHealthMessage.class); + serializer.registerType(i++, PlayerUpdateMessage.class); + serializer.registerType(i++, PlayerJoinMessage.class); + serializer.registerType(i++, PlayerLeaveMessage.class); + serializer.registerType(i++, PlayerTeamUpdateMessage.class); + serializer.registerType(i++, BlockColorMessage.class); + serializer.registerType(i++, InventorySelectedStackMessage.class); + serializer.registerType(i++, ChatMessage.class); + serializer.registerType(i++, ChatWrittenMessage.class); + serializer.registerType(i++, ClientRecoilMessage.class); // Separate serializers for client inventory messages. - serializer.registerTypeSerializer(13, new InventorySerializer()); - serializer.registerTypeSerializer(14, new ItemStackSerializer()); - serializer.registerType(15, InventorySelectedStackMessage.class); - serializer.registerType(16, SoundMessage.class); - serializer.registerType(17, ProjectileMessage.class); - serializer.registerType(18, ClientHealthMessage.class); - serializer.registerType(19, BlockColorMessage.class); - serializer.registerType(20, ChatMessage.class); - serializer.registerType(21, ChatWrittenMessage.class); - serializer.registerType(22, ClientRecoilMessage.class); + serializer.registerTypeSerializer(i++, new InventorySerializer()); + serializer.registerTypeSerializer(i++, new ItemStackSerializer()); + + serializer.registerType(i++, SoundMessage.class); } public static ExtendedDataInputStream getInputStream(InputStream in) { diff --git a/core/src/main/java/nl/andrewl/aos_core/net/client/PlayerTeamUpdateMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/client/PlayerTeamUpdateMessage.java new file mode 100644 index 0000000..9b844d7 --- /dev/null +++ b/core/src/main/java/nl/andrewl/aos_core/net/client/PlayerTeamUpdateMessage.java @@ -0,0 +1,13 @@ +package nl.andrewl.aos_core.net.client; + +import nl.andrewl.record_net.Message; + +/** + * A message that's sent by the server to announce that a player has changed to + * a specified team. Both the player and team should already be recognized by + * all clients; otherwise they can ignore this. + */ +public record PlayerTeamUpdateMessage( + int playerId, + int teamId +) implements Message {} diff --git a/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java b/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java index f00b78d..0817161 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java +++ b/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java @@ -30,21 +30,26 @@ public class PlayerManager { } public synchronized ServerPlayer register(ClientCommunicationHandler handler, String username) { - ServerPlayer player = new ServerPlayer(nextClientId++, username); + Team team = findBestTeamForNewPlayer(); + ServerPlayer player = new ServerPlayer(nextClientId++, username, team, PlayerMode.NORMAL); + var inv = player.getInventory(); + inv.getItemStacks().add(new GunItemStack(ItemTypes.RIFLE)); + inv.getItemStacks().add(new GunItemStack(ItemTypes.AK_47)); + inv.getItemStacks().add(new GunItemStack(ItemTypes.WINCHESTER)); + inv.getItemStacks().add(new BlockItemStack(ItemTypes.BLOCK, 50, (byte) 1)); + inv.setSelectedIndex(0); + System.out.printf("Registered player \"%s\" with id %d.%n", player.getUsername(), player.getId()); players.put(player.getId(), player); clientHandlers.put(player.getId(), handler); String joinMessage; - Team team = findBestTeamForNewPlayer(); - if (team != null) { - player.setTeam(team); - System.out.printf("Player \"%s\" joined the \"%s\" team.%n", player.getUsername(), team.getName()); - joinMessage = String.format("%s joined the %s team.", username, team.getName()); + if (player.getTeam() != null) { + System.out.printf("Player \"%s\" joined the \"%s\" team.%n", player.getUsername(), player.getTeam().getName()); + joinMessage = String.format("%s joined the %s team.", username, player.getTeam().getName()); } else { joinMessage = username + " joined the game."; } player.setPosition(getBestSpawnPoint(player)); - setMode(player, PlayerMode.NORMAL); // Tell all other players that this one has joined. broadcastTcpMessageToAllBut(new PlayerJoinMessage( player.getId(), player.getUsername(), player.getTeam() == null ? -1 : player.getTeam().getId(), @@ -163,12 +168,9 @@ public class PlayerManager { */ public void playerKilled(ServerPlayer player, ServerPlayer killedBy) { Vector3f deathPosition = new Vector3f(player.getPosition()); - player.setPosition(getBestSpawnPoint(player)); - player.setVelocity(new Vector3f(0)); player.incrementDeathCount(); - resupply(player); - broadcastUdpMessage(player.getUpdateMessage(System.currentTimeMillis())); broadcastUdpMessage(new SoundMessage("death", 1, deathPosition)); + respawn(player); String deathMessage; if (killedBy != null) { killedBy.incrementKillCount(); @@ -198,8 +200,16 @@ public class PlayerManager { handler.sendTcpMessage(ChatMessage.privateMessage("You've been resupplied at your team base.")); } + public void respawn(ServerPlayer player) { + player.setPosition(getBestSpawnPoint(player)); + player.setVelocity(new Vector3f(0)); + broadcastUdpMessage(player.getUpdateMessage(System.currentTimeMillis())); + resupply(player); + } + public void setMode(ServerPlayer player, PlayerMode mode) { player.setMode(mode); + var handler = getHandler(player); var inv = player.getInventory(); inv.clear(); if (mode == PlayerMode.NORMAL || mode == PlayerMode.CREATIVE) { @@ -208,7 +218,31 @@ public class PlayerManager { inv.getItemStacks().add(new GunItemStack(ItemTypes.WINCHESTER)); inv.getItemStacks().add(new BlockItemStack(ItemTypes.BLOCK, 50, (byte) 1)); inv.setSelectedIndex(0); + handler.sendTcpMessage(new ClientInventoryMessage(inv)); + broadcastUdpMessage(player.getUpdateMessage(System.currentTimeMillis())); } + if (mode != PlayerMode.NORMAL) { + player.setTeam(null); + broadcastTcpMessage(new PlayerTeamUpdateMessage(player.getId(), -1)); + } else { + player.setTeam(findBestTeamForNewPlayer()); + broadcastTcpMessage(new PlayerTeamUpdateMessage(player.getId(), player.getTeam() == null ? -1 : player.getTeam().getId())); + } + handler.sendTcpMessage(ChatMessage.privateMessage("Your mode has been updated to " + mode.name() + ".")); + } + + public void setTeam(ServerPlayer player, Team team) { + if (Objects.equals(team, player.getTeam()) || player.getMode() != PlayerMode.NORMAL) return; + player.setTeam(team); + broadcastUdpMessage(new PlayerTeamUpdateMessage(player.getId(), team == null ? -1 : team.getId())); + respawn(player); + String chatMessage; + if (team != null) { + chatMessage = "%s has changed to the %s team.".formatted(player.getUsername(), team.getName()); + } else { + chatMessage = "%s has changed to not be on a team.".formatted(player.getUsername()); + } + broadcastTcpMessage(ChatMessage.announce(chatMessage)); } public void handleUdpInit(DatagramInit init, DatagramPacket packet) { diff --git a/server/src/main/java/nl/andrewl/aos2_server/TeamManager.java b/server/src/main/java/nl/andrewl/aos2_server/TeamManager.java index 532a3db..a0f745c 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/TeamManager.java +++ b/server/src/main/java/nl/andrewl/aos2_server/TeamManager.java @@ -33,6 +33,9 @@ public class TeamManager { for (var team : teams.values()) { if (team.getName().equals(ident)) return Optional.of(team); } + for (var team : teams.values()) {// Try again ignoring case. + if (team.getName().equalsIgnoreCase(ident)) return Optional.of(team); + } try { int id = Integer.parseInt(ident); for (var team : teams.values()) { diff --git a/server/src/main/java/nl/andrewl/aos2_server/cli/ingame/PlayerCommandHandler.java b/server/src/main/java/nl/andrewl/aos2_server/cli/ingame/PlayerCommandHandler.java index 8c5fadd..b14c862 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/cli/ingame/PlayerCommandHandler.java +++ b/server/src/main/java/nl/andrewl/aos2_server/cli/ingame/PlayerCommandHandler.java @@ -5,6 +5,7 @@ import nl.andrewl.aos2_server.Server; import nl.andrewl.aos2_server.cli.ingame.commands.KillCommand; import nl.andrewl.aos2_server.cli.ingame.commands.KillDeathRatioCommand; import nl.andrewl.aos2_server.cli.ingame.commands.PlayerModeCommand; +import nl.andrewl.aos2_server.cli.ingame.commands.TeamsCommand; import nl.andrewl.aos2_server.model.ServerPlayer; import nl.andrewl.aos_core.net.client.ChatMessage; @@ -27,6 +28,7 @@ public class PlayerCommandHandler { commands.put("kd", new KillDeathRatioCommand()); commands.put("kill", new KillCommand()); commands.put("mode", new PlayerModeCommand()); + commands.put("teams", new TeamsCommand()); } public void handle(String rawCommand, ServerPlayer player, ClientCommunicationHandler handler) { diff --git a/server/src/main/java/nl/andrewl/aos2_server/cli/ingame/commands/PlayerModeCommand.java b/server/src/main/java/nl/andrewl/aos2_server/cli/ingame/commands/PlayerModeCommand.java index 0aa84cd..1266e6f 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/cli/ingame/commands/PlayerModeCommand.java +++ b/server/src/main/java/nl/andrewl/aos2_server/cli/ingame/commands/PlayerModeCommand.java @@ -6,7 +6,6 @@ import nl.andrewl.aos2_server.cli.ingame.PlayerCommand; import nl.andrewl.aos2_server.model.ServerPlayer; import nl.andrewl.aos_core.model.PlayerMode; import nl.andrewl.aos_core.net.client.ChatMessage; -import nl.andrewl.aos_core.net.client.ClientInventoryMessage; public class PlayerModeCommand implements PlayerCommand { @Override @@ -19,9 +18,6 @@ public class PlayerModeCommand implements PlayerCommand { try { PlayerMode mode = PlayerMode.valueOf(modeText); server.getPlayerManager().setMode(player, mode); - handler.sendTcpMessage(new ClientInventoryMessage(player.getInventory())); - server.getPlayerManager().broadcastUdpMessage(player.getUpdateMessage(System.currentTimeMillis())); - handler.sendTcpMessage(ChatMessage.privateMessage("Your mode has been updated to " + mode.name() + ".")); } catch (IllegalArgumentException e) { handler.sendTcpMessage(ChatMessage.privateMessage("Invalid mode. Should be NORMAL, CREATIVE, or SPECTATOR.")); } diff --git a/server/src/main/java/nl/andrewl/aos2_server/cli/ingame/commands/TeamsCommand.java b/server/src/main/java/nl/andrewl/aos2_server/cli/ingame/commands/TeamsCommand.java new file mode 100644 index 0000000..f7461f6 --- /dev/null +++ b/server/src/main/java/nl/andrewl/aos2_server/cli/ingame/commands/TeamsCommand.java @@ -0,0 +1,37 @@ +package nl.andrewl.aos2_server.cli.ingame.commands; + +import nl.andrewl.aos2_server.ClientCommunicationHandler; +import nl.andrewl.aos2_server.Server; +import nl.andrewl.aos2_server.cli.ingame.PlayerCommand; +import nl.andrewl.aos2_server.model.ServerPlayer; +import nl.andrewl.aos_core.model.Team; +import nl.andrewl.aos_core.net.client.ChatMessage; + +import java.util.stream.Collectors; + +public class TeamsCommand implements PlayerCommand { + @Override + public void handle(String[] args, ServerPlayer player, ClientCommunicationHandler handler, Server server) { + if (args.length == 0) { + String teamsString = server.getTeamManager().getTeams().stream() + .map(Team::getName).collect(Collectors.joining(", ")); + handler.sendTcpMessage(ChatMessage.privateMessage(teamsString)); + } else { + String cmd = args[0].trim().toLowerCase(); + if (cmd.equals("set")) { + if (args.length >= 2) { + String teamIdent = args[1].trim(); + server.getTeamManager().findByIdOrName(teamIdent) + .ifPresentOrElse( + team -> server.getPlayerManager().setTeam(player, team), + () -> handler.sendTcpMessage(ChatMessage.privateMessage("Unknown team.")) + ); + } else { + handler.sendTcpMessage(ChatMessage.privateMessage("Missing required team identifier.")); + } + } else { + handler.sendTcpMessage(ChatMessage.privateMessage("Unknown subcommand.")); + } + } + } +} diff --git a/server/src/main/java/nl/andrewl/aos2_server/model/ServerPlayer.java b/server/src/main/java/nl/andrewl/aos2_server/model/ServerPlayer.java index 37666a8..c6de76a 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/model/ServerPlayer.java +++ b/server/src/main/java/nl/andrewl/aos2_server/model/ServerPlayer.java @@ -2,6 +2,8 @@ package nl.andrewl.aos2_server.model; import nl.andrewl.aos2_server.logic.PlayerActionManager; import nl.andrewl.aos_core.model.Player; +import nl.andrewl.aos_core.model.PlayerMode; +import nl.andrewl.aos_core.model.Team; import nl.andrewl.aos_core.model.item.Inventory; import nl.andrewl.aos_core.net.client.PlayerUpdateMessage; @@ -24,8 +26,8 @@ public class ServerPlayer extends Player { private int deathCount; private int killCount; - public ServerPlayer(int id, String username) { - super(id, username); + public ServerPlayer(int id, String username, Team team, PlayerMode mode) { + super(id, username, team, mode); this.inventory = new Inventory(new ArrayList<>(), 0); this.health = 1f; this.actionManager = new PlayerActionManager(this);