Added CLI for server commands.

This commit is contained in:
Andrew Lalis 2021-06-20 15:48:45 +02:00
parent a4d2730c80
commit 94f13c0753
13 changed files with 302 additions and 16 deletions

View File

@ -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) {

View File

@ -16,4 +16,14 @@
<maven.compiler.target>16</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
</build>
</project>

View File

@ -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<Player> {
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;
}
}

View File

@ -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) {

View File

@ -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();
}
}

View File

@ -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<String, Command> 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();
}
}
}
}

View File

@ -0,0 +1,5 @@
package nl.andrewlalis.aos_server.command;
public interface Command {
void execute(String[] args);
}

View File

@ -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());
}
}
}

View File

@ -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<Player> 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() + ".");
}
}
}

View File

@ -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);
}
}

View File

@ -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.");
}
}

View File

@ -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();
}
}

View File

@ -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 <p> 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.