Added CLI for server commands.
This commit is contained in:
parent
a4d2730c80
commit
94f13c0753
|
@ -4,10 +4,7 @@ import nl.andrewlalis.aos_core.model.World;
|
||||||
import nl.andrewlalis.aos_core.net.*;
|
import nl.andrewlalis.aos_core.net.*;
|
||||||
import nl.andrewlalis.aos_core.net.chat.ChatMessage;
|
import nl.andrewlalis.aos_core.net.chat.ChatMessage;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.*;
|
||||||
import java.io.ObjectInputStream;
|
|
||||||
import java.io.ObjectOutputStream;
|
|
||||||
import java.io.StreamCorruptedException;
|
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
|
|
||||||
|
@ -66,7 +63,7 @@ public class MessageTransceiver extends Thread {
|
||||||
World world = ((WorldUpdateMessage) msg).getWorld();
|
World world = ((WorldUpdateMessage) msg).getWorld();
|
||||||
this.client.setWorld(world);
|
this.client.setWorld(world);
|
||||||
}
|
}
|
||||||
} catch (StreamCorruptedException e) {
|
} catch (StreamCorruptedException | EOFException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
this.running = false;
|
this.running = false;
|
||||||
} catch (SocketException e) {
|
} catch (SocketException e) {
|
||||||
|
|
10
core/pom.xml
10
core/pom.xml
|
@ -16,4 +16,14 @@
|
||||||
<maven.compiler.target>16</maven.compiler.target>
|
<maven.compiler.target>16</maven.compiler.target>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.8.1</version>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
|
@ -5,7 +5,7 @@ import nl.andrewlalis.aos_core.model.tools.Gun;
|
||||||
|
|
||||||
import java.util.Objects;
|
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 MOVEMENT_SPEED = 10; // Movement speed, in m/s
|
||||||
public static final double RADIUS = 0.5; // Collision radius, in meters.
|
public static final double RADIUS = 0.5; // Collision radius, in meters.
|
||||||
public static final double RESUPPLY_COOLDOWN = 30; // Seconds between allowing resupply.
|
public static final double RESUPPLY_COOLDOWN = 30; // Seconds between allowing resupply.
|
||||||
|
@ -146,4 +146,11 @@ public class Player extends PhysicsObject {
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(getId());
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package nl.andrewlalis.aos_server;
|
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 nl.andrewlalis.aos_core.net.chat.ChatMessage;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -10,6 +13,9 @@ import java.net.Socket;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thread which handles communicating with a single client socket connection.
|
||||||
|
*/
|
||||||
public class ClientHandler extends Thread {
|
public class ClientHandler extends Thread {
|
||||||
private final ExecutorService sendingQueue = Executors.newSingleThreadExecutor();
|
private final ExecutorService sendingQueue = Executors.newSingleThreadExecutor();
|
||||||
private final Server server;
|
private final Server server;
|
||||||
|
@ -34,6 +40,13 @@ public class ClientHandler extends Thread {
|
||||||
|
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
this.running = false;
|
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) {
|
public void send(Message message) {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import java.awt.*;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
import java.net.SocketException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Scanner;
|
import java.util.Scanner;
|
||||||
|
@ -27,10 +28,14 @@ public class Server {
|
||||||
private final ServerSocket serverSocket;
|
private final ServerSocket serverSocket;
|
||||||
private final World world;
|
private final World world;
|
||||||
private final WorldUpdater worldUpdater;
|
private final WorldUpdater worldUpdater;
|
||||||
|
private final ServerCli cli;
|
||||||
|
|
||||||
|
private volatile boolean running;
|
||||||
|
|
||||||
public Server(int port) throws IOException {
|
public Server(int port) throws IOException {
|
||||||
this.clientHandlers = new CopyOnWriteArrayList<>();
|
this.clientHandlers = new CopyOnWriteArrayList<>();
|
||||||
this.serverSocket = new ServerSocket(port);
|
this.serverSocket = new ServerSocket(port);
|
||||||
|
this.cli = new ServerCli(this);
|
||||||
|
|
||||||
this.world = new World(new Vec2(50, 70));
|
this.world = new World(new Vec2(50, 70));
|
||||||
world.getBarricades().add(new Barricade(10, 10, 30, 5));
|
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);
|
System.out.println("Started AOS-Server TCP on port " + port);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void acceptClientConnection() throws IOException {
|
public World getWorld() {
|
||||||
|
return world;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void acceptClientConnection() {
|
||||||
|
try {
|
||||||
Socket socket = this.serverSocket.accept();
|
Socket socket = this.serverSocket.accept();
|
||||||
var t = new ClientHandler(this, socket);
|
var t = new ClientHandler(this, socket);
|
||||||
t.start();
|
t.start();
|
||||||
this.clientHandlers.add(t);
|
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) {
|
public int registerNewPlayer(String name, ClientHandler handler) {
|
||||||
|
@ -105,6 +121,15 @@ public class Server {
|
||||||
System.out.println(message);
|
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() {
|
public void sendWorldToClients() {
|
||||||
for (ClientHandler handler : this.clientHandlers) {
|
for (ClientHandler handler : this.clientHandlers) {
|
||||||
handler.send(new WorldUpdateMessage(this.world));
|
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 {
|
public static void main(String[] args) throws IOException {
|
||||||
|
@ -184,9 +235,6 @@ public class Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
Server server = new Server(port);
|
Server server = new Server(port);
|
||||||
server.worldUpdater.start();
|
server.run();
|
||||||
while (true) {
|
|
||||||
server.acceptClientConnection();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package nl.andrewlalis.aos_server.command;
|
||||||
|
|
||||||
|
public interface Command {
|
||||||
|
void execute(String[] args);
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
Loading…
Reference in New Issue