diff --git a/src/assert/assert.c b/src/assert/assert.c index 77bade8..6873b46 100644 --- a/src/assert/assert.c +++ b/src/assert/assert.c @@ -7,6 +7,12 @@ #include "assert.h" +pthread_t assertMainThread = 0; + +void assertInit() { + assertMainThread = pthread_self(); +} + void assertTrueImpl( const char *file, const int32_t line, diff --git a/src/assert/assert.h b/src/assert/assert.h index 56b8668..3b4232f 100644 --- a/src/assert/assert.h +++ b/src/assert/assert.h @@ -8,6 +8,13 @@ #pragma once #include "dusk.h" +extern pthread_t assertMainThread; + +/** + * Initialises the assert system. + */ +void assertInit(); + /** * Assert a given value to be true. * @@ -128,4 +135,10 @@ void assertMemoryRangeMatchesImpl( #define assertStrLenMin(str, len, message) \ assertTrue(strlen(str) >= len, message) +#define assertIsMainThread(message) \ + assertTrue(pthread_self() == assertMainThread, message) + +#define assertNotMainThread(message) \ + assertFalse(pthread_self() == assertMainThread, message) + // EOF \ No newline at end of file diff --git a/src/main.c b/src/main.c index 57f5956..b7fb0cd 100644 --- a/src/main.c +++ b/src/main.c @@ -8,6 +8,7 @@ #include "console/console.h" #include "server/server.h" #include "util/string.h" +#include "assert/assert.h" bool_t exitRequested = false; @@ -24,7 +25,13 @@ void cmdServe(const consolecmdexec_t *exec) { } } - errorret_t ret = serverStart(port); + errorret_t ret = serverStart((serverstart_t){ + .type = SERVER_TYPE_NETWORKED, + .networked = { + .port = 3030 + } + }); + if(ret != ERROR_OK) { consolePrint("Failed to start server: %s", errorString()); errorFlush(); @@ -37,6 +44,7 @@ void cmdClose(const consolecmdexec_t *exec) { } int main(void) { + assertInit(); consoleInit(); serverInit(); diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index 27744cb..061c802 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -11,4 +11,6 @@ target_sources(${DUSK_TARGET_NAME} ) # Subdirs -add_subdirectory(packet) \ No newline at end of file +add_subdirectory(packet) +add_subdirectory(networked) +add_subdirectory(singleplayer) \ No newline at end of file diff --git a/src/server/networked/CMakeLists.txt b/src/server/networked/CMakeLists.txt new file mode 100644 index 0000000..0825cb8 --- /dev/null +++ b/src/server/networked/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright (c) 2025 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_TARGET_NAME} + PRIVATE + networkedclient.c + networkedserver.c +) \ No newline at end of file diff --git a/src/server/networked/networkedclient.c b/src/server/networked/networkedclient.c new file mode 100644 index 0000000..230ba75 --- /dev/null +++ b/src/server/networked/networkedclient.c @@ -0,0 +1,255 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "server/server.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "console/console.h" +#include + +errorret_t networkedClientAccept( + serverclient_t *client, + const serverclientaccept_t accept +) { + assertNotNull(client, "Client is NULL"); + assertNotNull(accept.server, "Server is NULL"); + assertTrue( + accept.server->type == SERVER_TYPE_NETWORKED, + "Server is not networked" + ); + + client->state = SERVER_CLIENT_STATE_ACCEPTING; + client->networked.socket = accept.networked.socket; + + // Set timeout to 8 seconds + client->networked.timeout.tv_sec = 8; + client->networked.timeout.tv_usec = 0; + + // Create a thread for the client + int32_t ret = pthread_create( + &client->networked.thread, + NULL, + networkedClientThread, + client + ); + if(ret != 0) { + client->state = SERVER_CLIENT_STATE_DISCONNECTED; + return error("Failed to create client thread"); + } + + // Set socket timeout + if(setsockopt( + client->networked.socket, + SOL_SOCKET, + SO_RCVTIMEO, + &client->networked.timeout, + sizeof(client->networked.timeout) + ) < 0) { + networkedClientCloseOnThread(client, "Failed to set socket timeout"); + return error("Failed to set socket timeout"); + } + + return ERROR_OK; +} + +void networkedClientClose(serverclient_t *client) { + assertIsMainThread("Server client close must be on main thread."); + assertNotNull(client, "Client is NULL"); + assertTrue( + client->server->type == SERVER_TYPE_NETWORKED, + "Server is not networked" + ); + + // Mark client as disconnecting + client->state = SERVER_CLIENT_STATE_DISCONNECTING; + + int32_t maxWaits = 0; + while(client->state == SERVER_CLIENT_STATE_DISCONNECTING) { + // Wait for the thread to finish + usleep(1000); + maxWaits++; + if(maxWaits > 10) break; + } + + pthread_join(client->networked.thread, NULL); + client->networked.thread = 0; + + // Close the socket + if(client->networked.socket != -1) { + close(client->networked.socket); + client->networked.socket = -1; + } + + client->state = SERVER_CLIENT_STATE_DISCONNECTED; + consolePrint("Client %d disconnected.", client->networked.socket); +} + +void networkedClientCloseOnThread( + serverclient_t *client, + const char_t *reason +) { + assertNotNull(client, "Client is NULL"); + assertNotNull(reason, "Reason is NULL"); + assertTrue( + client->server->type == SERVER_TYPE_NETWORKED, + "Server is not networked" + ); + assertNotMainThread("Client close must not be main thread"); + + // Terminate the socket + client->state = SERVER_CLIENT_STATE_DISCONNECTING; + close(client->networked.socket); + client->networked.socket = -1; + client->networked.thread = 0; + consolePrint("Client %d disconnected: %s", client->networked.socket, reason); + + // Mark this client as disconnected so it can be used again. + client->state = SERVER_CLIENT_STATE_DISCONNECTED; + pthread_exit(NULL); +} + +ssize_t networkedClientRead( + serverclient_t * client, + uint8_t *buffer, + const size_t len +) { + assertNotNull(client, "Client is NULL"); + assertNotNull(buffer, "Buffer is NULL"); + assertNotNull(client->server, "Server is NULL"); + assertTrue( + client->server->type == SERVER_TYPE_NETWORKED, + "Server is not networked" + ); + assertTrue(len > 0, "Buffer length is 0"); + + assertTrue( + client->state == SERVER_CLIENT_STATE_CONNECTED || + client->state == SERVER_CLIENT_STATE_ACCEPTING || + client->state == SERVER_CLIENT_STATE_DISCONNECTING, + "Client is not connected, accepting or disconnecting" + ); + + return recv(client->networked.socket, buffer, len, 0); +} + +errorret_t networkedClientWrite( + serverclient_t * client, + const uint8_t *data, + const size_t len +) { + assertNotNull(client, "Client is NULL"); + assertNotNull(data, "Data is NULL"); + assertTrue(len > 0, "Data length is 0"); + assertNotNull(client->server, "Server is NULL"); + assertTrue( + client->server->type == SERVER_TYPE_NETWORKED, + "Server is not networked" + ); + + if(client->state == SERVER_CLIENT_STATE_DISCONNECTED) { + return error("Client is disconnected"); + } + ssize_t sent = send(client->networked.socket, data, len, 0); + if(sent < 0) return error("Failed to send data"); + return ERROR_OK; +} + +errorret_t networkedClientWritePacket( + serverclient_t * client, + const packet_t *packet +) { + assertNotNull(packet, "Packet is NULL"); + assertTrue(packet->type != PACKET_TYPE_INVALID, "Packet type is INVALID"); + assertTrue(packet->length > 0, "Packet length is 0"); + assertTrue( + packet->length <= sizeof(packetdata_t), + "Packet length is too large (1)" + ); + + size_t fullSize = sizeof(packet_t) - sizeof(packet->data) + packet->length; + assertTrue(fullSize <= sizeof(packet_t), "Packet size is too large (2)"); + return networkedClientWrite(client, (const uint8_t *)packet, fullSize); +} + +void * networkedClientThread(void *arg) { + assertNotNull(arg, "Client is NULL"); + assertNotMainThread("Client thread must not be main thread"); + + serverclient_t *client = (serverclient_t *)arg; + char_t buffer[1024]; + ssize_t read; + errorret_t err; + packet_t packet; + + assertNotNull(client->server, "Server is NULL"); + assertTrue( + client->server->type == SERVER_TYPE_NETWORKED, + "Server is not networked" + ); + assertTrue( + client->state == SERVER_CLIENT_STATE_ACCEPTING, + "Client is not accepting?" + ); + + // First message from the client should always be "DUSK|VERSION" to match + // the server version. + { + const char_t *expecting = "DUSK|"DUSK_VERSION; + read = networkedClientRead(client, buffer, sizeof(buffer)); + if(read <= 0) { + packetDisconnectCreate(&packet, PACKET_DISCONNECT_REASON_INVALID_VERSION); + err = networkedClientWritePacket(client, &packet); + networkedClientCloseOnThread(client, "Failed to receive version"); + return NULL; + } + + buffer[read] = '\0'; // Null-terminate the string + if(strncmp(buffer, expecting, strlen(expecting)) != 0) { + packetDisconnectCreate(&packet, PACKET_DISCONNECT_REASON_INVALID_VERSION); + err = networkedClientWritePacket(client, &packet); + networkedClientCloseOnThread(client, "Invalid version"); + return NULL; + } + } + + // Send DUSK back! + packetWelcomeCreate(&packet); + err = networkedClientWritePacket(client, &packet); + if(err != ERROR_OK) { + networkedClientCloseOnThread(client, "Failed to send welcome message"); + return NULL; + } + + client->state = SERVER_CLIENT_STATE_CONNECTED; + while(client->state == SERVER_CLIENT_STATE_CONNECTED) { + read = networkedClientRead(client, buffer, sizeof(buffer)); + + if(read <= 0) { + networkedClientCloseOnThread(client, "Failed to receive data"); + break; + } + + buffer[read] = '\0'; // Null-terminate the string + consolePrint("Received: %s", buffer); + + + if(SERVER.state != SERVER_STATE_RUNNING) { + networkedClientCloseOnThread(client, "Server is shutting down"); + break; + } + } + + packetDisconnectCreate( + &packet, + PACKET_DISCONNECT_REASON_SERVER_SHUTDOWN + ); + networkedClientWritePacket(client, &packet); + if(errorCheck()) errorPrint(); + + client->state = SERVER_CLIENT_STATE_DISCONNECTED; + return NULL; +} \ No newline at end of file diff --git a/src/server/networked/networkedclient.h b/src/server/networked/networkedclient.h new file mode 100644 index 0000000..6f5a896 --- /dev/null +++ b/src/server/networked/networkedclient.h @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +typedef struct serverclient_s serverclient_t; +typedef struct serverclientaccept_s serverclientaccept_t; + +typedef struct { + int32_t socket; +} networkedclientaccept_t; + +typedef struct { + int32_t socket; + pthread_t thread; + struct timeval timeout; +} networkedclient_t; + +/** + * Accepts an incoming connection from a networked client. + * + * @param client Pointer to the server client structure. + * @param accept Accept structure containing client information. + * @return Error code indicating success or failure. + */ +errorret_t networkedClientAccept( + serverclient_t *client, + const serverclientaccept_t accept +); + +/** + * Closes the connection to a networked client. Waits for the thread to finish. + * + * @param client Pointer to the server client structure. + */ +void networkedClientClose(serverclient_t *client); + +/** + * Closes the connection to a networked client on the thread. + * + * @param client Pointer to the server client structure. + * @param reason Reason for closing the connection. + */ +void networkedClientCloseOnThread( + serverclient_t *client, + const char_t *reason +); + +/** + * Receives data from a server client. + * + * @param client Pointer to the server client structure. + * @param buffer Pointer to the buffer to store received data. + * @param len Max length of the buffer. + * @return Number of bytes received. 0 or less indicates an error. + */ +ssize_t networkedClientRead( + serverclient_t * client, + uint8_t *buffer, + const size_t len +); + +/** + * Writes data to a server client. + * + * @param client Pointer to the server client structure. + * @param data Pointer to the data to send. + * @param len Length of the data to send. + * @return Error code indicating success or failure. + */ +errorret_t networkedClientWrite( + serverclient_t * client, + const uint8_t *data, + const size_t len +); + +/** + * Writes a packet to a server client. + * + * @param client Pointer to the server client structure. + * @param packet Pointer to the packet to send. + * @return Error code indicating success or failure. + */ +errorret_t networkedClientWritePacket( + serverclient_t * client, + const packet_t *packet +); + +/** + * Thread function for handling a networked client. + * + * @param arg Pointer to the server client structure. + * @return NULL. + */ +void * networkedClientThread(void *arg); \ No newline at end of file diff --git a/src/server/networked/networkedserver.c b/src/server/networked/networkedserver.c new file mode 100644 index 0000000..a7d6f44 --- /dev/null +++ b/src/server/networked/networkedserver.c @@ -0,0 +1,217 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "server/server.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "console/console.h" + +errorret_t networkedServerStart( + server_t *server, + const serverstart_t start +) { + assertNotNull(server, "Server is null"); + assertTrue(start.type == SERVER_TYPE_NETWORKED,"Invalid server type"); + assertTrue(server->state == SERVER_STATE_STOPPED,"Server is already running"); + assertIsMainThread("Server start must be on main thread"); + + // Initialize the networked server. + consolePrint("Starting server on port %d...\n", start.networked.port); + memoryZero(&server->networked, sizeof(server->networked)); + + // Initialize the server socket + server->networked.socket = socket(AF_INET, SOCK_STREAM, 0); + if(server->networked.socket < 0) { + return error("Failed to create socket"); + } + server->networked.address.sin_family = AF_INET; + server->networked.address.sin_addr.s_addr = INADDR_ANY; + server->networked.address.sin_port = htons(start.networked.port); + + // Bind the socket to the address and port + int32_t res = bind( + server->networked.socket, + (struct sockaddr *)&server->networked.address, + sizeof(server->networked.address) + ); + if(res != 0) { + close(server->networked.socket); + return error("Failed to bind socket"); + } + + // Set the socket to listen for incoming connections + res = listen(server->networked.socket, SERVER_MAX_CLIENTS); + if(res != 0) { + close(server->networked.socket); + return error("Failed to listen on socket"); + } + + // Start the server thread. + server->state = SERVER_STATE_STARTING; + res = pthread_create(&server->thread, NULL, networkedServerThread, server); + if(res != 0) { + perror("Failed to create server thread"); + serverStop(); + return error("Failed to create server thread"); + } + + // Wait for the thread to start. + while(server->state == SERVER_STATE_STARTING) { + usleep(1000); + } + + // Server started, hand back. + consolePrint("Server started."); + return ERROR_OK; +} + +void networkedServerStop(server_t *server) { + assertNotNull(server, "Server is null"); + assertTrue(server->state != SERVER_STATE_STOPPED, "Server is already stopped"); + assertTrue(server->state != SERVER_STATE_STARTING, "Server is starting"); + assertIsMainThread("Server stop must be on main thread"); + + // Tell thread we want it to stop + server->state = SERVER_STATE_STOPPING; + + // Disconnect clients + uint8_t i = 0; + do { + serverclient_t *client = &server->clients[i++]; + if(client->state == SERVER_CLIENT_STATE_DISCONNECTED) continue; + serverClientClose(client); + } while(i < SERVER_MAX_CLIENTS); + + // Now we wait a short time for the thread to stop. + int32_t maxWaits = 0; + while(SERVER.state == SERVER_STATE_STOPPING) { + usleep(1000); + maxWaits++; + if(maxWaits < 1000) continue; + + consolePrint("Server thread did not stop in time, forcing exit."); + SERVER.state = SERVER_STATE_STOPPED; + break; + } + + // Close the socket + if(server->networked.socket >= 0) { + close(server->networked.socket); + server->networked.socket = -1; + } + + // Term the thread. + if(server->thread) { + pthread_join(server->thread, NULL); + server->thread = 0; + } +} + +void * networkedServerThread(void *arg) { + assertNotNull(arg, "Server is null"); + assertNotMainThread("Server thread must not be main thread"); + + server_t *server = (server_t *)arg; + struct timeval timeout; + fd_set readfds; + server->state = SERVER_STATE_RUNNING; + + // Main thread loop. + while(server->state == SERVER_STATE_RUNNING) { + // Wait a tiny bit to avoid large CPU usage. + usleep(1000); + + // Prepare the select call, used for waiting for incoming connections. + FD_ZERO(&readfds); + FD_SET(server->networked.socket, &readfds); + timeout.tv_sec = 1; + timeout.tv_usec = 0; + + // Wait for incoming connections + int32_t activity = select( + server->networked.socket + 1, + &readfds, + NULL, + NULL, + &timeout + ); + + // Timeout + if(activity == 0) continue; + + // Check for errors + if(activity < 0) { + consolePrint("Error in select"); + continue; + } + + // Check if there is a new connection + if(!FD_ISSET(server->networked.socket, &readfds)) continue; + + // Accept new connection. + int32_t clientSocket = accept(server->networked.socket, NULL, NULL); + if(clientSocket < 0) { + consolePrint("Failed to accept connection"); + continue; + } + + // Find an available client slot. + serverclient_t *client = NULL; + uint8_t i = 0; + do { + if(server->clients[i].state != SERVER_CLIENT_STATE_DISCONNECTED) { + i++; + continue; + } + client = &server->clients[i]; + break; + } while(i < SERVER_MAX_CLIENTS); + + // No available slots were found (Server full). This is the only time a + // packet is sent without a client thread. + if(!client) { + packet_t packet; + serverclient_t tempClient = { + .networked = { + .socket = clientSocket, + }, + .server = server, + .state = SERVER_CLIENT_STATE_ACCEPTING + }; + packetDisconnectCreate(&packet, PACKET_DISCONNECT_REASON_SERVER_FULL); + networkedClientWritePacket(&tempClient, &packet); + if(errorCheck()) errorPrint(); + consolePrint("Client %i disconnected: Server full."); + close(clientSocket); + continue; + } + + // Hand off to server client for acceptance. Only one client can be + // accepted at a time at the moment. + errorret_t ret = serverClientAccept(client, (serverclientaccept_t){ + .server = server, + .networked = { + .socket = clientSocket, + } + }); + if(ret != ERROR_OK) { + consolePrint("Failed to accept client connection", errorString()); + errorFlush(); + close(clientSocket); + } + } + + // Thread loop was exited. This is only possible when the server is stopped. + assertTrue( + server->state != SERVER_STATE_RUNNING, + "Server thread exiting while server is running?" + ); + + // Notify main thread we are stopped. + server->state = SERVER_STATE_STOPPED; + return NULL; +} \ No newline at end of file diff --git a/src/server/networked/networkedserver.h b/src/server/networked/networkedserver.h new file mode 100644 index 0000000..6f44ccb --- /dev/null +++ b/src/server/networked/networkedserver.h @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include +#include + +typedef struct { + int socket; + struct sockaddr_in address; +} networkedserver_t; + +typedef struct server_s server_t; +typedef struct serverstart_s serverstart_t; + +/** +* Initialize the networked server +* +* This function initializes the networked server by setting up the ENet library +* and creating a server host. +* +* @param server Pointer to the server structure. +* @param start Information about how to start the server. +* @return Returns an error code if the server fails to start. +*/ +errorret_t networkedServerStart(server_t *server, const serverstart_t start); + +/** + * Stop the networked server + * + * This function stops the networked server by shutting down the ENet library + * and closing the server host. + * + * @param server Pointer to the server structure. + */ +void networkedServerStop(server_t *server); + +/** + * Server thread function + * + * This function runs in a separate thread and handles incoming connections, + * messages, and disconnections. + * + * @param arg Pointer to the argument passed to the thread. + */ +void * networkedServerThread(void *arg); \ No newline at end of file diff --git a/src/server/packet/CMakeLists.txt b/src/server/packet/CMakeLists.txt index 5162f7c..6e7644a 100644 --- a/src/server/packet/CMakeLists.txt +++ b/src/server/packet/CMakeLists.txt @@ -8,4 +8,5 @@ target_sources(${DUSK_TARGET_NAME} PRIVATE packet.c packetwelcome.c + packetdisconnect.c ) \ No newline at end of file diff --git a/src/server/packet/packetdisconnect.h b/src/server/packet/packetdisconnect.h index d13e1f2..4b4e6d5 100644 --- a/src/server/packet/packetdisconnect.h +++ b/src/server/packet/packetdisconnect.h @@ -14,6 +14,8 @@ typedef enum { PACKET_DISCONNECT_REASON_UNKNOWN = 0, PACKET_DISCONNECT_REASON_INVALID_VERSION = 1, PACKET_DISCONNECT_REASON_MALFORMED_PACKET = 2, + PACKET_DISCONNECT_REASON_SERVER_FULL = 3, + PACKET_DISCONNECT_REASON_SERVER_SHUTDOWN = 4, } packetdisconnectreason_t; typedef struct { diff --git a/src/server/server.c b/src/server/server.c index 7b58e91..284a7c5 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -17,142 +17,25 @@ void serverInit() { SERVER.state = SERVER_STATE_STOPPED; } -errorret_t serverStart(uint16_t port) { +errorret_t serverStart(const serverstart_t start) { + assertIsMainThread("Server start must be on main thread"); + + // Do not start a running server. if(SERVER.state != SERVER_STATE_STOPPED) { return error("Server is already running"); } - consolePrint("Starting server on port %d...\n", port); - memoryZero(&SERVER, sizeof(server_t)); + SERVER.type = start.type; - // Initialize the server socket - SERVER.serverSocket = socket(AF_INET, SOCK_STREAM, 0); - if(SERVER.serverSocket < 0) { - return error("Failed to create socket"); - } - SERVER.serverAddress.sin_family = AF_INET; - SERVER.serverAddress.sin_addr.s_addr = INADDR_ANY; - SERVER.serverAddress.sin_port = htons(port); - - // Bind the socket to the address and port - int32_t res = bind( - SERVER.serverSocket, - (struct sockaddr *)&SERVER.serverAddress, - sizeof(SERVER.serverAddress) - ); - if(res != 0) { - close(SERVER.serverSocket); - return error("Failed to bind socket"); - } - - // Set the socket to listen for incoming connections - res = listen(SERVER.serverSocket, SERVER_MAX_CLIENTS); - if(res != 0) { - close(SERVER.serverSocket); - return error("Failed to listen on socket"); - } - - // Start the server thread. - SERVER.state = SERVER_STATE_STARTING; - res = pthread_create(&SERVER.thread, NULL, serverThread, NULL); - if(res != 0) { - perror("Failed to create server thread"); - serverStop(); - return error("Failed to create server thread"); - } - - // Wait - while(SERVER.state == SERVER_STATE_STARTING) { - usleep(1000); - } - - consolePrint("Server started."); - return ERROR_OK; -} - -void * serverThread(void *arg) { - struct timeval timeout; - fd_set readfds; - - SERVER.state = SERVER_STATE_RUNNING; - - while(SERVER.state == SERVER_STATE_RUNNING) { - usleep(1000); - - FD_ZERO(&readfds); - FD_SET(SERVER.serverSocket, &readfds); - - // Fix timeout struct - timeout.tv_sec = 1; - timeout.tv_usec = 0; - - // Wait for - int32_t activity = select( - SERVER.serverSocket + 1, - &readfds, - NULL, - NULL, - &timeout - ); - - // Check for errors - if(activity < 0) { - consolePrint("Error in select"); - continue; - } - - // Timeout - if(activity == 0) continue; - - // Check if there is a new connection - if(!FD_ISSET(SERVER.serverSocket, &readfds)) continue; - - // Accept new connection - int32_t clientSocket = accept(SERVER.serverSocket, NULL, NULL); - if(clientSocket < 0) { - consolePrint("Failed to accept connection"); - continue; - } - - // Initialize the client - serverclient_t *client = NULL; - uint8_t i = 0; - do { - if(SERVER.clients[i].state != SERVER_CLIENT_STATE_DISCONNECTED) { - i++; - continue; - } - client = &SERVER.clients[i]; + // Hand off to relevant server type to start. + switch(start.type) { + case SERVER_TYPE_NETWORKED: + return networkedServerStart(&SERVER, start); break; - } while(i < SERVER_MAX_CLIENTS); - // Can we receive this client? - if(!client) { - const char_t *errMsg = "ERR|Server full"; - send(clientSocket, errMsg, strlen(errMsg), 0); - consolePrint("Server full, closing connection."); - close(clientSocket); - continue; - } - - errorret_t ret = serverClientAccept(client, clientSocket); - if(ret != ERROR_OK) { - consolePrint("Failed to accept client connection", errorString()); - errorFlush(); - close(clientSocket); - } + default: + assertUnreachable("Invalid server type"); } - - printf("Server thread exiting.\n"); - assertTrue( - SERVER.state != SERVER_STATE_RUNNING, - "Server thread exiting while server is running?" - ); - - SERVER.thread = 0; - SERVER.state = SERVER_STATE_STOPPED; - - return NULL; } uint8_t serverGetClientCount() { @@ -165,46 +48,18 @@ uint8_t serverGetClientCount() { } void serverStop() { + assertIsMainThread("Server stop must be on main thread"); if(SERVER.state == SERVER_STATE_STOPPED) return; - assertTrue( - SERVER.state != SERVER_STATE_STARTING, - "Stopping a server that is starting?\n" - ); - consolePrint("Stopping server..."); - SERVER.state = SERVER_STATE_STOPPING; + switch(SERVER.type) { + case SERVER_TYPE_NETWORKED: + networkedServerStop(&SERVER); + break; - // Wait for the server thread to finish - int32_t maxWaits = 0; - while(SERVER.state == SERVER_STATE_STOPPING) { - usleep(1000); - maxWaits++; - if(maxWaits < 1000) continue; - - consolePrint("Server thread did not stop in time, forcing exit."); - SERVER.state = SERVER_STATE_STOPPED; - break; + default: + assertUnreachable("Invalid server type"); } - - // Kill the clients - uint8_t i = 0; - do { - serverclient_t *client = &SERVER.clients[i++]; - serverClientClose(client); - } while(i < SERVER_MAX_CLIENTS); - - // Stop the server - if(SERVER.serverSocket >= 0) { - close(SERVER.serverSocket); - SERVER.serverSocket = -1; - } - - if(SERVER.thread) { - pthread_join(SERVER.thread, NULL); - SERVER.thread = 0; - } - consolePrint("Server stopped."); } diff --git a/src/server/server.h b/src/server/server.h index 191bcf8..8d77bd9 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -7,11 +7,9 @@ #pragma once #include "serverclient.h" -#include -#include +#include "server/networked/networkedserver.h" -#define SERVER_MAX_CLIENTS 32 -#define SERVER_MAX_CHANNELS 2 +#define SERVER_MAX_CLIENTS 1 typedef enum { SERVER_STATE_STOPPED, @@ -20,12 +18,29 @@ typedef enum { SERVER_STATE_STOPPING } serverstate_t; -typedef struct { +typedef enum { + SERVER_TYPE_SINGLE_PLAYER, + SERVER_TYPE_NETWORKED +} servertype_t; + +typedef struct serverstart_s { + servertype_t type; + union { + struct { + uint16_t port; + } networked; + }; +} serverstart_t; + +typedef struct server_s { serverstate_t state; + servertype_t type; pthread_t thread; - int serverSocket; - struct sockaddr_in serverAddress; serverclient_t clients[SERVER_MAX_CLIENTS]; + + union { + networkedserver_t networked; + }; } server_t; extern server_t SERVER; @@ -47,17 +62,7 @@ void serverInit(); * @param port The port number to bind the server to. * @return Returns an error code if the server fails to start. */ -errorret_t serverStart(uint16_t port); - -/** - * Server thread function - * - * This function runs in a separate thread and handles incoming connections, - * messages, and disconnections. - * - * @param arg Pointer to the argument passed to the thread. - */ -void * serverThread(void *arg); +errorret_t serverStart(const serverstart_t start); /** * Get the client count diff --git a/src/server/serverclient.c b/src/server/serverclient.c index 54815d4..5ebcb5e 100644 --- a/src/server/serverclient.c +++ b/src/server/serverclient.c @@ -5,206 +5,41 @@ * https://opensource.org/licenses/MIT */ -#include "serverclient.h" #include "server.h" #include "assert/assert.h" #include "util/memory.h" -#include #include "console/console.h" errorret_t serverClientAccept( serverclient_t *client, - const int32_t socket + const serverclientaccept_t accept ) { memoryZero(client, sizeof(serverclient_t)); - client->socket = socket; - client->state = SERVER_CLIENT_STATE_ACCEPTING; + assertNotNull(accept.server, "Server is NULL"); + assertNotMainThread("Server client accept must not be main thread"); - // Set timeout to 8 seconds - client->timeout.tv_sec = 8; - client->timeout.tv_usec = 0; + client->server = accept.server; - // Create a thread for the client - if(pthread_create(&client->thread, NULL, serverClientThread, client) != 0) { - client->state = SERVER_CLIENT_STATE_DISCONNECTED; - return error("Failed to create client thread"); + switch(accept.server->type) { + case SERVER_TYPE_NETWORKED: + return networkedClientAccept(client, accept); + break; + + default: + assertUnreachable("Unknown server type"); } } -errorret_t serverClientSend( - serverclient_t * client, - const uint8_t *data, - const size_t len -) { - assertNotNull(client, "Client is NULL"); - assertNotNull(data, "Data is NULL"); - assertTrue(len > 0, "Data length is 0"); - - if(client->state != SERVER_CLIENT_STATE_ACCEPTING) { - return error("Client is not accepting"); - } - ssize_t sent = send(client->socket, data, len, 0); - if(sent < 0) return error("Failed to send data"); - return ERROR_OK; -} - -errorret_t serverClientSendString( - serverclient_t * client, - const char_t *data -) { - return serverClientSend(client, (const uint8_t *)data, strlen(data)); -} - -errorret_t serverClientSendPacket( - serverclient_t * client, - const packet_t *packet -) { - assertNotNull(client, "Client is NULL"); - assertNotNull(packet, "Packet is NULL"); - assertTrue(packet->type != PACKET_TYPE_INVALID, "Packet type is INVALID"); - assertTrue(packet->length > 0, "Packet length is 0"); - assertTrue( - packet->length <= sizeof(packetdata_t), - "Packet length is too large (1)" - ); - - size_t fullSize = sizeof(packet_t) - sizeof(packet->data) + packet->length; - assertTrue(fullSize <= sizeof(packet_t), "Packet size is too large (2)"); - - return serverClientSend(client, (const uint8_t *)packet, fullSize); -} - -ssize_t serverClientReceive( - serverclient_t * client, - uint8_t *buffer, - const size_t len -) { - assertNotNull(client, "Client is NULL"); - assertNotNull(buffer, "Buffer is NULL"); - assertTrue(len > 0, "Buffer length is 0"); - - if(client->state == SERVER_CLIENT_STATE_DISCONNECTED) return -1; - if(client->state == SERVER_CLIENT_STATE_DISCONNECTING) return -1; - - return recv(client->socket, buffer, len, 0); -} - void serverClientClose(serverclient_t *client) { assertNotNull(client, "Client is NULL"); + assertIsMainThread("Server client close must be on main thread"); if(client->state == SERVER_CLIENT_STATE_DISCONNECTED) return; - // Mark client as disconnecting - client->state = SERVER_CLIENT_STATE_DISCONNECTING; - - int32_t maxWaits = 0; - while(client->state == SERVER_CLIENT_STATE_DISCONNECTING) { - // Wait for the thread to finish - usleep(1000); - maxWaits++; - if(maxWaits > 10) break; - } - - pthread_join(client->thread, NULL); - client->thread = 0; - - // Close the socket - if(client->socket != -1) { - close(client->socket); - client->socket = -1; - } - - client->state = SERVER_CLIENT_STATE_DISCONNECTED; - consolePrint("Client %d disconnected.", client->socket); -} - -void serverClientCloseOnThread(serverclient_t *client, const char_t *reason) { - assertNotNull(client, "Client is NULL"); - assertNotNull(reason, "Reason is NULL"); - - client->state = SERVER_CLIENT_STATE_DISCONNECTING; - - close(client->socket); - client->socket = -1; - - client->state = SERVER_CLIENT_STATE_DISCONNECTED; - client->thread = 0; - - consolePrint("Client %d disconnected: %s", client->socket, reason); - pthread_exit(NULL); -} - -void *serverClientThread(void *arg) { - assertNotNull(arg, "Client is NULL"); - - serverclient_t *client = (serverclient_t *)arg; - char_t buffer[1024]; - ssize_t read; - errorret_t err; - packet_t packet; - - consolePrint("Accepting client on socket %d.", client->socket); - - // Set socket timeout - if(setsockopt( - client->socket, - SOL_SOCKET, - SO_RCVTIMEO, - &client->timeout, - sizeof(client->timeout) - ) < 0) { - serverClientCloseOnThread(client, "Failed to set socket timeout"); - return NULL; - } - - // First message from the client should always be "DUSK|VERSION" to match - // the server version. - { - const char_t *expecting = "DUSK|"DUSK_VERSION; - read = serverClientReceive(client, buffer, sizeof(buffer)); - if(read <= 0) { - packetDisconnectCreate(&packet, PACKET_DISCONNECT_REASON_INVALID_VERSION); - err = serverClientSendPacket(client, &packet); - serverClientCloseOnThread(client, "Failed to receive version"); - return NULL; - } - - buffer[read] = '\0'; // Null-terminate the string - if(strncmp(buffer, expecting, strlen(expecting)) != 0) { - packetDisconnectCreate(&packet, PACKET_DISCONNECT_REASON_INVALID_VERSION); - err = serverClientSendPacket(client, &packet); - serverClientCloseOnThread(client, "Invalid version"); - return NULL; - } - } - - // Send DUSK back! - packetWelcomeCreate(&packet); - err = serverClientSendPacket(client, &packet); - if(err != ERROR_OK) { - serverClientCloseOnThread(client, "Failed to send welcome message"); - return NULL; - } - - client->state = SERVER_CLIENT_STATE_CONNECTED; - while(client->state == SERVER_CLIENT_STATE_CONNECTED) { - read = serverClientReceive(client, buffer, sizeof(buffer)); - - if(read <= 0) { - serverClientCloseOnThread(client, "Failed to receive data"); + switch(client->server->type) { + case SERVER_TYPE_NETWORKED: + networkedClientClose(client); break; - } - - buffer[read] = '\0'; // Null-terminate the string - consolePrint("Received: %s", buffer); - - - if(SERVER.state != SERVER_STATE_RUNNING) { - serverClientCloseOnThread(client, "Server is shutting down"); - break; - } + default: + assertUnreachable("Unknown server type"); } - - serverClientSendString(client, "Server is shutting down"); - client->state = SERVER_CLIENT_STATE_DISCONNECTED; - return NULL; } \ No newline at end of file diff --git a/src/server/serverclient.h b/src/server/serverclient.h index 5f65a5c..1e2130e 100644 --- a/src/server/serverclient.h +++ b/src/server/serverclient.h @@ -6,8 +6,10 @@ */ #pragma once -#include "error/error.h" #include "server/packet/packet.h" +#include "server/networked/networkedclient.h" + +typedef struct server_s server_t; typedef enum { SERVER_CLIENT_STATE_DISCONNECTED, @@ -16,75 +18,31 @@ typedef enum { SERVER_CLIENT_STATE_DISCONNECTING, } serverclientstate_t; -typedef struct { - int32_t socket; +typedef struct serverclientaccept_s { + server_t *server; + union { + networkedclientaccept_t networked; + }; +} serverclientaccept_t; + +typedef struct serverclient_s { + server_t *server; serverclientstate_t state; - pthread_t thread; - struct timeval timeout; + union { + networkedclient_t networked; + }; } serverclient_t; /** * Accepts an incoming connection from a server client. * * @param client Pointer to the server client structure. - * @param socket The socket file descriptor for the client. + * @param accept Accept structure containing client information. * @return Error code indicating success or failure. */ errorret_t serverClientAccept( serverclient_t *client, - const int32_t socket -); - -/** - * Sends data to a server client. - * - * @param client Pointer to the server client structure. - * @param data Pointer to the data to send. - * @param len Length of the data to send. - * @return Error code indicating success or failure. - */ -errorret_t serverClientSend( - serverclient_t * client, - const uint8_t *data, - const size_t len -); - -/** - * Sends a string to a server client. - * - * @param client Pointer to the server client structure. - * @param data Pointer to the string to send. - * @return Error code indicating success or failure. - */ -errorret_t serverClientSendString( - serverclient_t * client, - const char_t *data -); - -/** - * Sends a packet to a server client. - * - * @param client Pointer to the server client structure. - * @param packet Pointer to the packet to send. - * @return Error code indicating success or failure. - */ -errorret_t serverClientSendPacket( - serverclient_t * client, - const packet_t *packet -); - -/** - * Receives data from a server client. - * - * @param client Pointer to the server client structure. - * @param buffer Pointer to the buffer to store received data. - * @param len Max length of the buffer. - * @return Number of bytes received. 0 or less indicates an error. - */ -ssize_t serverClientReceive( - serverclient_t * client, - uint8_t *buffer, - const size_t len + const serverclientaccept_t accept ); /** @@ -92,24 +50,4 @@ ssize_t serverClientReceive( * * @param client Pointer to the server client structure. */ -void serverClientClose(serverclient_t *client); - -/** - * Closes the connection to a server client without waiting for the thread to - * finish. - * - * @param client Pointer to the server client structure. - * @param reason Reason for closing the connection. - */ -void serverClientCloseOnThread( - serverclient_t *client, - const char_t *reason -); - -/** - * Thread function for handling a server client. - * - * @param arg Pointer to the server client structure. - * @return NULL. - */ -void *serverClientThread(void *arg); \ No newline at end of file +void serverClientClose(serverclient_t *client); \ No newline at end of file diff --git a/src/server/singleplayer/CMakeLists.txt b/src/server/singleplayer/CMakeLists.txt new file mode 100644 index 0000000..fb0e252 --- /dev/null +++ b/src/server/singleplayer/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2025 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_TARGET_NAME} + PRIVATE +) \ No newline at end of file