Added successful client connection flow.

This commit is contained in:
Andrew Lalis 2022-07-06 20:20:15 +02:00
parent 1bf7074b76
commit 682f9f9bc2
20 changed files with 514 additions and 155 deletions

View File

@ -2,106 +2,101 @@ package nl.andrewl.aos2_client;
import nl.andrewl.aos2_client.render.ChunkMesh; import nl.andrewl.aos2_client.render.ChunkMesh;
import nl.andrewl.aos2_client.render.ChunkRenderer; 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 nl.andrewl.aos_core.model.Chunk;
import org.joml.Vector3i; 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 java.util.Random;
import static org.lwjgl.glfw.GLFW.*; import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL46.*; import static org.lwjgl.opengl.GL46.*;
public class Client { public class Client implements Runnable {
public static void main(String[] args) { public static void main(String[] args) throws IOException {
var windowInfo = initUI(); InetAddress serverAddress = InetAddress.getByName(args[0]);
long windowHandle = windowInfo.windowHandle(); int serverPort = Integer.parseInt(args[1]);
String username = args[2].trim();
Camera cam = new Camera(); Client client = new Client(serverAddress, serverPort, username);
cam.setOrientationDegrees(90, 90); client.run();
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); // var windowInfo = WindowUtils.initUI();
// long windowHandle = windowInfo.windowHandle();
for (int x = 0; x < Chunk.SIZE; x++) { //
for (int z = 0; z < Chunk.SIZE; z++) { // Camera cam = new Camera();
chunk.setBlockAt(x, Chunk.SIZE - 1, z, (byte) 0); // cam.setOrientationDegrees(90, 90);
} // cam.setPosition(-3, 3, 0);
} // glfwSetCursorPosCallback(windowHandle, cam);
//
ChunkRenderer chunkRenderer = new ChunkRenderer(windowInfo.width(), windowInfo.height()); // Chunk chunk = Chunk.random(new Vector3i(0, 0, 0), new Random(1));
chunkRenderer.addChunkMesh(new ChunkMesh(chunk)); // Chunk chunk2 = Chunk.random(new Vector3i(1, 0, 0), new Random(1));
chunkRenderer.addChunkMesh(new ChunkMesh(chunk2)); // Chunk chunk3 = Chunk.random(new Vector3i(1, 0, 1), new Random(1));
chunkRenderer.addChunkMesh(new ChunkMesh(chunk3)); // Chunk chunk4 = Chunk.random(new Vector3i(0, 0, 1), new Random(1));
chunkRenderer.addChunkMesh(new ChunkMesh(chunk4)); //
// chunk.setBlockAt(0, 0, 0, (byte) 0);
while (!glfwWindowShouldClose(windowHandle)) { //
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // for (int x = 0; x < Chunk.SIZE; x++) {
// for (int z = 0; z < Chunk.SIZE; z++) {
chunkRenderer.draw(cam); // chunk.setBlockAt(x, Chunk.SIZE - 1, z, (byte) 0);
// }
glfwSwapBuffers(windowHandle); // }
glfwPollEvents(); //
// ChunkRenderer chunkRenderer = new ChunkRenderer(windowInfo.width(), windowInfo.height());
if (glfwGetKey(windowHandle, GLFW_KEY_W) == GLFW_PRESS) cam.move(Camera.FORWARD); // chunkRenderer.addChunkMesh(new ChunkMesh(chunk));
if (glfwGetKey(windowHandle, GLFW_KEY_S) == GLFW_PRESS) cam.move(Camera.BACKWARD); // chunkRenderer.addChunkMesh(new ChunkMesh(chunk2));
if (glfwGetKey(windowHandle, GLFW_KEY_A) == GLFW_PRESS) cam.move(Camera.LEFT); // chunkRenderer.addChunkMesh(new ChunkMesh(chunk3));
if (glfwGetKey(windowHandle, GLFW_KEY_D) == GLFW_PRESS) cam.move(Camera.RIGHT); // chunkRenderer.addChunkMesh(new ChunkMesh(chunk4));
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); // while (!glfwWindowShouldClose(windowHandle)) {
} // glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//
chunkRenderer.free(); // chunkRenderer.draw(cam);
//
Callbacks.glfwFreeCallbacks(windowHandle); // glfwSwapBuffers(windowHandle);
glfwDestroyWindow(windowHandle); // glfwPollEvents();
glfwTerminate(); //
glfwSetErrorCallback(null).free(); // 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() { private InetAddress serverAddress;
GLFWErrorCallback.createPrint(System.err).set(); private int serverPort;
if (!glfwInit()) throw new IllegalStateException("Could not initialize GLFW."); private String username;
glfwDefaultWindowHints(); private CommunicationHandler communicationHandler;
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); private volatile boolean running;
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
var vidMode = glfwGetVideoMode(glfwGetPrimaryMonitor()); public Client(InetAddress serverAddress, int serverPort, String username) {
if (vidMode == null) throw new IllegalStateException("Could not get information about the primary monitory."); this.serverAddress = serverAddress;
long windowHandle = glfwCreateWindow(vidMode.width(), vidMode.height(), "Ace of Shades 2", glfwGetPrimaryMonitor(), 0); this.serverPort = serverPort;
if (windowHandle == 0) throw new RuntimeException("Failed to create GLFW window."); this.username = username;
this.communicationHandler = new CommunicationHandler();
}
glfwSetKeyCallback(windowHandle, (window, key, scancode, action, mods) -> { @Override
if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) { public void run() {
glfwSetWindowShouldClose(windowHandle, true); running = false;
} try {
}); communicationHandler.establishConnection(serverAddress, serverPort, username);
System.out.println("Established connection to the server.");
glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED); } catch (IOException e) {
glfwSetInputMode(windowHandle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); e.printStackTrace();
running = false;
glfwSetWindowPos(windowHandle, 0, 0); }
glfwSetCursorPos(windowHandle, 0, 0); while (running) {
// Do game stuff
glfwMakeContextCurrent(windowHandle); System.out.println("Running!");
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());
} }
} }

View File

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

View File

@ -12,7 +12,7 @@ public class ChunkMesh {
private final int vaoId; private final int vaoId;
private final int eboId; private final int eboId;
private int indiciesCount; private int indexCount;
private final int[] positionData; private final int[] positionData;
private final Chunk chunk; private final Chunk chunk;
@ -41,14 +41,14 @@ public class ChunkMesh {
long start = System.nanoTime(); long start = System.nanoTime();
var meshData = ChunkMeshGenerator.generateMesh(chunk); var meshData = ChunkMeshGenerator.generateMesh(chunk);
double dur = (System.nanoTime() - start) / 1_000_000.0; double dur = (System.nanoTime() - start) / 1_000_000.0;
this.indiciesCount = meshData.indexBuffer().limit(); this.indexCount = meshData.indexBuffer().limit();
// Print some debug information. // Print some debug information.
System.out.printf( System.out.printf(
"Generated mesh for chunk (%d, %d, %d) in %.3f ms. %d vertices, %d indices.%n", "Generated mesh for chunk (%d, %d, %d) in %.3f ms. %d vertices, %d indices.%n",
chunk.getPosition().x, chunk.getPosition().y, chunk.getPosition().z, chunk.getPosition().x, chunk.getPosition().y, chunk.getPosition().z,
dur, dur,
meshData.vertexBuffer().limit() / 9, meshData.vertexBuffer().limit() / 9,
indiciesCount indexCount
); );
glBindBuffer(GL_ARRAY_BUFFER, vboId); glBindBuffer(GL_ARRAY_BUFFER, vboId);
@ -80,7 +80,7 @@ public class ChunkMesh {
public void draw() { public void draw() {
glBindVertexArray(vaoId); glBindVertexArray(vaoId);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboId); 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() { public void free() {

View File

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

View File

@ -35,6 +35,12 @@
<artifactId>record-net</artifactId> <artifactId>record-net</artifactId>
<version>v1.2.1</version> <version>v1.2.1</version>
</dependency> </dependency>
<!-- https://github.com/OpenHFT/Zero-Allocation-Hashing -->
<dependency>
<groupId>net.openhft</groupId>
<artifactId>zero-allocation-hashing</artifactId>
<version>0.15</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api --> <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency> <dependency>

View File

@ -1,6 +1,10 @@
package nl.andrewl.aos_core; 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.ConnectRequestMessage;
import nl.andrewl.aos_core.net.udp.DatagramInit;
import nl.andrewl.record_net.Message; import nl.andrewl.record_net.Message;
import nl.andrewl.record_net.Serializer; import nl.andrewl.record_net.Serializer;
import nl.andrewl.record_net.util.ExtendedDataInputStream; import nl.andrewl.record_net.util.ExtendedDataInputStream;
@ -20,6 +24,10 @@ public final class Net {
private static final Serializer serializer = new Serializer(); private static final Serializer serializer = new Serializer();
static { static {
serializer.registerType(1, ConnectRequestMessage.class); 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) { public static ExtendedDataInputStream getInputStream(InputStream in) {

View File

@ -1,5 +1,6 @@
package nl.andrewl.aos_core.model; package nl.andrewl.aos_core.model;
import net.openhft.hashing.LongHashFunction;
import org.joml.Vector3f; import org.joml.Vector3f;
import org.joml.Vector3i; import org.joml.Vector3i;
@ -104,6 +105,10 @@ public class Chunk {
return sb.toString(); return sb.toString();
} }
public long blockHash() {
return LongHashFunction.xx3(0).hashBytes(blocks);
}
public static Chunk random(Vector3i position, Random rand) { public static Chunk random(Vector3i position, Random rand) {
Chunk c = new Chunk(position); Chunk c = new Chunk(position);
for (int i = 0; i < TOTAL_SIZE; i++) { for (int i = 0; i < TOTAL_SIZE; i++) {

View File

@ -8,11 +8,33 @@ public class Player {
private final Vector3f velocity; private final Vector3f velocity;
private final Vector2f orientation; private final Vector2f orientation;
private final String username; private final String username;
private final int id;
public Player(String username) { public Player(int id, String username) {
this.position = new Vector3f(); this.position = new Vector3f();
this.velocity = new Vector3f(); this.velocity = new Vector3f();
this.orientation = new Vector2f(); this.orientation = new Vector2f();
this.id = id;
this.username = username; 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;
}
} }

View File

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

View File

@ -2,4 +2,11 @@ package nl.andrewl.aos_core.net;
import nl.andrewl.record_net.Message; 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 {}

View File

@ -2,4 +2,10 @@ package nl.andrewl.aos_core.net;
import nl.andrewl.record_net.Message; 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 {}

View File

@ -2,4 +2,4 @@ package nl.andrewl.aos_core.net;
import nl.andrewl.record_net.Message; import nl.andrewl.record_net.Message;
public record ConnectRequestMessage(String username, int udpPort) implements Message {} public record ConnectRequestMessage(String username) implements Message {}

View File

@ -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<Message> messageConsumer;
public TcpReceiver(ExtendedDataInputStream in, Consumer<Message> 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();
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -2,4 +2,11 @@ package nl.andrewl.aos_core.net.udp;
import nl.andrewl.record_net.Message; 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 {}

View File

@ -8,9 +8,9 @@ When referring to the names of packets, we will assume a common package name of
### Player Connection ### Player Connection
This workflow is involved in the establishment of a connection between the client and server. 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. 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`. 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). 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. 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 ### 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. 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.

View File

@ -1,85 +1,73 @@
package nl.andrewl.aos2_server; package nl.andrewl.aos2_server;
import nl.andrewl.aos_core.Net; 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.ConnectRejectMessage;
import nl.andrewl.aos_core.net.ConnectRequestMessage; 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.Message;
import nl.andrewl.record_net.util.ExtendedDataInputStream; import nl.andrewl.record_net.util.ExtendedDataInputStream;
import nl.andrewl.record_net.util.ExtendedDataOutputStream; import nl.andrewl.record_net.util.ExtendedDataOutputStream;
import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.net.*; import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class ClientHandler extends Thread { import java.net.InetAddress;
private static int nextThreadId = 1; import java.net.Socket;
public class ClientCommunicationHandler {
private final Server server; private final Server server;
private final Socket socket; private final Socket socket;
private final DatagramSocket datagramSocket; private final DatagramSocket datagramSocket;
private final ExtendedDataInputStream in; private final ExtendedDataInputStream in;
private final ExtendedDataOutputStream out; private final ExtendedDataOutputStream out;
private volatile boolean running;
private InetAddress clientAddress; private InetAddress clientAddress;
private int clientUdpPort; private int clientUdpPort;
private Player player;
public ClientHandler(Server server, Socket socket, DatagramSocket datagramSocket) throws IOException { public ClientCommunicationHandler(Server server, Socket socket, DatagramSocket datagramSocket) throws IOException {
super("aos-client-handler-" + nextThreadId++);
this.server = server; this.server = server;
this.socket = socket; this.socket = socket;
this.datagramSocket = datagramSocket; this.datagramSocket = datagramSocket;
this.in = Net.getInputStream(socket.getInputStream()); this.in = Net.getInputStream(socket.getInputStream());
this.out = Net.getOutputStream(socket.getOutputStream()); this.out = Net.getOutputStream(socket.getOutputStream());
establishConnection();
new Thread(new TcpReceiver(in, this::handleTcpMessage)).start();
} }
public void shutdown() { 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 { try {
socket.setSoTimeout(1000); socket.close();
} catch (SocketException e) { } catch (IOException e) {
throw new RuntimeException(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; boolean connectionEstablished = false;
int attempts = 0; int attempts = 0;
while (!connectionEstablished && attempts < 100) { while (!connectionEstablished && attempts < 100) {
try { try {
Message msg = Net.read(in); Message msg = Net.read(in);
if (msg instanceof ConnectRequestMessage connectMsg) { 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.clientAddress = socket.getInetAddress();
this.clientUdpPort = connectMsg.udpPort();
System.out.println("Player connected: " + connectMsg.username()); System.out.println("Player connected: " + connectMsg.username());
connectionEstablished = true; connectionEstablished = true;
try { this.player = server.registerPlayer(this, connectMsg.username());
socket.setSoTimeout(0); Net.write(new ConnectAcceptMessage(player.getId()), out);
} catch (SocketException e) {
throw new RuntimeException(e);
}
} }
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
@ -93,11 +81,11 @@ public class ClientHandler extends Thread {
e.printStackTrace(); e.printStackTrace();
} }
System.out.println("Player couldn't connect after " + attempts + " attempts. Aborting."); 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 { try {
sendDatagramPacket(Net.write(msg)); sendDatagramPacket(Net.write(msg));
} catch (IOException e) { } 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); DatagramPacket packet = new DatagramPacket(data, data.length, clientAddress, clientUdpPort);
sendDatagramPacket(packet); sendDatagramPacket(packet);
} }
private void sendDatagramPacket(DatagramPacket packet) { public void sendDatagramPacket(DatagramPacket packet) {
try { try {
packet.setAddress(clientAddress); packet.setAddress(clientAddress);
packet.setPort(clientUdpPort); packet.setPort(clientUdpPort);

View File

@ -1,22 +1,26 @@
package nl.andrewl.aos2_server; 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.io.IOException;
import java.net.DatagramSocket; import java.net.*;
import java.net.ServerSocket; import java.util.HashMap;
import java.net.Socket;
import java.net.SocketException;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map;
import java.util.Set; import java.util.Set;
public class Server implements Runnable { public class Server implements Runnable {
private final ServerSocket serverSocket; private final ServerSocket serverSocket;
private final DatagramSocket datagramSocket; private final DatagramSocket datagramSocket;
private volatile boolean running; private volatile boolean running;
private Set<ClientHandler> clientHandlers;
public static void main(String[] args) throws IOException { private int nextClientId = 1;
new Server().run(); private final Set<ClientCommunicationHandler> clientHandlers;
} private final Map<Integer, Player> players;
private final Map<Integer, ClientCommunicationHandler> playerClientHandlers;
public Server() throws IOException { public Server() throws IOException {
this.serverSocket = new ServerSocket(24464, 5); this.serverSocket = new ServerSocket(24464, 5);
@ -24,22 +28,49 @@ public class Server implements Runnable {
this.datagramSocket = new DatagramSocket(24464); this.datagramSocket = new DatagramSocket(24464);
this.datagramSocket.setReuseAddress(true); this.datagramSocket.setReuseAddress(true);
this.clientHandlers = new HashSet<>(); this.clientHandlers = new HashSet<>();
this.players = new HashMap<>();
this.playerClientHandlers = new HashMap<>();
} }
@Override @Override
public void run() { public void run() {
running = true; 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."); System.out.println("Started AOS2-Server on TCP/UDP port " + serverSocket.getLocalPort() + "; now accepting connections.");
while (running) { while (running) {
acceptClientConnection(); 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() { private void acceptClientConnection() {
try { try {
Socket clientSocket = serverSocket.accept(); Socket clientSocket = serverSocket.accept();
ClientHandler handler = new ClientHandler(this, clientSocket, datagramSocket); ClientCommunicationHandler handler = new ClientCommunicationHandler(this, clientSocket, datagramSocket);
handler.start();
clientHandlers.add(handler); clientHandlers.add(handler);
} catch (IOException e) { } catch (IOException e) {
if (e instanceof SocketException && !this.running && e.getMessage().equalsIgnoreCase("Socket closed")) { if (e instanceof SocketException && !this.running && e.getMessage().equalsIgnoreCase("Socket closed")) {
@ -48,4 +79,8 @@ public class Server implements Runnable {
e.printStackTrace(); e.printStackTrace();
} }
} }
public static void main(String[] args) throws IOException {
new Server().run();
}
} }