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();
|
playerSource = new SoundSource();
|
||||||
|
|
||||||
long lastFrameAt = System.currentTimeMillis();
|
long lastFrameAt = System.currentTimeMillis();
|
||||||
while (!gameRenderer.windowShouldClose()) {
|
while (!gameRenderer.windowShouldClose() && !communicationHandler.isDone()) {
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
float dt = (now - lastFrameAt) / 1000f;
|
float dt = (now - lastFrameAt) / 1000f;
|
||||||
world.processQueuedChunkUpdates();
|
world.processQueuedChunkUpdates();
|
||||||
|
|
|
@ -39,6 +39,7 @@ public class CommunicationHandler {
|
||||||
private ExtendedDataOutputStream out;
|
private ExtendedDataOutputStream out;
|
||||||
private ExtendedDataInputStream in;
|
private ExtendedDataInputStream in;
|
||||||
private int clientId;
|
private int clientId;
|
||||||
|
private boolean done;
|
||||||
|
|
||||||
public CommunicationHandler(Client client) {
|
public CommunicationHandler(Client client) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
@ -71,7 +72,7 @@ public class CommunicationHandler {
|
||||||
log.debug("Initial data received.");
|
log.debug("Initial data received.");
|
||||||
establishDatagramConnection();
|
establishDatagramConnection();
|
||||||
log.info("Connection to server established. My client id is {}.", clientId);
|
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();
|
new Thread(new UdpReceiver(datagramSocket, (msg, packet) -> client.onMessageReceived(msg))).start();
|
||||||
} else {
|
} else {
|
||||||
throw new IOException("Server returned an unexpected message: " + response);
|
throw new IOException("Server returned an unexpected message: " + response);
|
||||||
|
@ -84,9 +85,15 @@ public class CommunicationHandler {
|
||||||
datagramSocket.close();
|
datagramSocket.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
done = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isDone() {
|
||||||
|
return done;
|
||||||
|
}
|
||||||
|
|
||||||
public void sendMessage(Message msg) {
|
public void sendMessage(Message msg) {
|
||||||
try {
|
try {
|
||||||
Net.write(msg, out);
|
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 DatagramSocket socket;
|
||||||
private final UdpMessageHandler handler;
|
private final UdpMessageHandler handler;
|
||||||
|
private final Runnable shutdownHook;
|
||||||
|
|
||||||
public UdpReceiver(DatagramSocket socket, UdpMessageHandler handler) {
|
public UdpReceiver(DatagramSocket socket, UdpMessageHandler handler) {
|
||||||
|
this(socket, handler, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UdpReceiver(DatagramSocket socket, UdpMessageHandler handler, Runnable shutdownHook) {
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.handler = handler;
|
this.handler = handler;
|
||||||
|
this.shutdownHook = shutdownHook;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -39,12 +45,11 @@ public class UdpReceiver implements Runnable {
|
||||||
}
|
}
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
} catch (EOFException e) {
|
} catch (EOFException e) {
|
||||||
System.out.println("EOF!");
|
|
||||||
break;
|
break;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
System.out.println("UDP receiver shut down.");
|
if (shutdownHook != null) shutdownHook.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,17 @@
|
||||||
<artifactId>aos2-core</artifactId>
|
<artifactId>aos2-core</artifactId>
|
||||||
<version>${parent.version}</version>
|
<version>${parent.version}</version>
|
||||||
</dependency>
|
</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>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -2,6 +2,7 @@ package nl.andrewl.aos2_server;
|
||||||
|
|
||||||
import nl.andrewl.aos2_server.model.ServerPlayer;
|
import nl.andrewl.aos2_server.model.ServerPlayer;
|
||||||
import nl.andrewl.aos_core.Net;
|
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.item.ItemStack;
|
||||||
import nl.andrewl.aos_core.model.world.Chunk;
|
import nl.andrewl.aos_core.model.world.Chunk;
|
||||||
import nl.andrewl.aos_core.model.world.WorldIO;
|
import nl.andrewl.aos_core.model.world.WorldIO;
|
||||||
|
@ -91,6 +92,18 @@ public class ClientCommunicationHandler {
|
||||||
Message msg = Net.read(in);
|
Message msg = Net.read(in);
|
||||||
if (msg instanceof ConnectRequestMessage connectMsg) {
|
if (msg instanceof ConnectRequestMessage connectMsg) {
|
||||||
log.debug("Received connect request from player \"{}\"", connectMsg.username());
|
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.
|
// Try to set the TCP timeout back to 0 now that we've got the correct request.
|
||||||
socket.setSoTimeout(0);
|
socket.setSoTimeout(0);
|
||||||
this.clientAddress = socket.getInetAddress();
|
this.clientAddress = socket.getInetAddress();
|
||||||
|
|
|
@ -20,6 +20,7 @@ import org.slf4j.LoggerFactory;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.DatagramPacket;
|
import java.net.DatagramPacket;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component is responsible for managing the set of players connected to
|
* This component is responsible for managing the set of players connected to
|
||||||
|
@ -73,6 +74,21 @@ public class PlayerManager {
|
||||||
return players.get(id);
|
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() {
|
public Collection<ServerPlayer> getPlayers() {
|
||||||
return Collections.unmodifiableCollection(players.values());
|
return Collections.unmodifiableCollection(players.values());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package nl.andrewl.aos2_server;
|
package nl.andrewl.aos2_server;
|
||||||
|
|
||||||
|
import nl.andrewl.aos2_server.cli.ServerCli;
|
||||||
import nl.andrewl.aos2_server.config.ServerConfig;
|
import nl.andrewl.aos2_server.config.ServerConfig;
|
||||||
import nl.andrewl.aos2_server.logic.WorldUpdater;
|
import nl.andrewl.aos2_server.logic.WorldUpdater;
|
||||||
import nl.andrewl.aos2_server.model.ServerPlayer;
|
import nl.andrewl.aos2_server.model.ServerPlayer;
|
||||||
|
@ -60,6 +61,7 @@ public class Server implements Runnable {
|
||||||
while (running) {
|
while (running) {
|
||||||
acceptClientConnection();
|
acceptClientConnection();
|
||||||
}
|
}
|
||||||
|
log.info("Shutting down the server.");
|
||||||
playerManager.deregisterAll();
|
playerManager.deregisterAll();
|
||||||
worldUpdater.shutdown();
|
worldUpdater.shutdown();
|
||||||
datagramSocket.close(); // Shuts down the UdpReceiver.
|
datagramSocket.close(); // Shuts down the UdpReceiver.
|
||||||
|
@ -68,6 +70,20 @@ public class Server implements Runnable {
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(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) {
|
public void handleUdpMessage(Message msg, DatagramPacket packet) {
|
||||||
|
@ -136,6 +152,8 @@ public class Server implements Runnable {
|
||||||
configPaths.add(Path.of(args[0].trim()));
|
configPaths.add(Path.of(args[0].trim()));
|
||||||
}
|
}
|
||||||
ServerConfig cfg = Config.loadConfig(ServerConfig.class, configPaths, new ServerConfig());
|
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