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.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) {
|
||||
|
|
10
core/pom.xml
10
core/pom.xml
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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