Added ability to kick players.

This commit is contained in:
Andrew Lalis 2022-07-26 12:10:41 +02:00
parent 63bfc5557d
commit 0526527fff
12 changed files with 393 additions and 5 deletions

View File

@ -93,7 +93,7 @@ public class Client implements Runnable {
playerSource = new SoundSource();
long lastFrameAt = System.currentTimeMillis();
while (!gameRenderer.windowShouldClose()) {
while (!gameRenderer.windowShouldClose() && !communicationHandler.isDone()) {
long now = System.currentTimeMillis();
float dt = (now - lastFrameAt) / 1000f;
world.processQueuedChunkUpdates();

View File

@ -39,6 +39,7 @@ public class CommunicationHandler {
private ExtendedDataOutputStream out;
private ExtendedDataInputStream in;
private int clientId;
private boolean done;
public CommunicationHandler(Client client) {
this.client = client;
@ -71,7 +72,7 @@ public class CommunicationHandler {
log.debug("Initial data received.");
establishDatagramConnection();
log.info("Connection to server established. My client id is {}.", clientId);
new Thread(new TcpReceiver(in, client::onMessageReceived)).start();
new Thread(new TcpReceiver(in, client::onMessageReceived).withShutdownHook(this::shutdown)).start();
new Thread(new UdpReceiver(datagramSocket, (msg, packet) -> client.onMessageReceived(msg))).start();
} else {
throw new IOException("Server returned an unexpected message: " + response);
@ -84,9 +85,15 @@ public class CommunicationHandler {
datagramSocket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
done = true;
}
}
public boolean isDone() {
return done;
}
public void sendMessage(Message msg) {
try {
Net.write(msg, out);

View File

@ -0,0 +1,14 @@
package nl.andrewl.aos_core;
import java.util.regex.Pattern;
public class UsernameChecker {
private static final int MIN_LENGTH = 3;
private static final int MAX_LENGTH = 24;
private static final Pattern pattern = Pattern.compile("[a-zA-Z]+[a-zA-Z\\d-_]*");
public static boolean isValid(String username) {
if (username.length() < MIN_LENGTH || username.length() > MAX_LENGTH) return false;
return pattern.matcher(username).matches();
}
}

View File

@ -18,10 +18,16 @@ public class UdpReceiver implements Runnable {
private final DatagramSocket socket;
private final UdpMessageHandler handler;
private final Runnable shutdownHook;
public UdpReceiver(DatagramSocket socket, UdpMessageHandler handler) {
this(socket, handler, null);
}
public UdpReceiver(DatagramSocket socket, UdpMessageHandler handler, Runnable shutdownHook) {
this.socket = socket;
this.handler = handler;
this.shutdownHook = shutdownHook;
}
@Override
@ -39,12 +45,11 @@ public class UdpReceiver implements Runnable {
}
e.printStackTrace();
} catch (EOFException e) {
System.out.println("EOF!");
break;
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("UDP receiver shut down.");
if (shutdownHook != null) shutdownHook.run();
}
}

View File

@ -22,6 +22,17 @@
<artifactId>aos2-core</artifactId>
<version>${parent.version}</version>
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli-shell-jline3</artifactId>
<version>4.6.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.fusesource.jansi/jansi -->
<dependency>
<groupId>org.fusesource.jansi</groupId>
<artifactId>jansi</artifactId>
<version>2.4.0</version>
</dependency>
</dependencies>
<build>

View File

@ -2,6 +2,7 @@ package nl.andrewl.aos2_server;
import nl.andrewl.aos2_server.model.ServerPlayer;
import nl.andrewl.aos_core.Net;
import nl.andrewl.aos_core.UsernameChecker;
import nl.andrewl.aos_core.model.item.ItemStack;
import nl.andrewl.aos_core.model.world.Chunk;
import nl.andrewl.aos_core.model.world.WorldIO;
@ -91,6 +92,18 @@ public class ClientCommunicationHandler {
Message msg = Net.read(in);
if (msg instanceof ConnectRequestMessage connectMsg) {
log.debug("Received connect request from player \"{}\"", connectMsg.username());
// Ensure the connection is valid.
if (!UsernameChecker.isValid(connectMsg.username())) {
Net.write(new ConnectRejectMessage("Invalid username."), out);
socket.close();
return;
}
if (server.getPlayerManager().getPlayers().stream().anyMatch(p -> p.getUsername().equals(connectMsg.username()))) {
Net.write(new ConnectRejectMessage("Username is already taken."), out);
socket.close();
return;
}
// Try to set the TCP timeout back to 0 now that we've got the correct request.
socket.setSoTimeout(0);
this.clientAddress = socket.getInetAddress();

View File

@ -20,6 +20,7 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.DatagramPacket;
import java.util.*;
import java.util.regex.Pattern;
/**
* This component is responsible for managing the set of players connected to
@ -73,6 +74,21 @@ public class PlayerManager {
return players.get(id);
}
public Optional<ServerPlayer> findByIdOrName(String text) {
Pattern p = Pattern.compile("\\d+");
if (p.matcher(text).matches() && text.length() < 8) {
int id = Integer.parseInt(text);
return Optional.ofNullable(getPlayer(id));
} else {
String finalText = text.trim().toLowerCase();
List<ServerPlayer> matches = getPlayers().stream()
.filter(player -> player.getUsername().trim().toLowerCase().equals(finalText))
.toList();
if (matches.size() == 1) return Optional.of(matches.get(0));
return Optional.empty();
}
}
public Collection<ServerPlayer> getPlayers() {
return Collections.unmodifiableCollection(players.values());
}

View File

@ -1,5 +1,6 @@
package nl.andrewl.aos2_server;
import nl.andrewl.aos2_server.cli.ServerCli;
import nl.andrewl.aos2_server.config.ServerConfig;
import nl.andrewl.aos2_server.logic.WorldUpdater;
import nl.andrewl.aos2_server.model.ServerPlayer;
@ -60,6 +61,7 @@ public class Server implements Runnable {
while (running) {
acceptClientConnection();
}
log.info("Shutting down the server.");
playerManager.deregisterAll();
worldUpdater.shutdown();
datagramSocket.close(); // Shuts down the UdpReceiver.
@ -68,6 +70,20 @@ public class Server implements Runnable {
} catch (IOException e) {
throw new RuntimeException(e);
}
log.info("Shutdown complete.");
}
public boolean isRunning() {
return running;
}
public void shutdown() {
running = false;
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public void handleUdpMessage(Message msg, DatagramPacket packet) {
@ -136,6 +152,8 @@ public class Server implements Runnable {
configPaths.add(Path.of(args[0].trim()));
}
ServerConfig cfg = Config.loadConfig(ServerConfig.class, configPaths, new ServerConfig());
new Server(cfg).run();
Server server = new Server(cfg);
new Thread(server).start();
ServerCli.start(server);
}
}

View File

@ -0,0 +1,49 @@
package nl.andrewl.aos2_server.cli;
import nl.andrewl.aos2_server.model.ServerPlayer;
import nl.andrewl.aos_core.model.Player;
import picocli.CommandLine;
import java.util.Collection;
import java.util.Comparator;
@CommandLine.Command(
name = "players",
description = "Commands for interacting with the players on the server.",
mixinStandardHelpOptions = true
)
public class PlayersCommand {
@CommandLine.ParentCommand ServerCli cli;
@CommandLine.Command(description = "Lists all online players.")
public void list() {
var playerManager = cli.server.getPlayerManager();
Collection<ServerPlayer> players = playerManager.getPlayers();
if (players.isEmpty()) {
cli.out.println("There are no players connected to the server.");
} else {
TablePrinter tp = new TablePrinter(cli.out)
.drawBorders(true)
.addLine("Id", "Username", "Health", "Position", "Held Item", "Team");
players.stream().sorted(Comparator.comparing(Player::getId)).forEachOrdered(player -> {
tp.addLine(
player.getId(),
player.getUsername(),
String.format("%.2f / 1.00", player.getHealth()),
String.format("x=%.2f, y=%.2f, z=%.2f", player.getPosition().x, player.getPosition().y, player.getPosition().z),
player.getInventory().getSelectedItemStack().getType().getName(),
player.getTeam() == null ? "None" : player.getTeam().getName()
);
});
tp.println();
}
}
@CommandLine.Command(description = "Kicks a player from the server.")
public void kick(@CommandLine.Parameters(description = "The id or name of the player to kick.") String playerIdent) {
cli.server.getPlayerManager().findByIdOrName(playerIdent)
.ifPresentOrElse(player -> {
cli.server.getPlayerManager().deregister(player);
}, () -> cli.out.println("Player not found."));
}
}

View File

@ -0,0 +1,102 @@
package nl.andrewl.aos2_server.cli;
import nl.andrewl.aos2_server.Server;
import org.fusesource.jansi.AnsiConsole;
import org.jline.console.SystemRegistry;
import org.jline.console.impl.Builtins;
import org.jline.console.impl.SystemRegistryImpl;
import org.jline.keymap.KeyMap;
import org.jline.reader.*;
import org.jline.reader.impl.DefaultParser;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.widget.TailTipWidgets;
import picocli.CommandLine;
import picocli.shell.jline3.PicocliCommands;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Path;
@CommandLine.Command(
name = "",
description = "Interactive shell for server commands.",
footer = {"", "Press Ctrl-D to exit."},
subcommands = {StopCommand.class, PlayersCommand.class}
)
public class ServerCli implements Runnable {
final Server server;
PrintWriter out;
public ServerCli(Server server) {
this.server = server;
}
public void setReader(LineReader reader) {
this.out = reader.getTerminal().writer();
}
@Override
public void run() {
out.println(new CommandLine(this).getUsageMessage());
}
public static void start(Server server) {
AnsiConsole.systemInstall();
Builtins builtins = new Builtins(Path.of("."), null, null);
builtins.rename(Builtins.Command.TTOP, "top");
builtins.alias("zle", "widget");
builtins.alias("bindkey", "keymap");
ServerCli baseCommand = new ServerCli(server);
PicocliCommands.PicocliCommandsFactory commandsFactory = new PicocliCommands.PicocliCommandsFactory();
CommandLine cmd = new CommandLine(baseCommand, commandsFactory);
PicocliCommands picocliCommands = new PicocliCommands(cmd);
picocliCommands.commandDescription("bleh");
Parser parser = new DefaultParser();
try (Terminal terminal = TerminalBuilder.builder().build()) {
SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, () -> Path.of("."), null);
systemRegistry.setCommandRegistries(builtins, picocliCommands);
systemRegistry.register("help", picocliCommands);
LineReader reader = LineReaderBuilder.builder()
.terminal(terminal)
.completer(systemRegistry.completer())
.parser(parser)
.variable(LineReader.LIST_MAX, 50)
.build();
builtins.setLineReader(reader);
baseCommand.setReader(reader);
commandsFactory.setTerminal(terminal);
TailTipWidgets widgets = new TailTipWidgets(reader, systemRegistry::commandDescription, 5, TailTipWidgets.TipType.COMPLETER);
widgets.enable();
KeyMap<Binding> keyMap = reader.getKeyMaps().get("main");
keyMap.bind(new Reference("tailtip-toggle"), KeyMap.alt("s"));
String prompt = "server> ";
String rightPrompt = "HI";
String line;
while (server.isRunning()) {
try {
systemRegistry.cleanUp();
line = reader.readLine(prompt, rightPrompt, (MaskingCallback) null, null);
systemRegistry.execute(line);
} catch (UserInterruptException e) {
// Ignore
} catch (EndOfFileException e) {
break;
} catch (Exception e) {
systemRegistry.trace(e);
}
}
// If the user exits the CLI without calling "stop", we will shut down the server automatically.
if (server.isRunning()) {
server.shutdown();
}
} catch (IOException e) {
e.printStackTrace();
}
AnsiConsole.systemUninstall();
}
}

View File

@ -0,0 +1,17 @@
package nl.andrewl.aos2_server.cli;
import picocli.CommandLine;
@CommandLine.Command(
name = "stop",
description = "Stops the server.",
mixinStandardHelpOptions = true
)
public class StopCommand implements Runnable {
@CommandLine.ParentCommand ServerCli cli;
@Override
public void run() {
cli.server.shutdown();
}
}

View File

@ -0,0 +1,136 @@
package nl.andrewl.aos2_server.cli;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
public class TablePrinter {
private final PrintWriter out;
private final List<List<String>> lines = new ArrayList<>();
private boolean drawBorders = false;
private final int[] padding = new int[]{1, 1, 0, 0};
public TablePrinter(PrintWriter out) {
this.out = out;
}
public TablePrinter addLine(List<String> line) {
lines.add(line);
return this;
}
public TablePrinter addLine(Object... objects) {
List<String> line = new ArrayList<>(objects.length);
for (var obj : objects) {
line.add(obj.toString());
}
return addLine(line);
}
public TablePrinter drawBorders(boolean drawBorders) {
this.drawBorders = drawBorders;
return this;
}
public TablePrinter padding(int left, int right, int top, int bottom) {
this.padding[0] = left;
this.padding[1] = right;
this.padding[2] = top;
this.padding[3] = bottom;
return this;
}
public void println() {
int rowCount = lines.size();
int colCount = lines.stream().mapToInt(List::size).max().orElse(0);
if (rowCount == 0 || colCount == 0) out.println();
int[] colSizes = new int[colCount];
for (int i = 0; i < colCount; i++) {
final int columnIndex = i;
colSizes[i] = lines.stream().mapToInt(line -> {
if (columnIndex >= line.size()) return 0;
return line.get(columnIndex).length();
}).max().orElse(0);
}
for (int row = 0; row < rowCount; row++) {
// Row top border.
if (drawBorders) {
out.print('+');
for (int col = 0; col < colCount; col++) {
out.print("-".repeat(colSizes[col] + leftPadding() + rightPadding()));
out.print('+');
}
out.println();
}
// Top padding rows.
for (int p = 0; p < topPadding(); p++) {
for (int col = 0; col < colCount; col++) {
if (drawBorders) out.print('|');
out.print(" ".repeat(leftPadding() + colSizes[col] + rightPadding()));
}
if (drawBorders) out.print('|');
out.println();
}
// Column values.
for (int col = 0; col < colCount; col++) {
String value = getValueAt(row, col);
if (drawBorders) out.print('|');
out.print(" ".repeat(leftPadding()));
out.print(value);
out.print(" ".repeat(colSizes[col] - value.length() + rightPadding()));
}
if (drawBorders) out.print('|');
out.println();
// Bottom padding rows.
for (int p = 0; p < bottomPadding(); p++) {
for (int col = 0; col < colCount; col++) {
if (drawBorders) out.print('|');
out.print(" ".repeat(leftPadding() + colSizes[col] + rightPadding()));
}
if (drawBorders) out.print('|');
out.println();
}
// Last row bottom border.
if (row == rowCount - 1 && drawBorders) {
out.print('+');
for (int col = 0; col < colCount; col++) {
out.print("-".repeat(colSizes[col] + leftPadding() + rightPadding()));
out.print('+');
}
out.println();
}
}
}
private String getValueAt(int row, int col) {
if (row < lines.size()) {
List<String> line = lines.get(row);
if (col < line.size()) {
return line.get(col);
} else {
return "";
}
} else {
return "";
}
}
private int leftPadding() {
return padding[0];
}
private int rightPadding() {
return padding[1];
}
private int topPadding() {
return padding[2];
}
private int bottomPadding() {
return padding[3];
}
}