From 00c22525cbe49935023a6e46e9e961dcd73cd89e Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Tue, 5 Jul 2022 21:49:04 +0200 Subject: [PATCH] Added version script, and beginnings of network code. --- client/pom.xml | 3 +- .../{Aos2Client.java => Client.java} | 4 +- core/pom.xml | 12 ++ .../main/java/nl/andrewl/aos_core/Net.java | 48 ++++++++ .../net/PlayerConnectRejectMessage.java | 5 + .../net/PlayerConnectRequestMessage.java | 8 ++ .../andrewl/aos_core/net/udp/InitPacket.java | 5 + design/net.md | 12 ++ .../nl/andrewl/aos2_server/ClientHandler.java | 112 ++++++++++++++++++ .../java/nl/andrewl/aos2_server/Server.java | 51 ++++++++ setversion.d | 46 +++++++ 11 files changed, 302 insertions(+), 4 deletions(-) rename client/src/main/java/nl/andrewl/aos2_client/{Aos2Client.java => Client.java} (97%) create mode 100644 core/src/main/java/nl/andrewl/aos_core/Net.java create mode 100644 core/src/main/java/nl/andrewl/aos_core/net/PlayerConnectRejectMessage.java create mode 100644 core/src/main/java/nl/andrewl/aos_core/net/PlayerConnectRequestMessage.java create mode 100644 core/src/main/java/nl/andrewl/aos_core/net/udp/InitPacket.java create mode 100644 design/net.md create mode 100644 server/src/main/java/nl/andrewl/aos2_server/ClientHandler.java create mode 100644 server/src/main/java/nl/andrewl/aos2_server/Server.java create mode 100755 setversion.d 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; +}