diff --git a/client/pom.xml b/client/pom.xml
index f76ad7a..6473851 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -210,7 +210,6 @@
lwjgl-stb
${lwjgl.natives}
-
@@ -220,7 +219,7 @@
- nl.andrewl.aos2_client.Aos2Client
+ nl.andrewl.aos2_client.Client
diff --git a/client/src/main/java/nl/andrewl/aos2_client/Aos2Client.java b/client/src/main/java/nl/andrewl/aos2_client/Client.java
similarity index 97%
rename from client/src/main/java/nl/andrewl/aos2_client/Aos2Client.java
rename to client/src/main/java/nl/andrewl/aos2_client/Client.java
index b840338..69a0705 100644
--- a/client/src/main/java/nl/andrewl/aos2_client/Aos2Client.java
+++ b/client/src/main/java/nl/andrewl/aos2_client/Client.java
@@ -14,7 +14,7 @@ import java.util.Random;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL46.*;
-public class Aos2Client {
+public class Client {
public static void main(String[] args) {
var windowInfo = initUI();
long windowHandle = windowInfo.windowHandle();
@@ -76,7 +76,7 @@ public class Aos2Client {
var vidMode = glfwGetVideoMode(glfwGetPrimaryMonitor());
if (vidMode == null) throw new IllegalStateException("Could not get information about the primary monitory.");
- long windowHandle = glfwCreateWindow(vidMode.width(), vidMode.height(), "Ace of Shades 2", 0, 0);
+ long windowHandle = glfwCreateWindow(vidMode.width(), vidMode.height(), "Ace of Shades 2", glfwGetPrimaryMonitor(), 0);
if (windowHandle == 0) throw new RuntimeException("Failed to create GLFW window.");
glfwSetKeyCallback(windowHandle, (window, key, scancode, action, mods) -> {
diff --git a/core/pom.xml b/core/pom.xml
index 6df9073..390aea1 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -16,6 +16,13 @@
17
+
+
+ jitpack.io
+ https://jitpack.io
+
+
+
@@ -23,6 +30,11 @@
joml
1.10.4
+
+ com.github.andrewlalis
+ record-net
+ v1.2.1
+
diff --git a/core/src/main/java/nl/andrewl/aos_core/Net.java b/core/src/main/java/nl/andrewl/aos_core/Net.java
new file mode 100644
index 0000000..f86a1f9
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/aos_core/Net.java
@@ -0,0 +1,48 @@
+package nl.andrewl.aos_core;
+
+import nl.andrewl.aos_core.net.PlayerConnectRequestMessage;
+import nl.andrewl.record_net.Message;
+import nl.andrewl.record_net.Serializer;
+import nl.andrewl.record_net.util.ExtendedDataInputStream;
+import nl.andrewl.record_net.util.ExtendedDataOutputStream;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Common wrapper for message serialization. All methods in this class are
+ * thread-safe and meant for general use with any input or output streams.
+ */
+public final class Net {
+ private Net() {}
+
+ private static final Serializer serializer = new Serializer();
+ static {
+ serializer.registerType(1, PlayerConnectRequestMessage.class);
+ }
+
+ public static ExtendedDataInputStream getInputStream(InputStream in) {
+ return new ExtendedDataInputStream(serializer, in);
+ }
+
+ public static ExtendedDataOutputStream getOutputStream(OutputStream out) {
+ return new ExtendedDataOutputStream(serializer, out);
+ }
+
+ public static void write(Message msg, ExtendedDataOutputStream out) throws IOException {
+ serializer.writeMessage(msg, out);
+ }
+
+ public static byte[] write(Message msg) throws IOException {
+ return serializer.writeMessage(msg);
+ }
+
+ public static Message read(ExtendedDataInputStream in) throws IOException {
+ return serializer.readMessage(in);
+ }
+
+ public static Message read(byte[] data) throws IOException {
+ return serializer.readMessage(data);
+ }
+}
diff --git a/core/src/main/java/nl/andrewl/aos_core/net/PlayerConnectRejectMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/PlayerConnectRejectMessage.java
new file mode 100644
index 0000000..54f774c
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/aos_core/net/PlayerConnectRejectMessage.java
@@ -0,0 +1,5 @@
+package nl.andrewl.aos_core.net;
+
+import nl.andrewl.record_net.Message;
+
+public record PlayerConnectRejectMessage (String reason) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/aos_core/net/PlayerConnectRequestMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/PlayerConnectRequestMessage.java
new file mode 100644
index 0000000..077f9fc
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/aos_core/net/PlayerConnectRequestMessage.java
@@ -0,0 +1,8 @@
+package nl.andrewl.aos_core.net;
+
+import nl.andrewl.record_net.Message;
+
+public record PlayerConnectRequestMessage (
+ String username,
+ int udpPort
+) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/aos_core/net/udp/InitPacket.java b/core/src/main/java/nl/andrewl/aos_core/net/udp/InitPacket.java
new file mode 100644
index 0000000..43bb300
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/aos_core/net/udp/InitPacket.java
@@ -0,0 +1,5 @@
+package nl.andrewl.aos_core.net.udp;
+
+import nl.andrewl.record_net.Message;
+
+public record InitPacket () implements Message {}
diff --git a/design/net.md b/design/net.md
new file mode 100644
index 0000000..828efdc
--- /dev/null
+++ b/design/net.md
@@ -0,0 +1,12 @@
+# AOS-2 Network Protocol
+This document describes the network protocol used by Ace of Shades 2 for server-client communication.
+
+All communications, whether they be UDP or TCP, use the [record-net](https://github.com/andrewlalis/record-net) library for sending packets as serialized records.
+
+When referring to the names of packets, we will assume a common package name of `nl.andrewl.aos_core.net`.
+
+### Player Connection
+This workflow is involved in the establishment of a connection between the client and server.
+
+1. Player sends a `PlayerConnectRequestMessage` via TCP, immediately upon opening a socket connection. It contains the player's desired `username`, and their `udpPort` that they will use to connect.
+2. The server will respond with either a `PlayerConnectRejectMessage` with a `reason` for the rejection, or a `PlayerConnectAcceptMessage`.
diff --git a/server/src/main/java/nl/andrewl/aos2_server/ClientHandler.java b/server/src/main/java/nl/andrewl/aos2_server/ClientHandler.java
new file mode 100644
index 0000000..d471a23
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/aos2_server/ClientHandler.java
@@ -0,0 +1,112 @@
+package nl.andrewl.aos2_server;
+
+import nl.andrewl.aos_core.Net;
+import nl.andrewl.aos_core.net.PlayerConnectRejectMessage;
+import nl.andrewl.aos_core.net.PlayerConnectRequestMessage;
+import nl.andrewl.record_net.Message;
+import nl.andrewl.record_net.util.ExtendedDataInputStream;
+import nl.andrewl.record_net.util.ExtendedDataOutputStream;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.*;
+
+public class ClientHandler extends Thread {
+ private static int nextThreadId = 1;
+
+ private final Server server;
+ private final Socket socket;
+ private final DatagramSocket datagramSocket;
+ private final ExtendedDataInputStream in;
+ private final ExtendedDataOutputStream out;
+
+ private volatile boolean running;
+ private InetAddress clientAddress;
+ private int clientUdpPort;
+
+ public ClientHandler(Server server, Socket socket, DatagramSocket datagramSocket) throws IOException {
+ super("aos-client-handler-" + nextThreadId++);
+ this.server = server;
+ this.socket = socket;
+ this.datagramSocket = datagramSocket;
+ this.in = Net.getInputStream(socket.getInputStream());
+ this.out = Net.getOutputStream(socket.getOutputStream());
+ }
+
+ public void shutdown() {
+ running = false;
+ }
+
+ @Override
+ public void run() {
+ running = true;
+ establishConnection();
+ while (running) {
+ try {
+ Message msg = Net.read(in);
+ } catch (SocketException e) {
+ if (e.getMessage().equals("Socket closed") | e.getMessage().equals("Connection reset")) {
+ shutdown();
+ } else {
+ e.printStackTrace();
+ }
+ } catch (EOFException e) {
+ shutdown();
+ } catch (IOException e) {
+ e.printStackTrace();
+ shutdown();
+ }
+ }
+ }
+
+ private void establishConnection() {
+ boolean connectionEstablished = false;
+ int attempts = 0;
+ while (!connectionEstablished && attempts < 100) {
+ try {
+ Message msg = Net.read(in);
+ if (msg instanceof PlayerConnectRequestMessage connectMsg) {
+ this.clientAddress = socket.getInetAddress();
+ this.clientUdpPort = connectMsg.udpPort();
+ System.out.println("Player connected: " + connectMsg.username());
+ connectionEstablished = true;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ attempts++;
+ }
+ if (!connectionEstablished) {
+ try {
+ Net.write(new PlayerConnectRejectMessage("Too many connect attempts failed."), out);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ System.out.println("Player couldn't connect after " + attempts + " attempts. Aborting.");
+ shutdown();
+ }
+ }
+
+ private void sendDatagramPacket(Message msg) {
+ try {
+ sendDatagramPacket(Net.write(msg));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void sendDatagramPacket(byte[] data) {
+ DatagramPacket packet = new DatagramPacket(data, data.length, clientAddress, clientUdpPort);
+ sendDatagramPacket(packet);
+ }
+
+ private void sendDatagramPacket(DatagramPacket packet) {
+ try {
+ packet.setAddress(clientAddress);
+ packet.setPort(clientUdpPort);
+ datagramSocket.send(packet);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/server/src/main/java/nl/andrewl/aos2_server/Server.java b/server/src/main/java/nl/andrewl/aos2_server/Server.java
new file mode 100644
index 0000000..398a685
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/aos2_server/Server.java
@@ -0,0 +1,51 @@
+package nl.andrewl.aos2_server;
+
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.HashSet;
+import java.util.Set;
+
+public class Server implements Runnable {
+ private final ServerSocket serverSocket;
+ private final DatagramSocket datagramSocket;
+ private volatile boolean running;
+ private Set clientHandlers;
+
+ public static void main(String[] args) throws IOException {
+ new Server().run();
+ }
+
+ public Server() throws IOException {
+ this.serverSocket = new ServerSocket(24464, 5);
+ this.serverSocket.setReuseAddress(true);
+ this.datagramSocket = new DatagramSocket(24464);
+ this.datagramSocket.setReuseAddress(true);
+ this.clientHandlers = new HashSet<>();
+ }
+
+ @Override
+ public void run() {
+ running = true;
+ System.out.println("Started AOS2-Server on TCP/UDP port " + serverSocket.getLocalPort() + "; now accepting connections.");
+ while (running) {
+ acceptClientConnection();
+ }
+ }
+
+ private void acceptClientConnection() {
+ try {
+ Socket clientSocket = serverSocket.accept();
+ ClientHandler handler = new ClientHandler(this, clientSocket, datagramSocket);
+ handler.start();
+ clientHandlers.add(handler);
+ } 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();
+ }
+ }
+}
diff --git a/setversion.d b/setversion.d
new file mode 100755
index 0000000..70fc9d0
--- /dev/null
+++ b/setversion.d
@@ -0,0 +1,46 @@
+#!/usr/bin/rdmd
+/**
+ * This module takes the main parent POM's version, and applies it to all child
+ * modules.
+ *
+ * While you can run this with `./setversion.d`, it's faster if you compile
+ * with `dmd setversion.d` and then just run `./setversion`.
+ */
+module setversion;
+
+import std.stdio;
+import std.file : write, readText;
+
+void main() {
+ string newVersion = getMainVersion();
+ writefln!"Setting all modules to version %s"(newVersion);
+ string[] files = ["client/pom.xml", "core/pom.xml", "server/pom.xml"];
+ foreach (pomFile; files) {
+ string xml = replaceVersion(readText(pomFile), newVersion);
+ write(pomFile, xml);
+ writefln!"Updated %s to version %s"(pomFile, newVersion);
+ }
+}
+
+string getMainVersion() {
+ import std.file : readText;
+ import std.regex;
+ auto versionRegex = ctRegex!(`(\S+)<\/version>`);
+ auto c = matchFirst(readText("pom.xml"), versionRegex);
+ return c[1];
+}
+
+string replaceVersion(string xml, string newVersion) {
+ import std.regex;
+ import std.string : strip, indexOf;
+ auto versionRegex = ctRegex!(`[\s\S]*(\S+)<\/version>[\s\S]*<\/parent>`);
+ auto c = matchFirst(xml, versionRegex);
+ if (!c.empty) {
+ string currentVersion = c[1];
+ auto hitIndex = c.hit.indexOf(currentVersion);
+ string prefix = xml[0 .. c.pre.length + hitIndex];
+ string suffix = xml[c.pre.length + hitIndex + currentVersion.length .. $];
+ return prefix ~ newVersion ~ suffix;
+ }
+ return xml;
+}