diff --git a/client/src/main/java/nl/andrewl/aos2_client/Client.java b/client/src/main/java/nl/andrewl/aos2_client/Client.java
index 69a0705..bfb270f 100644
--- a/client/src/main/java/nl/andrewl/aos2_client/Client.java
+++ b/client/src/main/java/nl/andrewl/aos2_client/Client.java
@@ -2,106 +2,101 @@ package nl.andrewl.aos2_client;
import nl.andrewl.aos2_client.render.ChunkMesh;
import nl.andrewl.aos2_client.render.ChunkRenderer;
-import nl.andrewl.aos2_client.render.WindowInfo;
+import nl.andrewl.aos2_client.render.WindowUtils;
import nl.andrewl.aos_core.model.Chunk;
import org.joml.Vector3i;
-import org.lwjgl.glfw.Callbacks;
-import org.lwjgl.glfw.GLFWErrorCallback;
-import org.lwjgl.opengl.GL;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
import java.util.Random;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL46.*;
-public class Client {
- public static void main(String[] args) {
- var windowInfo = initUI();
- long windowHandle = windowInfo.windowHandle();
+public class Client implements Runnable {
+ public static void main(String[] args) throws IOException {
+ InetAddress serverAddress = InetAddress.getByName(args[0]);
+ int serverPort = Integer.parseInt(args[1]);
+ String username = args[2].trim();
- Camera cam = new Camera();
- cam.setOrientationDegrees(90, 90);
- cam.setPosition(-3, 3, 0);
- glfwSetCursorPosCallback(windowHandle, cam);
+ Client client = new Client(serverAddress, serverPort, username);
+ client.run();
- Chunk chunk = Chunk.random(new Vector3i(0, 0, 0), new Random(1));
- Chunk chunk2 = Chunk.random(new Vector3i(1, 0, 0), new Random(1));
- Chunk chunk3 = Chunk.random(new Vector3i(1, 0, 1), new Random(1));
- Chunk chunk4 = Chunk.random(new Vector3i(0, 0, 1), new Random(1));
- chunk.setBlockAt(0, 0, 0, (byte) 0);
-
- for (int x = 0; x < Chunk.SIZE; x++) {
- for (int z = 0; z < Chunk.SIZE; z++) {
- chunk.setBlockAt(x, Chunk.SIZE - 1, z, (byte) 0);
- }
- }
-
- ChunkRenderer chunkRenderer = new ChunkRenderer(windowInfo.width(), windowInfo.height());
- chunkRenderer.addChunkMesh(new ChunkMesh(chunk));
- chunkRenderer.addChunkMesh(new ChunkMesh(chunk2));
- chunkRenderer.addChunkMesh(new ChunkMesh(chunk3));
- chunkRenderer.addChunkMesh(new ChunkMesh(chunk4));
-
- while (!glfwWindowShouldClose(windowHandle)) {
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
-
- chunkRenderer.draw(cam);
-
- glfwSwapBuffers(windowHandle);
- glfwPollEvents();
-
- if (glfwGetKey(windowHandle, GLFW_KEY_W) == GLFW_PRESS) cam.move(Camera.FORWARD);
- if (glfwGetKey(windowHandle, GLFW_KEY_S) == GLFW_PRESS) cam.move(Camera.BACKWARD);
- if (glfwGetKey(windowHandle, GLFW_KEY_A) == GLFW_PRESS) cam.move(Camera.LEFT);
- if (glfwGetKey(windowHandle, GLFW_KEY_D) == GLFW_PRESS) cam.move(Camera.RIGHT);
- if (glfwGetKey(windowHandle, GLFW_KEY_SPACE) == GLFW_PRESS) cam.move(Camera.UP);
- if (glfwGetKey(windowHandle, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) cam.move(Camera.DOWN);
- }
-
- chunkRenderer.free();
-
- Callbacks.glfwFreeCallbacks(windowHandle);
- glfwDestroyWindow(windowHandle);
- glfwTerminate();
- glfwSetErrorCallback(null).free();
+// var windowInfo = WindowUtils.initUI();
+// long windowHandle = windowInfo.windowHandle();
+//
+// Camera cam = new Camera();
+// cam.setOrientationDegrees(90, 90);
+// cam.setPosition(-3, 3, 0);
+// glfwSetCursorPosCallback(windowHandle, cam);
+//
+// Chunk chunk = Chunk.random(new Vector3i(0, 0, 0), new Random(1));
+// Chunk chunk2 = Chunk.random(new Vector3i(1, 0, 0), new Random(1));
+// Chunk chunk3 = Chunk.random(new Vector3i(1, 0, 1), new Random(1));
+// Chunk chunk4 = Chunk.random(new Vector3i(0, 0, 1), new Random(1));
+//
+// chunk.setBlockAt(0, 0, 0, (byte) 0);
+//
+// for (int x = 0; x < Chunk.SIZE; x++) {
+// for (int z = 0; z < Chunk.SIZE; z++) {
+// chunk.setBlockAt(x, Chunk.SIZE - 1, z, (byte) 0);
+// }
+// }
+//
+// ChunkRenderer chunkRenderer = new ChunkRenderer(windowInfo.width(), windowInfo.height());
+// chunkRenderer.addChunkMesh(new ChunkMesh(chunk));
+// chunkRenderer.addChunkMesh(new ChunkMesh(chunk2));
+// chunkRenderer.addChunkMesh(new ChunkMesh(chunk3));
+// chunkRenderer.addChunkMesh(new ChunkMesh(chunk4));
+//
+// while (!glfwWindowShouldClose(windowHandle)) {
+// glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+//
+// chunkRenderer.draw(cam);
+//
+// glfwSwapBuffers(windowHandle);
+// glfwPollEvents();
+//
+// if (glfwGetKey(windowHandle, GLFW_KEY_W) == GLFW_PRESS) cam.move(Camera.FORWARD);
+// if (glfwGetKey(windowHandle, GLFW_KEY_S) == GLFW_PRESS) cam.move(Camera.BACKWARD);
+// if (glfwGetKey(windowHandle, GLFW_KEY_A) == GLFW_PRESS) cam.move(Camera.LEFT);
+// if (glfwGetKey(windowHandle, GLFW_KEY_D) == GLFW_PRESS) cam.move(Camera.RIGHT);
+// if (glfwGetKey(windowHandle, GLFW_KEY_SPACE) == GLFW_PRESS) cam.move(Camera.UP);
+// if (glfwGetKey(windowHandle, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) cam.move(Camera.DOWN);
+// }
+//
+// chunkRenderer.free();
+// WindowUtils.clearUI(windowHandle);
}
- private static WindowInfo initUI() {
- GLFWErrorCallback.createPrint(System.err).set();
- if (!glfwInit()) throw new IllegalStateException("Could not initialize GLFW.");
- glfwDefaultWindowHints();
- glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
- glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
+ private InetAddress serverAddress;
+ private int serverPort;
+ private String username;
+ private CommunicationHandler communicationHandler;
+ private volatile boolean running;
- 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", glfwGetPrimaryMonitor(), 0);
- if (windowHandle == 0) throw new RuntimeException("Failed to create GLFW window.");
+ public Client(InetAddress serverAddress, int serverPort, String username) {
+ this.serverAddress = serverAddress;
+ this.serverPort = serverPort;
+ this.username = username;
+ this.communicationHandler = new CommunicationHandler();
+ }
- glfwSetKeyCallback(windowHandle, (window, key, scancode, action, mods) -> {
- if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
- glfwSetWindowShouldClose(windowHandle, true);
- }
- });
-
- glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
- glfwSetInputMode(windowHandle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
-
- glfwSetWindowPos(windowHandle, 0, 0);
- glfwSetCursorPos(windowHandle, 0, 0);
-
- glfwMakeContextCurrent(windowHandle);
- glfwSwapInterval(1);
- glfwShowWindow(windowHandle);
-
- GL.createCapabilities();
-// GLUtil.setupDebugMessageCallback(System.out);
- glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
- glEnable(GL_CULL_FACE);
- glEnable(GL_DEPTH_TEST);
- glCullFace(GL_BACK);
-
- return new WindowInfo(windowHandle, vidMode.width(), vidMode.height());
+ @Override
+ public void run() {
+ running = false;
+ try {
+ communicationHandler.establishConnection(serverAddress, serverPort, username);
+ System.out.println("Established connection to the server.");
+ } catch (IOException e) {
+ e.printStackTrace();
+ running = false;
+ }
+ while (running) {
+ // Do game stuff
+ System.out.println("Running!");
+ }
}
}
diff --git a/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java b/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java
new file mode 100644
index 0000000..c1243dd
--- /dev/null
+++ b/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java
@@ -0,0 +1,96 @@
+package nl.andrewl.aos2_client;
+
+import nl.andrewl.aos_core.Net;
+import nl.andrewl.aos_core.net.*;
+import nl.andrewl.aos_core.net.udp.DatagramInit;
+import nl.andrewl.record_net.Message;
+import nl.andrewl.record_net.util.ExtendedDataInputStream;
+import nl.andrewl.record_net.util.ExtendedDataOutputStream;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.Socket;
+
+public class CommunicationHandler {
+ private Socket socket;
+ private DatagramSocket datagramSocket;
+ private ExtendedDataInputStream in;
+ private ExtendedDataOutputStream out;
+ private int clientId;
+
+ public int establishConnection(InetAddress address, int port, String username) throws IOException {
+ System.out.printf("Connecting to server at %s, port %d, with username \"%s\"...%n", address, port, username);
+ if (socket != null && !socket.isClosed()) {
+ socket.close();
+ }
+ socket = new Socket(address, port);
+ socket.setSoTimeout(1000);
+ in = Net.getInputStream(socket.getInputStream());
+ out = Net.getOutputStream(socket.getOutputStream());
+ Net.write(new ConnectRequestMessage(username), out);
+ Message response = Net.read(in);
+ socket.setSoTimeout(0);
+ if (response instanceof ConnectRejectMessage rejectMessage) {
+ throw new IOException("Attempt to connect rejected: " + rejectMessage.reason());
+ }
+ if (response instanceof ConnectAcceptMessage acceptMessage) {
+ this.clientId = acceptMessage.clientId();
+ new Thread(new TcpReceiver(in, this::handleMessage)).start();
+ establishDatagramConnection();
+ new Thread(new UdpReceiver(datagramSocket, this::handleUdpMessage)).start();
+ return acceptMessage.clientId();
+ } else {
+ throw new IOException("Server returned an unexpected message: " + response);
+ }
+ }
+
+ public void sendMessage(Message msg) {
+ try {
+ Net.write(msg, out);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void sendDatagramPacket(Message msg) {
+ try {
+ byte[] data = Net.write(msg);
+ DatagramPacket packet = new DatagramPacket(data, data.length, socket.getRemoteSocketAddress());
+ datagramSocket.send(packet);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void establishDatagramConnection() throws IOException {
+ datagramSocket = new DatagramSocket();
+ boolean connectionEstablished = false;
+ int attempts = 0;
+ while (!connectionEstablished && attempts < 100) {
+ sendDatagramPacket(new DatagramInit(clientId));
+ byte[] buffer = new byte[UdpReceiver.MAX_PACKET_SIZE];
+ DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
+ datagramSocket.receive(packet);
+ Message msg = Net.read(buffer);
+ if (msg instanceof DatagramInit echo && echo.clientId() == clientId) {
+ connectionEstablished = true;
+ } else {
+ attempts++;
+ }
+ }
+ if (!connectionEstablished) {
+ throw new IOException("Could not establish a datagram connection to the server after " + attempts + " attempts.");
+ }
+ System.out.println("Established datagram communication with the server.");
+ }
+
+ private void handleMessage(Message msg) {
+ System.out.println("Received message: " + msg);
+ }
+
+ private void handleUdpMessage(Message msg, DatagramPacket packet) {
+ System.out.println("Received udp message: " + msg);
+ }
+}
diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMesh.java b/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMesh.java
index a852e30..98f0e44 100644
--- a/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMesh.java
+++ b/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMesh.java
@@ -12,7 +12,7 @@ public class ChunkMesh {
private final int vaoId;
private final int eboId;
- private int indiciesCount;
+ private int indexCount;
private final int[] positionData;
private final Chunk chunk;
@@ -41,14 +41,14 @@ public class ChunkMesh {
long start = System.nanoTime();
var meshData = ChunkMeshGenerator.generateMesh(chunk);
double dur = (System.nanoTime() - start) / 1_000_000.0;
- this.indiciesCount = meshData.indexBuffer().limit();
+ this.indexCount = meshData.indexBuffer().limit();
// Print some debug information.
System.out.printf(
"Generated mesh for chunk (%d, %d, %d) in %.3f ms. %d vertices, %d indices.%n",
chunk.getPosition().x, chunk.getPosition().y, chunk.getPosition().z,
dur,
meshData.vertexBuffer().limit() / 9,
- indiciesCount
+ indexCount
);
glBindBuffer(GL_ARRAY_BUFFER, vboId);
@@ -80,7 +80,7 @@ public class ChunkMesh {
public void draw() {
glBindVertexArray(vaoId);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboId);
- glDrawElements(GL_TRIANGLES, indiciesCount, GL_UNSIGNED_INT, 0);
+ glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0);
}
public void free() {
diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/WindowUtils.java b/client/src/main/java/nl/andrewl/aos2_client/render/WindowUtils.java
new file mode 100644
index 0000000..c5d0bbb
--- /dev/null
+++ b/client/src/main/java/nl/andrewl/aos2_client/render/WindowUtils.java
@@ -0,0 +1,55 @@
+package nl.andrewl.aos2_client.render;
+
+import org.lwjgl.glfw.Callbacks;
+import org.lwjgl.glfw.GLFWErrorCallback;
+import org.lwjgl.opengl.GL;
+
+import static org.lwjgl.glfw.GLFW.*;
+import static org.lwjgl.opengl.GL11.*;
+
+public class WindowUtils {
+ public static WindowInfo initUI() {
+ GLFWErrorCallback.createPrint(System.err).set();
+ if (!glfwInit()) throw new IllegalStateException("Could not initialize GLFW.");
+ glfwDefaultWindowHints();
+ glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
+ glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
+
+ 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", glfwGetPrimaryMonitor(), 0);
+ if (windowHandle == 0) throw new RuntimeException("Failed to create GLFW window.");
+
+ glfwSetKeyCallback(windowHandle, (window, key, scancode, action, mods) -> {
+ if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
+ glfwSetWindowShouldClose(windowHandle, true);
+ }
+ });
+
+ glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
+ glfwSetInputMode(windowHandle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
+
+ glfwSetWindowPos(windowHandle, 0, 0);
+ glfwSetCursorPos(windowHandle, 0, 0);
+
+ glfwMakeContextCurrent(windowHandle);
+ glfwSwapInterval(1);
+ glfwShowWindow(windowHandle);
+
+ GL.createCapabilities();
+// GLUtil.setupDebugMessageCallback(System.out);
+ glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+ glEnable(GL_CULL_FACE);
+ glEnable(GL_DEPTH_TEST);
+ glCullFace(GL_BACK);
+
+ return new WindowInfo(windowHandle, vidMode.width(), vidMode.height());
+ }
+
+ public static void clearUI(long windowHandle) {
+ Callbacks.glfwFreeCallbacks(windowHandle);
+ glfwDestroyWindow(windowHandle);
+ glfwTerminate();
+ glfwSetErrorCallback(null).free();
+ }
+}
diff --git a/core/pom.xml b/core/pom.xml
index 390aea1..3425060 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -35,6 +35,12 @@
record-net
v1.2.1
+
+
+ net.openhft
+ zero-allocation-hashing
+ 0.15
+
diff --git a/core/src/main/java/nl/andrewl/aos_core/Net.java b/core/src/main/java/nl/andrewl/aos_core/Net.java
index d88c571..29bc5ab 100644
--- a/core/src/main/java/nl/andrewl/aos_core/Net.java
+++ b/core/src/main/java/nl/andrewl/aos_core/Net.java
@@ -1,6 +1,10 @@
package nl.andrewl.aos_core;
+import nl.andrewl.aos_core.net.ChunkHashMessage;
+import nl.andrewl.aos_core.net.ConnectAcceptMessage;
+import nl.andrewl.aos_core.net.ConnectRejectMessage;
import nl.andrewl.aos_core.net.ConnectRequestMessage;
+import nl.andrewl.aos_core.net.udp.DatagramInit;
import nl.andrewl.record_net.Message;
import nl.andrewl.record_net.Serializer;
import nl.andrewl.record_net.util.ExtendedDataInputStream;
@@ -20,6 +24,10 @@ public final class Net {
private static final Serializer serializer = new Serializer();
static {
serializer.registerType(1, ConnectRequestMessage.class);
+ serializer.registerType(2, ConnectAcceptMessage.class);
+ serializer.registerType(3, ConnectRejectMessage.class);
+ serializer.registerType(4, DatagramInit.class);
+ serializer.registerType(5, ChunkHashMessage.class);
}
public static ExtendedDataInputStream getInputStream(InputStream in) {
diff --git a/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java b/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java
index d3dc5a5..1b916dd 100644
--- a/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java
+++ b/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java
@@ -1,5 +1,6 @@
package nl.andrewl.aos_core.model;
+import net.openhft.hashing.LongHashFunction;
import org.joml.Vector3f;
import org.joml.Vector3i;
@@ -104,6 +105,10 @@ public class Chunk {
return sb.toString();
}
+ public long blockHash() {
+ return LongHashFunction.xx3(0).hashBytes(blocks);
+ }
+
public static Chunk random(Vector3i position, Random rand) {
Chunk c = new Chunk(position);
for (int i = 0; i < TOTAL_SIZE; i++) {
diff --git a/core/src/main/java/nl/andrewl/aos_core/model/Player.java b/core/src/main/java/nl/andrewl/aos_core/model/Player.java
index b51ce8c..408de4c 100644
--- a/core/src/main/java/nl/andrewl/aos_core/model/Player.java
+++ b/core/src/main/java/nl/andrewl/aos_core/model/Player.java
@@ -8,11 +8,33 @@ public class Player {
private final Vector3f velocity;
private final Vector2f orientation;
private final String username;
+ private final int id;
- public Player(String username) {
+ public Player(int id, String username) {
this.position = new Vector3f();
this.velocity = new Vector3f();
this.orientation = new Vector2f();
+ this.id = id;
this.username = username;
}
+
+ public Vector3f getPosition() {
+ return position;
+ }
+
+ public Vector3f getVelocity() {
+ return velocity;
+ }
+
+ public Vector2f getOrientation() {
+ return orientation;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public int getId() {
+ return id;
+ }
}
diff --git a/core/src/main/java/nl/andrewl/aos_core/net/ChunkHashMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/ChunkHashMessage.java
new file mode 100644
index 0000000..b043c10
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/aos_core/net/ChunkHashMessage.java
@@ -0,0 +1,17 @@
+package nl.andrewl.aos_core.net;
+
+import nl.andrewl.record_net.Message;
+
+/**
+ * A message sent by the client, which contains a hash of a chunk, so that the
+ * server can determine if it's up-to-date, or if the server needs to send the
+ * latest chunk data to the user.
+ * @param cx The chunk x coordinate.
+ * @param cy The chunk y coordinate.
+ * @param cz The chunk z coordinate.
+ * @param hash The hash value of the chunk.
+ */
+public record ChunkHashMessage(
+ int cx, int cy, int cz,
+ long hash
+) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/aos_core/net/ConnectAcceptMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/ConnectAcceptMessage.java
index 0f7f46a..5208148 100644
--- a/core/src/main/java/nl/andrewl/aos_core/net/ConnectAcceptMessage.java
+++ b/core/src/main/java/nl/andrewl/aos_core/net/ConnectAcceptMessage.java
@@ -2,4 +2,11 @@ package nl.andrewl.aos_core.net;
import nl.andrewl.record_net.Message;
-public record ConnectAcceptMessage () implements Message {}
+/**
+ * The message that's sent by the server to indicate that a connecting client
+ * has been accepted and can join the server.
+ * @param clientId The client's id.
+ */
+public record ConnectAcceptMessage (
+ int clientId
+) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/aos_core/net/ConnectRejectMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/ConnectRejectMessage.java
index 5bed255..b96aa0c 100644
--- a/core/src/main/java/nl/andrewl/aos_core/net/ConnectRejectMessage.java
+++ b/core/src/main/java/nl/andrewl/aos_core/net/ConnectRejectMessage.java
@@ -2,4 +2,10 @@ package nl.andrewl.aos_core.net;
import nl.andrewl.record_net.Message;
-public record ConnectRejectMessage(String reason) implements Message {}
+/**
+ * A message that's sent by the server when a connecting client is rejected.
+ * @param reason The reason for the rejection.
+ */
+public record ConnectRejectMessage(
+ String reason
+) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/aos_core/net/ConnectRequestMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/ConnectRequestMessage.java
index 91d52e8..2e0f3c6 100644
--- a/core/src/main/java/nl/andrewl/aos_core/net/ConnectRequestMessage.java
+++ b/core/src/main/java/nl/andrewl/aos_core/net/ConnectRequestMessage.java
@@ -2,4 +2,4 @@ package nl.andrewl.aos_core.net;
import nl.andrewl.record_net.Message;
-public record ConnectRequestMessage(String username, int udpPort) implements Message {}
+public record ConnectRequestMessage(String username) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/aos_core/net/TcpReceiver.java b/core/src/main/java/nl/andrewl/aos_core/net/TcpReceiver.java
new file mode 100644
index 0000000..da207b1
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/aos_core/net/TcpReceiver.java
@@ -0,0 +1,39 @@
+package nl.andrewl.aos_core.net;
+
+import nl.andrewl.aos_core.Net;
+import nl.andrewl.record_net.Message;
+import nl.andrewl.record_net.util.ExtendedDataInputStream;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.SocketException;
+import java.util.function.Consumer;
+
+public class TcpReceiver implements Runnable {
+ private final ExtendedDataInputStream in;
+ private final Consumer messageConsumer;
+
+ public TcpReceiver(ExtendedDataInputStream in, Consumer messageConsumer) {
+ this.in = in;
+ this.messageConsumer = messageConsumer;
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ Message msg = Net.read(in);
+ messageConsumer.accept(msg);
+ } catch (SocketException e) {
+ if (e.getMessage().equals("Socket closed")) {
+ return;
+ }
+ e.printStackTrace();
+ } catch (EOFException e) {
+ return;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/nl/andrewl/aos_core/net/UdpMessageHandler.java b/core/src/main/java/nl/andrewl/aos_core/net/UdpMessageHandler.java
new file mode 100644
index 0000000..643d522
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/aos_core/net/UdpMessageHandler.java
@@ -0,0 +1,10 @@
+package nl.andrewl.aos_core.net;
+
+import nl.andrewl.record_net.Message;
+
+import java.net.DatagramPacket;
+
+@FunctionalInterface
+public interface UdpMessageHandler {
+ void handle(Message msg, DatagramPacket packet);
+}
diff --git a/core/src/main/java/nl/andrewl/aos_core/net/UdpReceiver.java b/core/src/main/java/nl/andrewl/aos_core/net/UdpReceiver.java
new file mode 100644
index 0000000..27b3e44
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/aos_core/net/UdpReceiver.java
@@ -0,0 +1,44 @@
+package nl.andrewl.aos_core.net;
+
+import nl.andrewl.aos_core.Net;
+import nl.andrewl.record_net.Message;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.SocketException;
+
+public class UdpReceiver implements Runnable {
+ public static final short MAX_PACKET_SIZE = 1400;
+
+ private final DatagramSocket socket;
+ private final UdpMessageHandler handler;
+
+ public UdpReceiver(DatagramSocket socket, UdpMessageHandler handler) {
+ this.socket = socket;
+ this.handler = handler;
+ }
+
+ @Override
+ public void run() {
+ byte[] buffer = new byte[MAX_PACKET_SIZE];
+ DatagramPacket packet = new DatagramPacket(buffer, MAX_PACKET_SIZE);
+ while (true) {
+ try {
+ socket.receive(packet);
+ Message msg = Net.read(buffer);
+ handler.handle(msg, packet);
+ } catch (SocketException e) {
+ if (e.getMessage().equals("Socket closed")) {
+ return;
+ }
+ e.printStackTrace();
+ } catch (EOFException e) {
+ return;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/nl/andrewl/aos_core/net/udp/ChunkUpdateMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/udp/ChunkUpdateMessage.java
new file mode 100644
index 0000000..e5b4750
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/aos_core/net/udp/ChunkUpdateMessage.java
@@ -0,0 +1,19 @@
+package nl.andrewl.aos_core.net.udp;
+
+import nl.andrewl.record_net.Message;
+
+/**
+ * A message that's sent to clients when a block in a chunk is updated.
+ * @param cx The chunk x coordinate.
+ * @param cy The chunk y coordinate.
+ * @param cz The chunk z coordinate.
+ * @param lx The local x coordinate in the chunk.
+ * @param ly The local y coordinate in the chunk.
+ * @param lz The local z coordinate in the chunk.
+ * @param newBlock The new block data in the specified position.
+ */
+public record ChunkUpdateMessage(
+ int cx, int cy, int cz,
+ int lx, int ly, int lz,
+ byte newBlock
+) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/aos_core/net/udp/DatagramInit.java b/core/src/main/java/nl/andrewl/aos_core/net/udp/DatagramInit.java
index 670c5a7..80dbd19 100644
--- a/core/src/main/java/nl/andrewl/aos_core/net/udp/DatagramInit.java
+++ b/core/src/main/java/nl/andrewl/aos_core/net/udp/DatagramInit.java
@@ -2,4 +2,11 @@ package nl.andrewl.aos_core.net.udp;
import nl.andrewl.record_net.Message;
-public record DatagramInit() implements Message {}
+/**
+ * The message that's sent initially by the client, and responded to by the
+ * server, when a client is establishing a UDP "connection" to the server.
+ * @param clientId The client's id.
+ */
+public record DatagramInit(
+ int clientId
+) implements Message {}
diff --git a/design/net.md b/design/net.md
index a50d3c8..a29c024 100644
--- a/design/net.md
+++ b/design/net.md
@@ -8,9 +8,9 @@ When referring to the names of packets, we will assume a common package name of
### Player Connection
This workflow is involved in the establishment of a connection between the client and server.
-1. Player sends a `ConnectRequestMessage` 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 `ConnectRejectMessage` with a `reason` for the rejection, or a `ConnectAcceptMessage`.
-3. If the player received an acceptance message, they will then send a `DatagramInit` to the server's UDP socket (on the same address/port). The player should keep sending such an init message until they receive a `DatagramInit` message echoed back as a response. The player should then stop sending init messages, and expect to begin receiving normal communication data through the datagram socket.
+1. Player sends a `ConnectRequestMessage` via TCP, immediately upon opening a socket connection. It contains the player's desired `username`.
+2. The server will respond with either a `ConnectRejectMessage` with a `reason` for the rejection, or a `ConnectAcceptMessage` containing the client's `clientId`.
+3. If the player received an acceptance message, they will then send a `DatagramInit` to the server's UDP socket (on the same address/port) containing the `clientId` received in the `ConnectAcceptMessage`. The player should keep sending such an init message until they receive a `DatagramInit` message echoed back as a response. The player should then stop sending init messages, and expect to begin receiving normal communication data through the datagram socket.
### World Data
A combination of TCP and UDP communication is used to ensure that all connected clients have the latest information about the state of the world.
diff --git a/server/src/main/java/nl/andrewl/aos2_server/ClientHandler.java b/server/src/main/java/nl/andrewl/aos2_server/ClientCommunicationHandler.java
similarity index 61%
rename from server/src/main/java/nl/andrewl/aos2_server/ClientHandler.java
rename to server/src/main/java/nl/andrewl/aos2_server/ClientCommunicationHandler.java
index 15e1568..7000906 100644
--- a/server/src/main/java/nl/andrewl/aos2_server/ClientHandler.java
+++ b/server/src/main/java/nl/andrewl/aos2_server/ClientCommunicationHandler.java
@@ -1,85 +1,73 @@
package nl.andrewl.aos2_server;
import nl.andrewl.aos_core.Net;
+import nl.andrewl.aos_core.model.Player;
+import nl.andrewl.aos_core.net.ConnectAcceptMessage;
import nl.andrewl.aos_core.net.ConnectRejectMessage;
import nl.andrewl.aos_core.net.ConnectRequestMessage;
+import nl.andrewl.aos_core.net.TcpReceiver;
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;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.Socket;
+public class ClientCommunicationHandler {
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;
+ private Player player;
- public ClientHandler(Server server, Socket socket, DatagramSocket datagramSocket) throws IOException {
- super("aos-client-handler-" + nextThreadId++);
+ public ClientCommunicationHandler(Server server, Socket socket, DatagramSocket datagramSocket) throws IOException {
this.server = server;
this.socket = socket;
this.datagramSocket = datagramSocket;
this.in = Net.getInputStream(socket.getInputStream());
this.out = Net.getOutputStream(socket.getOutputStream());
+ establishConnection();
+ new Thread(new TcpReceiver(in, this::handleTcpMessage)).start();
}
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() {
try {
- socket.setSoTimeout(1000);
- } catch (SocketException e) {
- throw new RuntimeException(e);
+ socket.close();
+ } catch (IOException e) {
+ e.printStackTrace();
}
+ }
+
+ public void setClientUdpPort(int port) {
+ this.clientUdpPort = port;
+ }
+
+ private void handleTcpMessage(Message msg) {
+ System.out.println("Message received from client " + player.getUsername() + ": " + msg);
+ }
+
+ private void establishConnection() throws IOException {
+ socket.setSoTimeout(1000);
boolean connectionEstablished = false;
int attempts = 0;
while (!connectionEstablished && attempts < 100) {
try {
Message msg = Net.read(in);
if (msg instanceof ConnectRequestMessage connectMsg) {
+ // Try to set the TCP timeout back to 0 now that we've got the correct request.
+ socket.setSoTimeout(0);
this.clientAddress = socket.getInetAddress();
- this.clientUdpPort = connectMsg.udpPort();
System.out.println("Player connected: " + connectMsg.username());
connectionEstablished = true;
- try {
- socket.setSoTimeout(0);
- } catch (SocketException e) {
- throw new RuntimeException(e);
- }
+ this.player = server.registerPlayer(this, connectMsg.username());
+ Net.write(new ConnectAcceptMessage(player.getId()), out);
}
} catch (IOException e) {
e.printStackTrace();
@@ -93,11 +81,11 @@ public class ClientHandler extends Thread {
e.printStackTrace();
}
System.out.println("Player couldn't connect after " + attempts + " attempts. Aborting.");
- shutdown();
+ socket.close();
}
}
- private void sendDatagramPacket(Message msg) {
+ public void sendDatagramPacket(Message msg) {
try {
sendDatagramPacket(Net.write(msg));
} catch (IOException e) {
@@ -105,12 +93,12 @@ public class ClientHandler extends Thread {
}
}
- private void sendDatagramPacket(byte[] data) {
+ public void sendDatagramPacket(byte[] data) {
DatagramPacket packet = new DatagramPacket(data, data.length, clientAddress, clientUdpPort);
sendDatagramPacket(packet);
}
- private void sendDatagramPacket(DatagramPacket packet) {
+ public void sendDatagramPacket(DatagramPacket packet) {
try {
packet.setAddress(clientAddress);
packet.setPort(clientUdpPort);
diff --git a/server/src/main/java/nl/andrewl/aos2_server/Server.java b/server/src/main/java/nl/andrewl/aos2_server/Server.java
index 398a685..365f82e 100644
--- a/server/src/main/java/nl/andrewl/aos2_server/Server.java
+++ b/server/src/main/java/nl/andrewl/aos2_server/Server.java
@@ -1,22 +1,26 @@
package nl.andrewl.aos2_server;
+import nl.andrewl.aos_core.model.Player;
+import nl.andrewl.aos_core.net.UdpReceiver;
+import nl.andrewl.aos_core.net.udp.DatagramInit;
+import nl.andrewl.record_net.Message;
+
import java.io.IOException;
-import java.net.DatagramSocket;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.net.SocketException;
+import java.net.*;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.Map;
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();
- }
+ private int nextClientId = 1;
+ private final Set clientHandlers;
+ private final Map players;
+ private final Map playerClientHandlers;
public Server() throws IOException {
this.serverSocket = new ServerSocket(24464, 5);
@@ -24,22 +28,49 @@ public class Server implements Runnable {
this.datagramSocket = new DatagramSocket(24464);
this.datagramSocket.setReuseAddress(true);
this.clientHandlers = new HashSet<>();
+ this.players = new HashMap<>();
+ this.playerClientHandlers = new HashMap<>();
}
@Override
public void run() {
running = true;
+ new Thread(new UdpReceiver(datagramSocket, this::handleUdpMessage)).start();
System.out.println("Started AOS2-Server on TCP/UDP port " + serverSocket.getLocalPort() + "; now accepting connections.");
while (running) {
acceptClientConnection();
}
+ datagramSocket.close();
+ for (var handler : clientHandlers) handler.shutdown();
+ }
+
+ public void handleUdpMessage(Message msg, DatagramPacket packet) {
+ // Echo any init message from known clients.
+ if (msg instanceof DatagramInit init) {
+ var handler = getHandler(init.clientId());
+ if (handler != null) {
+ handler.setClientUdpPort(packet.getPort());
+ handler.sendDatagramPacket(msg);
+ }
+ }
+ }
+
+ public synchronized Player registerPlayer(ClientCommunicationHandler handler, String username) {
+ Player player = new Player(nextClientId++, username);
+ players.put(player.getId(), player);
+ playerClientHandlers.put(player.getId(), handler);
+ System.out.println("Registered player " + username + " with id " + player.getId());
+ return player;
+ }
+
+ public ClientCommunicationHandler getHandler(int id) {
+ return playerClientHandlers.get(id);
}
private void acceptClientConnection() {
try {
Socket clientSocket = serverSocket.accept();
- ClientHandler handler = new ClientHandler(this, clientSocket, datagramSocket);
- handler.start();
+ ClientCommunicationHandler handler = new ClientCommunicationHandler(this, clientSocket, datagramSocket);
clientHandlers.add(handler);
} catch (IOException e) {
if (e instanceof SocketException && !this.running && e.getMessage().equalsIgnoreCase("Socket closed")) {
@@ -48,4 +79,8 @@ public class Server implements Runnable {
e.printStackTrace();
}
}
+
+ public static void main(String[] args) throws IOException {
+ new Server().run();
+ }
}