Added version script, and beginnings of network code.

This commit is contained in:
Andrew Lalis 2022-07-05 21:49:04 +02:00
parent 141e89951a
commit 00c22525cb
11 changed files with 302 additions and 4 deletions

View File

@ -210,7 +210,6 @@
<artifactId>lwjgl-stb</artifactId>
<classifier>${lwjgl.natives}</classifier>
</dependency>
</dependencies>
<build>
@ -220,7 +219,7 @@
<configuration>
<archive>
<manifest>
<mainClass>nl.andrewl.aos2_client.Aos2Client</mainClass>
<mainClass>nl.andrewl.aos2_client.Client</mainClass>
</manifest>
</archive>
<descriptorRefs>

View File

@ -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) -> {

View File

@ -16,6 +16,13 @@
<maven.compiler.target>17</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.joml/joml -->
<dependency>
@ -23,6 +30,11 @@
<artifactId>joml</artifactId>
<version>1.10.4</version>
</dependency>
<dependency>
<groupId>com.github.andrewlalis</groupId>
<artifactId>record-net</artifactId>
<version>v1.2.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>

View File

@ -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);
}
}

View File

@ -0,0 +1,5 @@
package nl.andrewl.aos_core.net;
import nl.andrewl.record_net.Message;
public record PlayerConnectRejectMessage (String reason) implements Message {}

View File

@ -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 {}

View File

@ -0,0 +1,5 @@
package nl.andrewl.aos_core.net.udp;
import nl.andrewl.record_net.Message;
public record InitPacket () implements Message {}

12
design/net.md Normal file
View File

@ -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`.

View File

@ -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();
}
}
}

View File

@ -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<ClientHandler> 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();
}
}
}

46
setversion.d Executable file
View File

@ -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!(`<version>(\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!(`<parent>[\s\S]*<version>(\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;
}