Added successful client connection flow.
This commit is contained in:
parent
1bf7074b76
commit
682f9f9bc2
|
@ -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);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
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));
|
||||
private InetAddress serverAddress;
|
||||
private int serverPort;
|
||||
private String username;
|
||||
private CommunicationHandler communicationHandler;
|
||||
private volatile boolean running;
|
||||
|
||||
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);
|
||||
public Client(InetAddress serverAddress, int serverPort, String username) {
|
||||
this.serverAddress = serverAddress;
|
||||
this.serverPort = serverPort;
|
||||
this.username = username;
|
||||
this.communicationHandler = new CommunicationHandler();
|
||||
}
|
||||
|
||||
chunkRenderer.free();
|
||||
|
||||
Callbacks.glfwFreeCallbacks(windowHandle);
|
||||
glfwDestroyWindow(windowHandle);
|
||||
glfwTerminate();
|
||||
glfwSetErrorCallback(null).free();
|
||||
@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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
while (running) {
|
||||
// Do game stuff
|
||||
System.out.println("Running!");
|
||||
}
|
||||
});
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -35,6 +35,12 @@
|
|||
<artifactId>record-net</artifactId>
|
||||
<version>v1.2.1</version>
|
||||
</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 -->
|
||||
<dependency>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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++) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {}
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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 {}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void establishConnection() {
|
||||
try {
|
||||
socket.setSoTimeout(1000);
|
||||
} catch (SocketException e) {
|
||||
throw new RuntimeException(e);
|
||||
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);
|
|
@ -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<ClientHandler> clientHandlers;
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
new Server().run();
|
||||
}
|
||||
private int nextClientId = 1;
|
||||
private final Set<ClientCommunicationHandler> clientHandlers;
|
||||
private final Map<Integer, Player> players;
|
||||
private final Map<Integer, ClientCommunicationHandler> 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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue