Added ability to kick players.
This commit is contained in:
parent
63bfc5557d
commit
0526527fff
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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."));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue