Prepping for singleplayer style client and server.

This commit is contained in:
2025-04-09 14:24:36 -05:00
parent 9208ebb685
commit a04014c054
16 changed files with 753 additions and 445 deletions

View File

@ -7,6 +7,12 @@
#include "assert.h" #include "assert.h"
pthread_t assertMainThread = 0;
void assertInit() {
assertMainThread = pthread_self();
}
void assertTrueImpl( void assertTrueImpl(
const char *file, const char *file,
const int32_t line, const int32_t line,

View File

@ -8,6 +8,13 @@
#pragma once #pragma once
#include "dusk.h" #include "dusk.h"
extern pthread_t assertMainThread;
/**
* Initialises the assert system.
*/
void assertInit();
/** /**
* Assert a given value to be true. * Assert a given value to be true.
* *
@ -128,4 +135,10 @@ void assertMemoryRangeMatchesImpl(
#define assertStrLenMin(str, len, message) \ #define assertStrLenMin(str, len, message) \
assertTrue(strlen(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 // EOF

View File

@ -8,6 +8,7 @@
#include "console/console.h" #include "console/console.h"
#include "server/server.h" #include "server/server.h"
#include "util/string.h" #include "util/string.h"
#include "assert/assert.h"
bool_t exitRequested = false; 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) { if(ret != ERROR_OK) {
consolePrint("Failed to start server: %s", errorString()); consolePrint("Failed to start server: %s", errorString());
errorFlush(); errorFlush();
@ -37,6 +44,7 @@ void cmdClose(const consolecmdexec_t *exec) {
} }
int main(void) { int main(void) {
assertInit();
consoleInit(); consoleInit();
serverInit(); serverInit();

View File

@ -12,3 +12,5 @@ target_sources(${DUSK_TARGET_NAME}
# Subdirs # Subdirs
add_subdirectory(packet) add_subdirectory(packet)
add_subdirectory(networked)
add_subdirectory(singleplayer)

View File

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

View File

@ -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 <sys/socket.h>
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;
}

View File

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

View File

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

View File

@ -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 <netinet/in.h>
#include <arpa/inet.h>
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);

View File

@ -8,4 +8,5 @@ target_sources(${DUSK_TARGET_NAME}
PRIVATE PRIVATE
packet.c packet.c
packetwelcome.c packetwelcome.c
packetdisconnect.c
) )

View File

@ -14,6 +14,8 @@ typedef enum {
PACKET_DISCONNECT_REASON_UNKNOWN = 0, PACKET_DISCONNECT_REASON_UNKNOWN = 0,
PACKET_DISCONNECT_REASON_INVALID_VERSION = 1, PACKET_DISCONNECT_REASON_INVALID_VERSION = 1,
PACKET_DISCONNECT_REASON_MALFORMED_PACKET = 2, PACKET_DISCONNECT_REASON_MALFORMED_PACKET = 2,
PACKET_DISCONNECT_REASON_SERVER_FULL = 3,
PACKET_DISCONNECT_REASON_SERVER_SHUTDOWN = 4,
} packetdisconnectreason_t; } packetdisconnectreason_t;
typedef struct { typedef struct {

View File

@ -17,142 +17,25 @@ void serverInit() {
SERVER.state = SERVER_STATE_STOPPED; 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) { if(SERVER.state != SERVER_STATE_STOPPED) {
return error("Server is already running"); return error("Server is already running");
} }
consolePrint("Starting server on port %d...\n", port); SERVER.type = start.type;
memoryZero(&SERVER, sizeof(server_t));
// Initialize the server socket // Hand off to relevant server type to start.
SERVER.serverSocket = socket(AF_INET, SOCK_STREAM, 0); switch(start.type) {
if(SERVER.serverSocket < 0) { case SERVER_TYPE_NETWORKED:
return error("Failed to create socket"); return networkedServerStart(&SERVER, start);
}
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];
break; break;
} while(i < SERVER_MAX_CLIENTS);
// Can we receive this client? default:
if(!client) { assertUnreachable("Invalid server type");
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);
}
}
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() { uint8_t serverGetClientCount() {
@ -165,46 +48,18 @@ uint8_t serverGetClientCount() {
} }
void serverStop() { void serverStop() {
assertIsMainThread("Server stop must be on main thread");
if(SERVER.state == SERVER_STATE_STOPPED) return; if(SERVER.state == SERVER_STATE_STOPPED) return;
assertTrue(
SERVER.state != SERVER_STATE_STARTING,
"Stopping a server that is starting?\n"
);
consolePrint("Stopping server..."); consolePrint("Stopping server...");
SERVER.state = SERVER_STATE_STOPPING; switch(SERVER.type) {
case SERVER_TYPE_NETWORKED:
// Wait for the server thread to finish networkedServerStop(&SERVER);
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; 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."); consolePrint("Server stopped.");
} }

View File

@ -7,11 +7,9 @@
#pragma once #pragma once
#include "serverclient.h" #include "serverclient.h"
#include <netinet/in.h> #include "server/networked/networkedserver.h"
#include <arpa/inet.h>
#define SERVER_MAX_CLIENTS 32 #define SERVER_MAX_CLIENTS 1
#define SERVER_MAX_CHANNELS 2
typedef enum { typedef enum {
SERVER_STATE_STOPPED, SERVER_STATE_STOPPED,
@ -20,12 +18,29 @@ typedef enum {
SERVER_STATE_STOPPING SERVER_STATE_STOPPING
} serverstate_t; } 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; serverstate_t state;
servertype_t type;
pthread_t thread; pthread_t thread;
int serverSocket;
struct sockaddr_in serverAddress;
serverclient_t clients[SERVER_MAX_CLIENTS]; serverclient_t clients[SERVER_MAX_CLIENTS];
union {
networkedserver_t networked;
};
} server_t; } server_t;
extern server_t SERVER; extern server_t SERVER;
@ -47,17 +62,7 @@ void serverInit();
* @param port The port number to bind the server to. * @param port The port number to bind the server to.
* @return Returns an error code if the server fails to start. * @return Returns an error code if the server fails to start.
*/ */
errorret_t serverStart(uint16_t port); errorret_t serverStart(const serverstart_t start);
/**
* 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);
/** /**
* Get the client count * Get the client count

View File

@ -5,206 +5,41 @@
* https://opensource.org/licenses/MIT * https://opensource.org/licenses/MIT
*/ */
#include "serverclient.h"
#include "server.h" #include "server.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include <sys/socket.h>
#include "console/console.h" #include "console/console.h"
errorret_t serverClientAccept( errorret_t serverClientAccept(
serverclient_t *client, serverclient_t *client,
const int32_t socket const serverclientaccept_t accept
) { ) {
memoryZero(client, sizeof(serverclient_t)); memoryZero(client, sizeof(serverclient_t));
client->socket = socket; assertNotNull(accept.server, "Server is NULL");
client->state = SERVER_CLIENT_STATE_ACCEPTING; assertNotMainThread("Server client accept must not be main thread");
// Set timeout to 8 seconds client->server = accept.server;
client->timeout.tv_sec = 8;
client->timeout.tv_usec = 0;
// Create a thread for the client switch(accept.server->type) {
if(pthread_create(&client->thread, NULL, serverClientThread, client) != 0) { case SERVER_TYPE_NETWORKED:
client->state = SERVER_CLIENT_STATE_DISCONNECTED; return networkedClientAccept(client, accept);
return error("Failed to create client thread"); 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) { void serverClientClose(serverclient_t *client) {
assertNotNull(client, "Client is NULL"); assertNotNull(client, "Client is NULL");
assertIsMainThread("Server client close must be on main thread");
if(client->state == SERVER_CLIENT_STATE_DISCONNECTED) return; if(client->state == SERVER_CLIENT_STATE_DISCONNECTED) return;
// Mark client as disconnecting switch(client->server->type) {
client->state = SERVER_CLIENT_STATE_DISCONNECTING; case SERVER_TYPE_NETWORKED:
networkedClientClose(client);
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");
break; break;
default:
assertUnreachable("Unknown server type");
} }
buffer[read] = '\0'; // Null-terminate the string
consolePrint("Received: %s", buffer);
if(SERVER.state != SERVER_STATE_RUNNING) {
serverClientCloseOnThread(client, "Server is shutting down");
break;
}
}
serverClientSendString(client, "Server is shutting down");
client->state = SERVER_CLIENT_STATE_DISCONNECTED;
return NULL;
} }

View File

@ -6,8 +6,10 @@
*/ */
#pragma once #pragma once
#include "error/error.h"
#include "server/packet/packet.h" #include "server/packet/packet.h"
#include "server/networked/networkedclient.h"
typedef struct server_s server_t;
typedef enum { typedef enum {
SERVER_CLIENT_STATE_DISCONNECTED, SERVER_CLIENT_STATE_DISCONNECTED,
@ -16,75 +18,31 @@ typedef enum {
SERVER_CLIENT_STATE_DISCONNECTING, SERVER_CLIENT_STATE_DISCONNECTING,
} serverclientstate_t; } serverclientstate_t;
typedef struct { typedef struct serverclientaccept_s {
int32_t socket; server_t *server;
union {
networkedclientaccept_t networked;
};
} serverclientaccept_t;
typedef struct serverclient_s {
server_t *server;
serverclientstate_t state; serverclientstate_t state;
pthread_t thread; union {
struct timeval timeout; networkedclient_t networked;
};
} serverclient_t; } serverclient_t;
/** /**
* Accepts an incoming connection from a server client. * Accepts an incoming connection from a server client.
* *
* @param client Pointer to the server client structure. * @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. * @return Error code indicating success or failure.
*/ */
errorret_t serverClientAccept( errorret_t serverClientAccept(
serverclient_t *client, serverclient_t *client,
const int32_t socket const serverclientaccept_t accept
);
/**
* 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
); );
/** /**
@ -93,23 +51,3 @@ ssize_t serverClientReceive(
* @param client Pointer to the server client structure. * @param client Pointer to the server client structure.
*/ */
void serverClientClose(serverclient_t *client); 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);

View File

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