diff --git a/client/src/main/java/nl/andrewlalis/aos_client/MessageTransceiver.java b/client/src/main/java/nl/andrewlalis/aos_client/MessageTransceiver.java index d4ac63a..2472f12 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/MessageTransceiver.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/MessageTransceiver.java @@ -4,10 +4,7 @@ import nl.andrewlalis.aos_core.model.World; import nl.andrewlalis.aos_core.net.*; import nl.andrewlalis.aos_core.net.chat.ChatMessage; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.StreamCorruptedException; +import java.io.*; import java.net.Socket; import java.net.SocketException; @@ -66,7 +63,7 @@ public class MessageTransceiver extends Thread { World world = ((WorldUpdateMessage) msg).getWorld(); this.client.setWorld(world); } - } catch (StreamCorruptedException e) { + } catch (StreamCorruptedException | EOFException e) { e.printStackTrace(); this.running = false; } catch (SocketException e) { diff --git a/core/pom.xml b/core/pom.xml index 38e6a52..6fda464 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -16,4 +16,14 @@ 16 + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + + \ No newline at end of file diff --git a/core/src/main/java/nl/andrewlalis/aos_core/model/Player.java b/core/src/main/java/nl/andrewlalis/aos_core/model/Player.java index ce577b6..c69b698 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/model/Player.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/model/Player.java @@ -5,7 +5,7 @@ import nl.andrewlalis.aos_core.model.tools.Gun; import java.util.Objects; -public class Player extends PhysicsObject { +public class Player extends PhysicsObject implements Comparable { public static final double MOVEMENT_SPEED = 10; // Movement speed, in m/s public static final double RADIUS = 0.5; // Collision radius, in meters. public static final double RESUPPLY_COOLDOWN = 30; // Seconds between allowing resupply. @@ -146,4 +146,11 @@ public class Player extends PhysicsObject { public int hashCode() { return Objects.hash(getId()); } + + @Override + public int compareTo(Player o) { + int r = this.name.compareTo(o.getName()); + if (r == 0) return Integer.compare(this.id, o.getId()); + return r; + } } diff --git a/server/src/main/java/nl/andrewlalis/aos_server/ClientHandler.java b/server/src/main/java/nl/andrewlalis/aos_server/ClientHandler.java index 0806f53..21269ea 100644 --- a/server/src/main/java/nl/andrewlalis/aos_server/ClientHandler.java +++ b/server/src/main/java/nl/andrewlalis/aos_server/ClientHandler.java @@ -1,6 +1,9 @@ package nl.andrewlalis.aos_server; -import nl.andrewlalis.aos_core.net.*; +import nl.andrewlalis.aos_core.net.IdentMessage; +import nl.andrewlalis.aos_core.net.Message; +import nl.andrewlalis.aos_core.net.PlayerControlStateMessage; +import nl.andrewlalis.aos_core.net.Type; import nl.andrewlalis.aos_core.net.chat.ChatMessage; import java.io.IOException; @@ -10,6 +13,9 @@ import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +/** + * Thread which handles communicating with a single client socket connection. + */ public class ClientHandler extends Thread { private final ExecutorService sendingQueue = Executors.newSingleThreadExecutor(); private final Server server; @@ -34,6 +40,13 @@ public class ClientHandler extends Thread { public void shutdown() { this.running = false; + try { + this.in.close(); + this.out.close(); + this.socket.close(); + } catch (IOException e) { + System.err.println("Could not close streams when shutting down client handler for player " + this.playerId + ": " + e.getMessage()); + } } public void send(Message message) { diff --git a/server/src/main/java/nl/andrewlalis/aos_server/Server.java b/server/src/main/java/nl/andrewlalis/aos_server/Server.java index fc6635c..843ff86 100644 --- a/server/src/main/java/nl/andrewlalis/aos_server/Server.java +++ b/server/src/main/java/nl/andrewlalis/aos_server/Server.java @@ -14,6 +14,7 @@ import java.awt.*; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; +import java.net.SocketException; import java.util.Arrays; import java.util.List; import java.util.Scanner; @@ -27,10 +28,14 @@ public class Server { private final ServerSocket serverSocket; private final World world; private final WorldUpdater worldUpdater; + private final ServerCli cli; + + private volatile boolean running; public Server(int port) throws IOException { this.clientHandlers = new CopyOnWriteArrayList<>(); this.serverSocket = new ServerSocket(port); + this.cli = new ServerCli(this); this.world = new World(new Vec2(50, 70)); world.getBarricades().add(new Barricade(10, 10, 30, 5)); @@ -58,11 +63,22 @@ public class Server { System.out.println("Started AOS-Server TCP on port " + port); } - public void acceptClientConnection() throws IOException { - Socket socket = this.serverSocket.accept(); - var t = new ClientHandler(this, socket); - t.start(); - this.clientHandlers.add(t); + public World getWorld() { + return world; + } + + public void acceptClientConnection() { + try { + Socket socket = this.serverSocket.accept(); + var t = new ClientHandler(this, socket); + t.start(); + this.clientHandlers.add(t); + } catch (IOException e) { + if (e instanceof SocketException && !this.running && e.getMessage().equalsIgnoreCase("Socket closed")) { + return; // Ignore this exception, since it is expected on shutdown. + } + e.printStackTrace(); + } } public int registerNewPlayer(String name, ClientHandler handler) { @@ -105,6 +121,15 @@ public class Server { System.out.println(message); } + public void kickPlayer(Player player) { + for (ClientHandler handler : this.clientHandlers) { + if (handler.getPlayerId() == player.getId()) { + handler.shutdown(); + return; + } + } + } + public void sendWorldToClients() { for (ClientHandler handler : this.clientHandlers) { handler.send(new WorldUpdateMessage(this.world)); @@ -167,6 +192,32 @@ public class Server { } } + public void shutdown() { + this.running = false; + try { + this.serverSocket.close(); + for (ClientHandler handler : this.clientHandlers) { + handler.shutdown(); + } + } catch (IOException e) { + System.err.println("Could not close server socket on shutdown: " + e.getMessage()); + } + } + + public void run() { + this.running = true; + this.worldUpdater.start(); + this.cli.start(); + while (this.running) { + this.acceptClientConnection(); + } + System.out.println("Stopped accepting new client connections."); + this.worldUpdater.shutdown(); + System.out.println("Stopped world updater."); + this.cli.shutdown(); + System.out.println("Stopped CLI interface."); + } + public static void main(String[] args) throws IOException { @@ -184,9 +235,6 @@ public class Server { } Server server = new Server(port); - server.worldUpdater.start(); - while (true) { - server.acceptClientConnection(); - } + server.run(); } } diff --git a/server/src/main/java/nl/andrewlalis/aos_server/ServerCli.java b/server/src/main/java/nl/andrewlalis/aos_server/ServerCli.java new file mode 100644 index 0000000..2c61574 --- /dev/null +++ b/server/src/main/java/nl/andrewlalis/aos_server/ServerCli.java @@ -0,0 +1,65 @@ +package nl.andrewlalis.aos_server; + +import nl.andrewlalis.aos_server.command.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Command-line interface for issuing commands to the AOS server at runtime. + */ +public class ServerCli extends Thread { + private final Map commands = new HashMap<>(); + + private final BufferedReader reader; + + private volatile boolean running; + + public ServerCli(Server server) { + this.reader = new BufferedReader(new InputStreamReader(System.in)); + this.commands.put("reset", new ResetCommand(server)); + this.commands.put("help", new HelpCommand()); + this.commands.put("stop", new StopCommand(server)); + + this.commands.put("list", new ListPlayersCommand(server)); + this.commands.put("kick", new KickCommand(server)); + } + + public void shutdown() { + this.running = false; + try { + this.reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void run() { + this.running = true; + String input; + System.out.println("Server command-line-interface initialized. Type \"help\" for more information."); + while (this.running) { + try { + input = reader.readLine(); + String[] words = input.split("\\s+"); + if (words.length == 0) continue; + String command = words[0].toLowerCase(); + String[] args = Arrays.copyOfRange(words, 1, words.length); + Command cmd = this.commands.get(command); + if (cmd == null) { + System.out.println("Unknown command."); + } else { + cmd.execute(args); + if (command.equals("stop")) this.running = false; // Needed to exit and avoid a blocking read. + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/server/src/main/java/nl/andrewlalis/aos_server/command/Command.java b/server/src/main/java/nl/andrewlalis/aos_server/command/Command.java new file mode 100644 index 0000000..de6eed9 --- /dev/null +++ b/server/src/main/java/nl/andrewlalis/aos_server/command/Command.java @@ -0,0 +1,5 @@ +package nl.andrewlalis.aos_server.command; + +public interface Command { + void execute(String[] args); +} diff --git a/server/src/main/java/nl/andrewlalis/aos_server/command/HelpCommand.java b/server/src/main/java/nl/andrewlalis/aos_server/command/HelpCommand.java new file mode 100644 index 0000000..7818407 --- /dev/null +++ b/server/src/main/java/nl/andrewlalis/aos_server/command/HelpCommand.java @@ -0,0 +1,18 @@ +package nl.andrewlalis.aos_server.command; + +import java.io.IOException; +import java.io.InputStream; + +public class HelpCommand implements Command { + @Override + public void execute(String[] args) { + try { + InputStream is = HelpCommand.class.getClassLoader().getResourceAsStream("help.txt"); + if (is == null) throw new IOException("Could not load help.txt."); + String helpMessage = new String(is.readAllBytes()); + System.out.println(helpMessage); + } catch (IOException e) { + System.err.println("Could not load help information: " + e.getMessage()); + } + } +} diff --git a/server/src/main/java/nl/andrewlalis/aos_server/command/KickCommand.java b/server/src/main/java/nl/andrewlalis/aos_server/command/KickCommand.java new file mode 100644 index 0000000..52fded9 --- /dev/null +++ b/server/src/main/java/nl/andrewlalis/aos_server/command/KickCommand.java @@ -0,0 +1,39 @@ +package nl.andrewlalis.aos_server.command; + +import nl.andrewlalis.aos_core.model.Player; +import nl.andrewlalis.aos_server.Server; + +import java.util.ArrayList; +import java.util.List; + +public class KickCommand implements Command { + private final Server server; + + public KickCommand(Server server) { + this.server = server; + } + + @Override + public void execute(String[] args) { + if (args.length < 1) { + System.out.println("Missing player id/name argument."); + return; + } + String query = args[0].trim(); + List matchingPlayers = new ArrayList<>(); + for (var p : this.server.getWorld().getPlayers().values()) { + if (Integer.toString(p.getId()).equals(query) || p.getName().equals(query)) { + matchingPlayers.add(p); + } + } + if (matchingPlayers.isEmpty()) { + System.out.println("No matching players found."); + } else if (matchingPlayers.size() > 1) { + System.out.println("More than one matching player found."); + } else { + Player player = matchingPlayers.get(0); + this.server.kickPlayer(player); + System.out.println("Kicked player " + player.getName() + "."); + } + } +} diff --git a/server/src/main/java/nl/andrewlalis/aos_server/command/ListPlayersCommand.java b/server/src/main/java/nl/andrewlalis/aos_server/command/ListPlayersCommand.java new file mode 100644 index 0000000..431e39e --- /dev/null +++ b/server/src/main/java/nl/andrewlalis/aos_server/command/ListPlayersCommand.java @@ -0,0 +1,35 @@ +package nl.andrewlalis.aos_server.command; + +import nl.andrewlalis.aos_core.model.Player; +import nl.andrewlalis.aos_server.Server; + +import java.util.stream.Collectors; + +public class ListPlayersCommand implements Command { + private final Server server; + + public ListPlayersCommand(Server server) { + this.server = server; + } + + @Override + public void execute(String[] args) { + if (this.server.getWorld().getPlayers().isEmpty()) { + System.out.println("There are no players currently online."); + return; + } + String message = this.server.getWorld().getPlayers().values().stream() + .sorted() + .map(player -> String.format( + "%d | %s Team: %s, Health: %.1f / %.1f, Gun: %s", + player.getId(), + player.getName(), + player.getTeam() == null ? "none" : player.getTeam().getName(), + player.getHealth(), + Player.MAX_HEALTH, + player.getGun().getType().name() + )) + .collect(Collectors.joining("\n")); + System.out.println(message); + } +} diff --git a/server/src/main/java/nl/andrewlalis/aos_server/command/ResetCommand.java b/server/src/main/java/nl/andrewlalis/aos_server/command/ResetCommand.java new file mode 100644 index 0000000..9394298 --- /dev/null +++ b/server/src/main/java/nl/andrewlalis/aos_server/command/ResetCommand.java @@ -0,0 +1,17 @@ +package nl.andrewlalis.aos_server.command; + +import nl.andrewlalis.aos_server.Server; + +public class ResetCommand implements Command { + private final Server server; + + public ResetCommand(Server server) { + this.server = server; + } + + @Override + public void execute(String[] args) { + this.server.resetGame(); + System.out.println("Reset the game."); + } +} diff --git a/server/src/main/java/nl/andrewlalis/aos_server/command/StopCommand.java b/server/src/main/java/nl/andrewlalis/aos_server/command/StopCommand.java new file mode 100644 index 0000000..cbc95e0 --- /dev/null +++ b/server/src/main/java/nl/andrewlalis/aos_server/command/StopCommand.java @@ -0,0 +1,16 @@ +package nl.andrewlalis.aos_server.command; + +import nl.andrewlalis.aos_server.Server; + +public class StopCommand implements Command { + private final Server server; + + public StopCommand(Server server) { + this.server = server; + } + + @Override + public void execute(String[] args) { + this.server.shutdown(); + } +} diff --git a/server/src/main/resources/help.txt b/server/src/main/resources/help.txt new file mode 100644 index 0000000..e9a7d52 --- /dev/null +++ b/server/src/main/resources/help.txt @@ -0,0 +1,16 @@ +Ace of Shades - Server CLI Help +------------------------------- + +This command-line interface is used to issue commands while the server is +running, to change the state of the game or configuration options, without +having to restart. + +The following commands are available: + +stop Stops the server, disconnecting all clients. +reset Resets the server by respawning all players and resets scores. +help Shows this help message. + +list Show a list of all connected players. +kick

Kick a player with the given id or name. If more than one player + exists with a given name, you need to use their unique id.