From f8607d114c200d3065e1fe80b637c53762e706e9 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sun, 2 Aug 2026 21:06:23 -0500 Subject: [PATCH] Add UDP socket client/server multiplayer protocol Implements the multiplayer transport layer from ROADMAP.md: UUID-based client identity, a reliable-packet channel over UDP, handshake/ack/ ping/disconnect/player-joined/player-left/player-state packet types, and split online (networked) vs offline (in-process singleplayer) server modes wired into the engine's update/dispose loop. --- ROADMAP.md | 5 +- src/dusk/engine/engine.c | 10 + src/dusk/network/CMakeLists.txt | 2 + src/dusk/network/networkaddr.c | 16 ++ src/dusk/network/networkaddr.h | 23 ++ src/dusk/network/socket/CMakeLists.txt | 16 ++ src/dusk/network/socket/client/CMakeLists.txt | 15 + src/dusk/network/socket/client/client.c | 16 ++ src/dusk/network/socket/client/client.h | 34 +++ src/dusk/network/socket/client/clientroster.c | 51 ++++ src/dusk/network/socket/client/clientroster.h | 50 ++++ src/dusk/network/socket/client/clientthread.c | 43 +++ src/dusk/network/socket/client/clientthread.h | 29 ++ .../socket/client/handler/CMakeLists.txt | 17 ++ .../client/handler/clientpackethandlerack.c | 21 ++ .../client/handler/clientpackethandlerack.h | 22 ++ .../handler/clientpackethandlerdisconnect.c | 27 ++ .../handler/clientpackethandlerdisconnect.h | 22 ++ .../clientpackethandlerhandshakeaccept.c | 25 ++ .../clientpackethandlerhandshakeaccept.h | 22 ++ .../clientpackethandlerhandshakereject.c | 25 ++ .../clientpackethandlerhandshakereject.h | 21 ++ .../client/handler/clientpackethandlerlist.c | 28 ++ .../client/handler/clientpackethandlerlist.h | 19 ++ .../client/handler/clientpackethandlerping.c | 18 ++ .../client/handler/clientpackethandlerping.h | 22 ++ .../handler/clientpackethandlerplayerjoined.c | 32 +++ .../handler/clientpackethandlerplayerjoined.h | 23 ++ .../handler/clientpackethandlerplayerleft.c | 25 ++ .../handler/clientpackethandlerplayerleft.h | 22 ++ .../handler/clientpackethandlerplayerstate.c | 36 +++ .../handler/clientpackethandlerplayerstate.h | 23 ++ src/dusk/network/socket/client/localclient.c | 263 ++++++++++++++++++ src/dusk/network/socket/client/localclient.h | 169 +++++++++++ src/dusk/network/socket/client/remoteclient.h | 20 ++ src/dusk/network/socket/packet.c | 139 +++++++++ src/dusk/network/socket/packet.h | 89 ++++++ src/dusk/network/socket/packetdata.h | 29 ++ src/dusk/network/socket/packetinbox.c | 53 ++++ src/dusk/network/socket/packetinbox.h | 58 ++++ src/dusk/network/socket/packetregistry.c | 44 +++ src/dusk/network/socket/packetregistry.h | 17 ++ src/dusk/network/socket/packetreliable.c | 118 ++++++++ src/dusk/network/socket/packetreliable.h | 119 ++++++++ src/dusk/network/socket/packettype.h | 24 ++ src/dusk/network/socket/payload/packetack.h | 13 + .../network/socket/payload/packetdisconnect.h | 13 + .../network/socket/payload/packethandshake.h | 24 ++ .../socket/payload/packethandshakeaccept.h | 13 + .../socket/payload/packethandshakereject.h | 20 ++ src/dusk/network/socket/payload/packetping.h | 12 + .../socket/payload/packetplayerjoined.h | 15 + .../network/socket/payload/packetplayerleft.h | 13 + .../socket/payload/packetplayerstate.h | 16 ++ src/dusk/network/socket/server/CMakeLists.txt | 15 + .../socket/server/handler/CMakeLists.txt | 14 + .../server/handler/serverpackethandlerack.c | 24 ++ .../server/handler/serverpackethandlerack.h | 24 ++ .../handler/serverpackethandlerdisconnect.c | 25 ++ .../handler/serverpackethandlerdisconnect.h | 26 ++ .../handler/serverpackethandlerhandshake.c | 148 ++++++++++ .../handler/serverpackethandlerhandshake.h | 52 ++++ .../server/handler/serverpackethandlerlist.c | 20 ++ .../server/handler/serverpackethandlerlist.h | 27 ++ .../server/handler/serverpackethandlerping.c | 20 ++ .../server/handler/serverpackethandlerping.h | 24 ++ .../handler/serverpackethandlerplayerstate.c | 34 +++ .../handler/serverpackethandlerplayerstate.h | 24 ++ src/dusk/network/socket/server/serverclient.c | 102 +++++++ src/dusk/network/socket/server/serverclient.h | 80 ++++++ .../network/socket/server/serverclientlocal.h | 16 ++ .../socket/server/serverclientremote.h | 27 ++ .../network/socket/server/serveroffline.c | 148 ++++++++++ .../network/socket/server/serveroffline.h | 129 +++++++++ src/dusk/network/socket/server/serveronline.c | 160 +++++++++++ src/dusk/network/socket/server/serveronline.h | 88 ++++++ src/dusk/network/socket/server/serverthread.c | 59 ++++ src/dusk/network/socket/server/serverthread.h | 32 +++ src/dusk/util/CMakeLists.txt | 1 + src/dusk/util/uuid.c | 37 +++ src/dusk/util/uuid.h | 45 +++ src/dusklinux/network/CMakeLists.txt | 1 + .../network/networkdgramsocketlinux.c | 196 +++++++++++++ .../network/networkdgramsocketlinux.h | 135 +++++++++ .../network/networkdgramsocketplatform.h | 20 ++ test/network/CMakeLists.txt | 5 + test/network/socket/CMakeLists.txt | 9 + test/network/socket/test_networksocket.c | 258 +++++++++++++++++ test/network/socket/test_serveroffline.c | 121 ++++++++ test/network/test_network.c | 62 +++++ 90 files changed, 4247 insertions(+), 2 deletions(-) create mode 100644 src/dusk/network/networkaddr.c create mode 100644 src/dusk/network/networkaddr.h create mode 100644 src/dusk/network/socket/CMakeLists.txt create mode 100644 src/dusk/network/socket/client/CMakeLists.txt create mode 100644 src/dusk/network/socket/client/client.c create mode 100644 src/dusk/network/socket/client/client.h create mode 100644 src/dusk/network/socket/client/clientroster.c create mode 100644 src/dusk/network/socket/client/clientroster.h create mode 100644 src/dusk/network/socket/client/clientthread.c create mode 100644 src/dusk/network/socket/client/clientthread.h create mode 100644 src/dusk/network/socket/client/handler/CMakeLists.txt create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerack.c create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerack.h create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerdisconnect.c create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerdisconnect.h create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerhandshakeaccept.c create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerhandshakeaccept.h create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerhandshakereject.c create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerhandshakereject.h create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerlist.c create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerlist.h create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerping.c create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerping.h create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerplayerjoined.c create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerplayerjoined.h create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerplayerleft.c create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerplayerleft.h create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerplayerstate.c create mode 100644 src/dusk/network/socket/client/handler/clientpackethandlerplayerstate.h create mode 100644 src/dusk/network/socket/client/localclient.c create mode 100644 src/dusk/network/socket/client/localclient.h create mode 100644 src/dusk/network/socket/client/remoteclient.h create mode 100644 src/dusk/network/socket/packet.c create mode 100644 src/dusk/network/socket/packet.h create mode 100644 src/dusk/network/socket/packetdata.h create mode 100644 src/dusk/network/socket/packetinbox.c create mode 100644 src/dusk/network/socket/packetinbox.h create mode 100644 src/dusk/network/socket/packetregistry.c create mode 100644 src/dusk/network/socket/packetregistry.h create mode 100644 src/dusk/network/socket/packetreliable.c create mode 100644 src/dusk/network/socket/packetreliable.h create mode 100644 src/dusk/network/socket/packettype.h create mode 100644 src/dusk/network/socket/payload/packetack.h create mode 100644 src/dusk/network/socket/payload/packetdisconnect.h create mode 100644 src/dusk/network/socket/payload/packethandshake.h create mode 100644 src/dusk/network/socket/payload/packethandshakeaccept.h create mode 100644 src/dusk/network/socket/payload/packethandshakereject.h create mode 100644 src/dusk/network/socket/payload/packetping.h create mode 100644 src/dusk/network/socket/payload/packetplayerjoined.h create mode 100644 src/dusk/network/socket/payload/packetplayerleft.h create mode 100644 src/dusk/network/socket/payload/packetplayerstate.h create mode 100644 src/dusk/network/socket/server/CMakeLists.txt create mode 100644 src/dusk/network/socket/server/handler/CMakeLists.txt create mode 100644 src/dusk/network/socket/server/handler/serverpackethandlerack.c create mode 100644 src/dusk/network/socket/server/handler/serverpackethandlerack.h create mode 100644 src/dusk/network/socket/server/handler/serverpackethandlerdisconnect.c create mode 100644 src/dusk/network/socket/server/handler/serverpackethandlerdisconnect.h create mode 100644 src/dusk/network/socket/server/handler/serverpackethandlerhandshake.c create mode 100644 src/dusk/network/socket/server/handler/serverpackethandlerhandshake.h create mode 100644 src/dusk/network/socket/server/handler/serverpackethandlerlist.c create mode 100644 src/dusk/network/socket/server/handler/serverpackethandlerlist.h create mode 100644 src/dusk/network/socket/server/handler/serverpackethandlerping.c create mode 100644 src/dusk/network/socket/server/handler/serverpackethandlerping.h create mode 100644 src/dusk/network/socket/server/handler/serverpackethandlerplayerstate.c create mode 100644 src/dusk/network/socket/server/handler/serverpackethandlerplayerstate.h create mode 100644 src/dusk/network/socket/server/serverclient.c create mode 100644 src/dusk/network/socket/server/serverclient.h create mode 100644 src/dusk/network/socket/server/serverclientlocal.h create mode 100644 src/dusk/network/socket/server/serverclientremote.h create mode 100644 src/dusk/network/socket/server/serveroffline.c create mode 100644 src/dusk/network/socket/server/serveroffline.h create mode 100644 src/dusk/network/socket/server/serveronline.c create mode 100644 src/dusk/network/socket/server/serveronline.h create mode 100644 src/dusk/network/socket/server/serverthread.c create mode 100644 src/dusk/network/socket/server/serverthread.h create mode 100644 src/dusk/util/uuid.c create mode 100644 src/dusk/util/uuid.h create mode 100644 src/dusklinux/network/networkdgramsocketlinux.c create mode 100644 src/dusklinux/network/networkdgramsocketlinux.h create mode 100644 src/dusklinux/network/networkdgramsocketplatform.h create mode 100644 test/network/socket/CMakeLists.txt create mode 100644 test/network/socket/test_networksocket.c create mode 100644 test/network/socket/test_serveroffline.c create mode 100644 test/network/test_network.c diff --git a/ROADMAP.md b/ROADMAP.md index 6e30517c..d97c07d4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -33,8 +33,9 @@ for a memory/CPU optimization survey specifically targeting the PSP build 9. Build the socket server and client implementation, including handlers for the different packet types. 10. Add a dedicated multiplayer entity type, `clientplayer`, alongside - the existing `npc` and `player` types. Limit to 8 (defined - constant) for now. + the existing `npc` and `player` types. Limit to 16 (defined + constant, see SERVER_CLIENT_COUNT_MAX/CLIENT_COUNT_MAX in + src/dusk/network/socket/) for now. 11. Send and receive `clientplayer` position over the network. 12. Create a UI menu for creating a server and joining a server. For now, join IPs are hard-coded (testing against a fixed IP of diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 5881e956..b726946b 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -18,6 +18,9 @@ #include "ui/ui.h" #include "assert/assert.h" #include "network/network.h" +#include "network/socket/client/client.h" +#include "network/socket/client/clientroster.h" +#include "network/socket/server/serveronline.h" #include "game/game.h" #include "system/system.h" #include "console/console.h" @@ -67,6 +70,8 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorret_t engineUpdate(void) { // Order here is important. errorChain(networkUpdate()); + errorChain(clientUpdate()); + errorChain(serverOnlineUpdate()); timeUpdate(); const systemdialogtype_t dialogType = systemGetActiveDialogType(); @@ -99,6 +104,11 @@ void engineExit(void) { errorret_t engineDispose(void) { errorChain(gameDispose()); errorChain(sceneDispose()); + + errorChain(serverOnlineStop()); + client_t *localClient = clientRosterFindLocal(); + if(localClient) localClientDisconnect(&localClient->local); + errorChain(networkDispose()); localeManagerDispose(); errorChain(uiDispose()); diff --git a/src/dusk/network/CMakeLists.txt b/src/dusk/network/CMakeLists.txt index 84d07b66..9b78f515 100644 --- a/src/dusk/network/CMakeLists.txt +++ b/src/dusk/network/CMakeLists.txt @@ -7,7 +7,9 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC network.c networkinfo.c + networkaddr.c ) # Subdirs add_subdirectory(http) +add_subdirectory(socket) diff --git a/src/dusk/network/networkaddr.c b/src/dusk/network/networkaddr.c new file mode 100644 index 00000000..0ac0a36f --- /dev/null +++ b/src/dusk/network/networkaddr.c @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "networkaddr.h" +#include "util/memory.h" + +bool_t networkAddrEquals(const networkaddr_t *a, const networkaddr_t *b) { + if(a->port != b->port) return false; + return memoryCompare( + a->ip.ip, b->ip.ip, NETWORK_INFO_IPV4_OCTET_COUNT + ) == 0; +} diff --git a/src/dusk/network/networkaddr.h b/src/dusk/network/networkaddr.h new file mode 100644 index 00000000..cd5a239c --- /dev/null +++ b/src/dusk/network/networkaddr.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "network/networkinfo.h" + +typedef struct { + networkinfoipv4_t ip; + uint16_t port; +} networkaddr_t; + +/** + * Compares two network addresses for equality. + * + * @param a The first address. + * @param b The second address. + * @return true if the IP and port both match, false otherwise. + */ +bool_t networkAddrEquals(const networkaddr_t *a, const networkaddr_t *b); diff --git a/src/dusk/network/socket/CMakeLists.txt b/src/dusk/network/socket/CMakeLists.txt new file mode 100644 index 00000000..389ec56e --- /dev/null +++ b/src/dusk/network/socket/CMakeLists.txt @@ -0,0 +1,16 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + packet.c + packetregistry.c + packetreliable.c + packetinbox.c +) + +# Subdirs +add_subdirectory(client) +add_subdirectory(server) diff --git a/src/dusk/network/socket/client/CMakeLists.txt b/src/dusk/network/socket/client/CMakeLists.txt new file mode 100644 index 00000000..f3e7d63a --- /dev/null +++ b/src/dusk/network/socket/client/CMakeLists.txt @@ -0,0 +1,15 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + client.c + clientroster.c + localclient.c + clientthread.c +) + +# Subdirs +add_subdirectory(handler) diff --git a/src/dusk/network/socket/client/client.c b/src/dusk/network/socket/client/client.c new file mode 100644 index 00000000..9c74a0b6 --- /dev/null +++ b/src/dusk/network/socket/client/client.c @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "client.h" +#include "clientroster.h" + +errorret_t clientUpdate(void) { + client_t *local = clientRosterFindLocal(); + if(!local) errorOk(); + + return localClientUpdate(&local->local); +} diff --git a/src/dusk/network/socket/client/client.h b/src/dusk/network/socket/client/client.h new file mode 100644 index 00000000..019b592a --- /dev/null +++ b/src/dusk/network/socket/client/client.h @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "localclient.h" +#include "remoteclient.h" + +typedef enum { + CLIENT_TYPE_NULL, + CLIENT_TYPE_LOCAL, + CLIENT_TYPE_REMOTE, +} clienttype_t; + +typedef struct { + clienttype_t type; + union { + localclient_t local; + remoteclient_t remote; + }; +} client_t; + +/** + * Drains and updates the roster's local client slot, if any is + * connected/connecting. A no-op if this process isn't a client of any + * server. Called once per frame. + * + * @return An error if a fatal error occurred while processing. + */ +errorret_t clientUpdate(void); diff --git a/src/dusk/network/socket/client/clientroster.c b/src/dusk/network/socket/client/clientroster.c new file mode 100644 index 00000000..2fb5e022 --- /dev/null +++ b/src/dusk/network/socket/client/clientroster.c @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "clientroster.h" +#include "util/memory.h" + +client_t CLIENT_ROSTER[CLIENT_COUNT_MAX]; + +void clientRosterInit(void) { + memoryZero(CLIENT_ROSTER, sizeof(CLIENT_ROSTER)); +} + +client_t * clientRosterFindLocal(void) { + for(uint32_t i = 0; i < CLIENT_COUNT_MAX; i++) { + if(CLIENT_ROSTER[i].type == CLIENT_TYPE_LOCAL) return &CLIENT_ROSTER[i]; + } + + return NULL; +} + +client_t * clientRosterFindById(const uuid_t *id) { + for(uint32_t i = 0; i < CLIENT_COUNT_MAX; i++) { + client_t *client = &CLIENT_ROSTER[i]; + if(client->type != CLIENT_TYPE_REMOTE) continue; + if(uuidEquals(&client->remote.id, id)) return client; + } + + return NULL; +} + +client_t * clientRosterClaim(const clienttype_t type) { + for(uint32_t i = 0; i < CLIENT_COUNT_MAX; i++) { + client_t *client = &CLIENT_ROSTER[i]; + if(client->type != CLIENT_TYPE_NULL) continue; + + memoryZero(client, sizeof(client_t)); + client->type = type; + return client; + } + + return NULL; +} + +void clientRosterRemove(client_t *client) { + memoryZero(client, sizeof(client_t)); + client->type = CLIENT_TYPE_NULL; +} diff --git a/src/dusk/network/socket/client/clientroster.h b/src/dusk/network/socket/client/clientroster.h new file mode 100644 index 00000000..55fdfed2 --- /dev/null +++ b/src/dusk/network/socket/client/clientroster.h @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "util/uuid.h" +#include "network/socket/client/client.h" + +#define CLIENT_COUNT_MAX 16 + +extern client_t CLIENT_ROSTER[CLIENT_COUNT_MAX]; + +/** + * Resets every roster slot back to CLIENT_TYPE_NULL. + */ +void clientRosterInit(void); + +/** + * Finds the one CLIENT_TYPE_LOCAL slot in the roster, if any. + * + * @return The local client slot, or NULL if not connected/hosting. + */ +client_t * clientRosterFindLocal(void); + +/** + * Finds a CLIENT_TYPE_REMOTE slot by its assigned id. + * + * @param id The remote client's id to search for. + * @return The matching slot, or NULL if not found. + */ +client_t * clientRosterFindById(const uuid_t *id); + +/** + * Claims the first CLIENT_TYPE_NULL slot in the roster and assigns it + * the given type. The rest of the slot's data is zeroed. + * + * @param type The type to assign to the claimed slot. + * @return The claimed slot, or NULL if the roster is full. + */ +client_t * clientRosterClaim(const clienttype_t type); + +/** + * Returns a slot to CLIENT_TYPE_NULL, clearing its data. + * + * @param client The slot to free. + */ +void clientRosterRemove(client_t *client); diff --git a/src/dusk/network/socket/client/clientthread.c b/src/dusk/network/socket/client/clientthread.c new file mode 100644 index 00000000..4d29bf15 --- /dev/null +++ b/src/dusk/network/socket/client/clientthread.c @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "clientthread.h" +#include "assert/assert.h" + +void clientThreadRun(thread_t *thread) { + assertNotMainThread("clientThreadRun must not run on the main thread."); + localclient_t *self = (localclient_t *)thread->data; + + uint8_t buffer[PACKET_WIRE_SIZE_MAX]; + + while(!threadShouldStop(thread)) { + size_t received = 0; + errorret_t ret = networkDgramSocketPlatformReceive( + &self->socket, buffer, sizeof(buffer), &received + ); + + if(errorIsNotOk(ret)) { + errorCatch(errorPrint(ret)); + continue; + } + + if(received == 0) continue; + + packet_t packet; + errorret_t decodeRet = packetWireDecode(buffer, received, &packet); + if(errorIsNotOk(decodeRet)) { + errorCatch(errorPrint(decodeRet)); + continue; + } + + clientThreadEnqueue(self, &packet); + } +} + +void clientThreadEnqueue(localclient_t *self, const packet_t *packet) { + packetInboxPush(&self->inbox, packet); +} diff --git a/src/dusk/network/socket/client/clientthread.h b/src/dusk/network/socket/client/clientthread.h new file mode 100644 index 00000000..4781bd1d --- /dev/null +++ b/src/dusk/network/socket/client/clientthread.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "thread/thread.h" +#include "network/socket/packet.h" +#include "network/socket/client/localclient.h" + +/** + * Background reader thread body: blocks on the socket's receive with a + * short timeout, decoding and enqueueing each valid datagram into + * self's inbox. thread->data must be the localclient_t to read for. + * + * @param thread The thread this is running on. + */ +void clientThreadRun(thread_t *thread); + +/** + * Pushes a decoded packet into self's inbox for the main thread to + * drain. Silently drops the packet if the inbox is full. + * + * @param self The local client whose inbox to push into. + * @param packet The packet to enqueue. + */ +void clientThreadEnqueue(localclient_t *self, const packet_t *packet); diff --git a/src/dusk/network/socket/client/handler/CMakeLists.txt b/src/dusk/network/socket/client/handler/CMakeLists.txt new file mode 100644 index 00000000..7555c3f2 --- /dev/null +++ b/src/dusk/network/socket/client/handler/CMakeLists.txt @@ -0,0 +1,17 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + clientpackethandlerlist.c + clientpackethandlerhandshakeaccept.c + clientpackethandlerhandshakereject.c + clientpackethandlerdisconnect.c + clientpackethandlerack.c + clientpackethandlerping.c + clientpackethandlerplayerstate.c + clientpackethandlerplayerjoined.c + clientpackethandlerplayerleft.c +) diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerack.c b/src/dusk/network/socket/client/handler/clientpackethandlerack.c new file mode 100644 index 00000000..afb831f3 --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerack.c @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "clientpackethandlerack.h" +#include "network/socket/payload/packetack.h" + +errorret_t clientPacketHandlerAck( + localclient_t *self, + const packet_t *packet +) { + packetack_t payload; + errorChain(packetDecode(packet, &payload)); + + packetReliableChannelAck(&self->reliable, payload.sequence); + + errorOk(); +} diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerack.h b/src/dusk/network/socket/client/handler/clientpackethandlerack.h new file mode 100644 index 00000000..8d7c7166 --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerack.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "network/socket/client/handler/clientpackethandlerlist.h" + +/** + * Handles an ACK: marks the matching reliable-pending send as + * delivered. + * + * @param self The local client the packet arrived on. + * @param packet The received packet. + * @return An error if the payload failed to decode. + */ +errorret_t clientPacketHandlerAck( + localclient_t *self, + const packet_t *packet +); diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerdisconnect.c b/src/dusk/network/socket/client/handler/clientpackethandlerdisconnect.c new file mode 100644 index 00000000..d9f16e21 --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerdisconnect.c @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "clientpackethandlerdisconnect.h" +#include "network/socket/payload/packetdisconnect.h" + +errorret_t clientPacketHandlerDisconnect( + localclient_t *self, + const packet_t *packet +) { + packetdisconnect_t payload; + errorChain(packetDecode(packet, &payload)); + + errorret_t ret = errorThrowImpl( + &ERROR_STATE, ERROR_NOT_OK, __FILE__, __func__, __LINE__, + "Disconnected by server" + ); + if(self->onDisconnected) self->onDisconnected(ret, self->user); + + localClientTeardown(self); + + errorOk(); +} diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerdisconnect.h b/src/dusk/network/socket/client/handler/clientpackethandlerdisconnect.h new file mode 100644 index 00000000..63ad9d4f --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerdisconnect.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "network/socket/client/handler/clientpackethandlerlist.h" + +/** + * Handles a server-initiated DISCONNECT (kick): fires onDisconnected + * and tears down. + * + * @param self The local client the packet arrived on. + * @param packet The received packet. + * @return An error if the payload failed to decode. + */ +errorret_t clientPacketHandlerDisconnect( + localclient_t *self, + const packet_t *packet +); diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerhandshakeaccept.c b/src/dusk/network/socket/client/handler/clientpackethandlerhandshakeaccept.c new file mode 100644 index 00000000..91bda92b --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerhandshakeaccept.c @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "clientpackethandlerhandshakeaccept.h" +#include "network/socket/payload/packethandshakeaccept.h" + +errorret_t clientPacketHandlerHandshakeAccept( + localclient_t *self, + const packet_t *packet +) { + packethandshakeaccept_t payload; + errorChain(packetDecode(packet, &payload)); + + self->id = payload.id; + self->state = LOCAL_CLIENT_STATE_CONNECTED; + packetReliableChannelAckType(&self->reliable, PACKET_TYPE_HANDSHAKE); + + if(self->onConnected) self->onConnected(self->user); + + errorOk(); +} diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerhandshakeaccept.h b/src/dusk/network/socket/client/handler/clientpackethandlerhandshakeaccept.h new file mode 100644 index 00000000..805a5597 --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerhandshakeaccept.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "network/socket/client/handler/clientpackethandlerlist.h" + +/** + * Handles a HANDSHAKE_ACCEPT: records the assigned id, moves to + * CONNECTED, and fires onConnected. + * + * @param self The local client the packet arrived on. + * @param packet The received packet. + * @return An error if the payload failed to decode. + */ +errorret_t clientPacketHandlerHandshakeAccept( + localclient_t *self, + const packet_t *packet +); diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerhandshakereject.c b/src/dusk/network/socket/client/handler/clientpackethandlerhandshakereject.c new file mode 100644 index 00000000..3e3ea89a --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerhandshakereject.c @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "clientpackethandlerhandshakereject.h" +#include "network/socket/payload/packethandshakereject.h" + +errorret_t clientPacketHandlerHandshakeReject( + localclient_t *self, + const packet_t *packet +) { + packethandshakereject_t payload; + errorChain(packetDecode(packet, &payload)); + + packetReliableChannelAckType(&self->reliable, PACKET_TYPE_HANDSHAKE); + + if(self->onRejected) self->onRejected(payload.reason, self->user); + + localClientTeardown(self); + + errorOk(); +} diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerhandshakereject.h b/src/dusk/network/socket/client/handler/clientpackethandlerhandshakereject.h new file mode 100644 index 00000000..0ca7ad9f --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerhandshakereject.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "network/socket/client/handler/clientpackethandlerlist.h" + +/** + * Handles a HANDSHAKE_REJECT: fires onRejected and tears down. + * + * @param self The local client the packet arrived on. + * @param packet The received packet. + * @return An error if the payload failed to decode. + */ +errorret_t clientPacketHandlerHandshakeReject( + localclient_t *self, + const packet_t *packet +); diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerlist.c b/src/dusk/network/socket/client/handler/clientpackethandlerlist.c new file mode 100644 index 00000000..ae751be1 --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerlist.c @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "clientpackethandlerlist.h" +#include "clientpackethandlerhandshakeaccept.h" +#include "clientpackethandlerhandshakereject.h" +#include "clientpackethandlerdisconnect.h" +#include "clientpackethandlerack.h" +#include "clientpackethandlerping.h" +#include "clientpackethandlerplayerstate.h" +#include "clientpackethandlerplayerjoined.h" +#include "clientpackethandlerplayerleft.h" + +clientpackethandlercallback_t * +CLIENT_PACKET_HANDLERS[PACKET_TYPE_COUNT] = { + [PACKET_TYPE_HANDSHAKE_ACCEPT] = clientPacketHandlerHandshakeAccept, + [PACKET_TYPE_HANDSHAKE_REJECT] = clientPacketHandlerHandshakeReject, + [PACKET_TYPE_DISCONNECT] = clientPacketHandlerDisconnect, + [PACKET_TYPE_ACK] = clientPacketHandlerAck, + [PACKET_TYPE_PING] = clientPacketHandlerPing, + [PACKET_TYPE_PLAYER_STATE] = clientPacketHandlerPlayerState, + [PACKET_TYPE_PLAYER_JOINED] = clientPacketHandlerPlayerJoined, + [PACKET_TYPE_PLAYER_LEFT] = clientPacketHandlerPlayerLeft, +}; diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerlist.h b/src/dusk/network/socket/client/handler/clientpackethandlerlist.h new file mode 100644 index 00000000..5aee3acf --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerlist.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "network/socket/packet.h" +#include "network/socket/client/localclient.h" + +typedef errorret_t (clientpackethandlercallback_t)( + localclient_t *self, + const packet_t *packet +); + +extern clientpackethandlercallback_t * + CLIENT_PACKET_HANDLERS[PACKET_TYPE_COUNT]; diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerping.c b/src/dusk/network/socket/client/handler/clientpackethandlerping.c new file mode 100644 index 00000000..1cc1da65 --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerping.c @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "clientpackethandlerping.h" + +errorret_t clientPacketHandlerPing( + localclient_t *self, + const packet_t *packet +) { + (void)self; + (void)packet; + + errorOk(); +} diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerping.h b/src/dusk/network/socket/client/handler/clientpackethandlerping.h new file mode 100644 index 00000000..7d7745c1 --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerping.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "network/socket/client/handler/clientpackethandlerlist.h" + +/** + * Handles a PING: a no-op, since receiving any packet already refreshes + * self->lastReceivedAt in the drain loop before dispatch. + * + * @param self The local client the packet arrived on. + * @param packet The received packet. + * @return Always ok. + */ +errorret_t clientPacketHandlerPing( + localclient_t *self, + const packet_t *packet +); diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerplayerjoined.c b/src/dusk/network/socket/client/handler/clientpackethandlerplayerjoined.c new file mode 100644 index 00000000..0c3c5c7e --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerplayerjoined.c @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "clientpackethandlerplayerjoined.h" +#include "network/socket/payload/packetplayerjoined.h" +#include "network/socket/client/clientroster.h" +#include "util/string.h" + +errorret_t clientPacketHandlerPlayerJoined( + localclient_t *self, + const packet_t *packet +) { + (void)self; + + packetplayerjoined_t payload; + errorChain(packetDecode(packet, &payload)); + + client_t *slot = clientRosterFindById(&payload.id); + if(!slot) slot = clientRosterClaim(CLIENT_TYPE_REMOTE); + if(!slot) errorThrow("No free roster slots for joining player"); + + slot->remote.id = payload.id; + stringCopy( + slot->remote.username, payload.username, PACKET_HANDSHAKE_USERNAME_MAX - 1 + ); + + errorOk(); +} diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerplayerjoined.h b/src/dusk/network/socket/client/handler/clientpackethandlerplayerjoined.h new file mode 100644 index 00000000..03e7adb7 --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerplayerjoined.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "network/socket/client/handler/clientpackethandlerlist.h" + +/** + * Handles a PLAYER_JOINED broadcast: claims (or reuses) a roster slot + * for the newly joined remote player. + * + * @param self The local client the packet arrived on. + * @param packet The received packet. + * @return An error if the payload failed to decode or the roster is + * full. + */ +errorret_t clientPacketHandlerPlayerJoined( + localclient_t *self, + const packet_t *packet +); diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerplayerleft.c b/src/dusk/network/socket/client/handler/clientpackethandlerplayerleft.c new file mode 100644 index 00000000..aa0bd294 --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerplayerleft.c @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "clientpackethandlerplayerleft.h" +#include "network/socket/payload/packetplayerleft.h" +#include "network/socket/client/clientroster.h" + +errorret_t clientPacketHandlerPlayerLeft( + localclient_t *self, + const packet_t *packet +) { + (void)self; + + packetplayerleft_t payload; + errorChain(packetDecode(packet, &payload)); + + client_t *slot = clientRosterFindById(&payload.id); + if(slot) clientRosterRemove(slot); + + errorOk(); +} diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerplayerleft.h b/src/dusk/network/socket/client/handler/clientpackethandlerplayerleft.h new file mode 100644 index 00000000..1da3d240 --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerplayerleft.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "network/socket/client/handler/clientpackethandlerlist.h" + +/** + * Handles a PLAYER_LEFT broadcast: removes the matching roster slot, if + * any. + * + * @param self The local client the packet arrived on. + * @param packet The received packet. + * @return An error if the payload failed to decode. + */ +errorret_t clientPacketHandlerPlayerLeft( + localclient_t *self, + const packet_t *packet +); diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerplayerstate.c b/src/dusk/network/socket/client/handler/clientpackethandlerplayerstate.c new file mode 100644 index 00000000..a597b037 --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerplayerstate.c @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "clientpackethandlerplayerstate.h" +#include "network/socket/payload/packetplayerstate.h" +#include "network/socket/client/clientroster.h" +#include + +errorret_t clientPacketHandlerPlayerState( + localclient_t *self, + const packet_t *packet +) { + (void)self; + + packetplayerstate_t payload; + errorChain(packetDecode(packet, &payload)); + + client_t *slot = clientRosterFindById(&payload.id); + if(!slot) errorOk(); + + if(slot->remote.hasState && + packet->sequence <= slot->remote.lastStateSequence) { + errorOk(); + } + + slot->remote.hasState = true; + slot->remote.lastStateSequence = packet->sequence; + glm_vec3_copy(payload.position, slot->remote.lastPosition); + glm_vec3_copy(payload.rotation, slot->remote.lastRotation); + + errorOk(); +} diff --git a/src/dusk/network/socket/client/handler/clientpackethandlerplayerstate.h b/src/dusk/network/socket/client/handler/clientpackethandlerplayerstate.h new file mode 100644 index 00000000..36d5c866 --- /dev/null +++ b/src/dusk/network/socket/client/handler/clientpackethandlerplayerstate.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "network/socket/client/handler/clientpackethandlerlist.h" + +/** + * Handles a PLAYER_STATE broadcast: applies the reported position and + * rotation to the matching roster slot, if any, and if not stale + * (superseded by a sequence number already applied). + * + * @param self The local client the packet arrived on. + * @param packet The received packet. + * @return An error if the payload failed to decode. + */ +errorret_t clientPacketHandlerPlayerState( + localclient_t *self, + const packet_t *packet +); diff --git a/src/dusk/network/socket/client/localclient.c b/src/dusk/network/socket/client/localclient.c new file mode 100644 index 00000000..9a66a494 --- /dev/null +++ b/src/dusk/network/socket/client/localclient.c @@ -0,0 +1,263 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "localclient.h" +#include "clientthread.h" +#include "handler/clientpackethandlerlist.h" +#include "network/socket/packetregistry.h" +#include "network/socket/payload/packetping.h" +#include "network/socket/payload/packetdisconnect.h" +#include "network/socket/payload/packetack.h" +#include "network/socket/server/serveroffline.h" +#include "util/memory.h" +#include "util/string.h" +#include "assert/assert.h" +#include "time/time.h" + +errorret_t localClientResendCallback( + const packet_t *packet, + const networkaddr_t *destAddr, + void *user +) { + localclient_t *self = (localclient_t *)user; + (void)destAddr; + + if(self->offline) return serverOfflineReceiveFromClient(packet); + + uint8_t buffer[PACKET_WIRE_SIZE_MAX]; + size_t length = 0; + errorChain(packetWireEncode(packet, buffer, sizeof(buffer), &length)); + + size_t sent = 0; + return networkDgramSocketPlatformSend(&self->socket, buffer, length, &sent); +} + +errorret_t localClientConnect( + localclient_t *self, + const char_t *host, + const uint16_t port, + const char_t *username, + void (*onConnected)(void *user), + void (*onRejected)( + const packethandshakerejectreason_t reason, void *user + ), + void (*onDisconnected)(errorret_t error, void *user), + void *user +) { + assertNotNull(self, "self must not be NULL"); + assertNotNull(host, "host must not be NULL"); + assertNotNull(username, "username must not be NULL"); + assertNotNull(onConnected, "onConnected must not be NULL"); + assertNotNull(onRejected, "onRejected must not be NULL"); + assertNotNull(onDisconnected, "onDisconnected must not be NULL"); + + memoryZero(self, sizeof(localclient_t)); + + stringCopy(self->username, username, PACKET_HANDSHAKE_USERNAME_MAX - 1); + self->onConnected = onConnected; + self->onRejected = onRejected; + self->onDisconnected = onDisconnected; + self->user = user; + + packetReliableChannelInit(&self->reliable); + packetInboxInit(&self->inbox); + + errorChain(networkDgramSocketPlatformOpen(&self->socket)); + errorChain(networkDgramSocketPlatformConnect(&self->socket, host, port)); + + self->state = LOCAL_CLIENT_STATE_HANDSHAKING; + self->lastReceivedAt = TIME.time; + self->lastSentAt = TIME.time; + + threadInit(&self->readerThread, clientThreadRun); + self->readerThread.data = self; + threadStart(&self->readerThread); + + packethandshake_t handshake; + handshake.protocolVersion = NETWORK_PROTOCOL_VERSION; + stringCopy( + handshake.username, self->username, PACKET_HANDSHAKE_USERNAME_MAX - 1 + ); + + errorChain(localClientSend(self, PACKET_TYPE_HANDSHAKE, &handshake, true)); + + errorOk(); +} + +errorret_t localClientConnectOffline( + localclient_t *self, + const char_t *username, + void (*onConnected)(void *user), + void (*onRejected)( + const packethandshakerejectreason_t reason, void *user + ), + void (*onDisconnected)(errorret_t error, void *user), + void *user +) { + assertNotNull(self, "self must not be NULL"); + assertNotNull(username, "username must not be NULL"); + assertNotNull(onConnected, "onConnected must not be NULL"); + assertNotNull(onRejected, "onRejected must not be NULL"); + assertNotNull(onDisconnected, "onDisconnected must not be NULL"); + + memoryZero(self, sizeof(localclient_t)); + self->offline = true; + + stringCopy(self->username, username, PACKET_HANDSHAKE_USERNAME_MAX - 1); + self->onConnected = onConnected; + self->onRejected = onRejected; + self->onDisconnected = onDisconnected; + self->user = user; + + packetReliableChannelInit(&self->reliable); + packetInboxInit(&self->inbox); + + errorChain(serverOfflineRegisterClient(&self->inbox)); + + self->state = LOCAL_CLIENT_STATE_HANDSHAKING; + self->lastReceivedAt = TIME.time; + self->lastSentAt = TIME.time; + + packethandshake_t handshake; + handshake.protocolVersion = NETWORK_PROTOCOL_VERSION; + stringCopy( + handshake.username, self->username, PACKET_HANDSHAKE_USERNAME_MAX - 1 + ); + + errorChain(localClientSend(self, PACKET_TYPE_HANDSHAKE, &handshake, true)); + + errorOk(); +} + +errorret_t localClientUpdate(localclient_t *self) { + assertNotNull(self, "self must not be NULL"); + if(self->state == LOCAL_CLIENT_STATE_DISCONNECTED) errorOk(); + + packet_t drained[PACKET_INBOX_SIZE]; + const uint32_t drainedCount = + packetInboxDrain(&self->inbox, drained, PACKET_INBOX_SIZE); + + for(uint32_t i = 0; i < drainedCount; i++) { + self->lastReceivedAt = TIME.time; + + const packettype_t type = drained[i].type; + + if(PACKET_TYPE_INFO[type].reliable) { + packetack_t ack = { .sequence = drained[i].sequence }; + errorret_t ackRet = localClientSend(self, PACKET_TYPE_ACK, &ack, false); + if(errorIsNotOk(ackRet)) errorCatch(errorPrint(ackRet)); + } + + if(CLIENT_PACKET_HANDLERS[type]) { + errorret_t ret = CLIENT_PACKET_HANDLERS[type](self, &drained[i]); + if(errorIsNotOk(ret)) errorCatch(errorPrint(ret)); + } + + if(self->state == LOCAL_CLIENT_STATE_DISCONNECTED) errorOk(); + } + + bool_t expired = false; + packetReliableChannelTick( + &self->reliable, localClientResendCallback, self, &expired + ); + + if(expired) { + errorret_t ret = errorThrowImpl( + &ERROR_STATE, ERROR_NOT_OK, __FILE__, __func__, __LINE__, + "Connection timed out (reliable retries exhausted)" + ); + if(self->onDisconnected) self->onDisconnected(ret, self->user); + localClientTeardown(self); + errorOk(); + } + + if(TIME.time - self->lastReceivedAt > LOCAL_CLIENT_TIMEOUT_SECONDS) { + errorret_t ret = errorThrowImpl( + &ERROR_STATE, ERROR_NOT_OK, __FILE__, __func__, __LINE__, + "Connection timed out (no packets received)" + ); + if(self->onDisconnected) self->onDisconnected(ret, self->user); + localClientTeardown(self); + errorOk(); + } + + if( + self->state == LOCAL_CLIENT_STATE_CONNECTED && + TIME.time - self->lastSentAt > LOCAL_CLIENT_PING_INTERVAL_SECONDS + ) { + packetping_t ping = { .unused = 0 }; + errorChain(localClientSend(self, PACKET_TYPE_PING, &ping, false)); + } + + errorOk(); +} + +errorret_t localClientSend( + localclient_t *self, + const packettype_t type, + const void *payload, + const bool_t reliable +) { + assertNotNull(self, "self must not be NULL"); + + const uint32_t sequence = + packetReliableChannelNextSequence(&self->reliable); + + packet_t packet; + errorChain(packetEncode(&packet, type, sequence, payload)); + + if(self->offline) { + errorChain(serverOfflineReceiveFromClient(&packet)); + } else { + uint8_t buffer[PACKET_WIRE_SIZE_MAX]; + size_t length = 0; + errorChain(packetWireEncode(&packet, buffer, sizeof(buffer), &length)); + + size_t sent = 0; + errorChain( + networkDgramSocketPlatformSend(&self->socket, buffer, length, &sent) + ); + } + + self->lastSentAt = TIME.time; + + if(reliable) { + errorChain(packetReliableChannelTrack(&self->reliable, &packet, NULL)); + } + + errorOk(); +} + +void localClientDisconnect(localclient_t *self) { + assertNotNull(self, "self must not be NULL"); + if(self->state != LOCAL_CLIENT_STATE_CONNECTED) return; + + packetdisconnect_t disconnect = { .id = self->id }; + errorret_t ret = localClientSend( + self, PACKET_TYPE_DISCONNECT, &disconnect, false + ); + if(errorIsNotOk(ret)) errorCatch(errorPrint(ret)); + + localClientTeardown(self); +} + +void localClientTeardown(localclient_t *self) { + assertNotNull(self, "self must not be NULL"); + if(self->state == LOCAL_CLIENT_STATE_DISCONNECTED) return; + + if(self->offline) { + serverOfflineUnregisterClient(&self->inbox); + } else { + threadStop(&self->readerThread); + networkDgramSocketPlatformClose(&self->socket); + } + + packetReliableChannelDispose(&self->reliable); + packetInboxDispose(&self->inbox); + + self->state = LOCAL_CLIENT_STATE_DISCONNECTED; +} diff --git a/src/dusk/network/socket/client/localclient.h b/src/dusk/network/socket/client/localclient.h new file mode 100644 index 00000000..0d655a76 --- /dev/null +++ b/src/dusk/network/socket/client/localclient.h @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "thread/thread.h" +#include "util/uuid.h" +#include "network/networkdgramsocketplatform.h" +#include "network/socket/packet.h" +#include "network/socket/packetinbox.h" +#include "network/socket/packetreliable.h" +#include "network/socket/payload/packethandshake.h" +#include "network/socket/payload/packethandshakereject.h" + +#define LOCAL_CLIENT_TIMEOUT_SECONDS 10.0f +#define LOCAL_CLIENT_PING_INTERVAL_SECONDS 2.0f + +typedef enum { + LOCAL_CLIENT_STATE_DISCONNECTED, + LOCAL_CLIENT_STATE_HANDSHAKING, + LOCAL_CLIENT_STATE_CONNECTED, + LOCAL_CLIENT_STATE_DISCONNECTING, +} localclientstate_t; + +typedef struct { + bool_t offline; + networkdgramsocketplatform_t socket; + localclientstate_t state; + uuid_t id; + char_t username[PACKET_HANDSHAKE_USERNAME_MAX]; + packetreliablechannel_t reliable; + float_t lastReceivedAt; + float_t lastSentAt; + + thread_t readerThread; + packetinbox_t inbox; + + void (*onConnected)(void *user); + void (*onRejected)( + const packethandshakerejectreason_t reason, void *user + ); + void (*onDisconnected)(errorret_t error, void *user); + void *user; +} localclient_t; + +/** + * packetReliableChannelTick resend callback: re-sends packet to the + * server via self's connected socket. destAddr is unused (the client's + * socket already has a fixed peer). + * + * @param packet The packet to resend. + * @param destAddr Unused. + * @param user The localclient_t to resend on. + * @return An error if the send failed. + */ +errorret_t localClientResendCallback( + const packet_t *packet, + const networkaddr_t *destAddr, + void *user +); + +/** + * Connects to a server: opens the UDP socket, starts the background + * reader thread, and sends a handshake. onConnected/onRejected fire + * once the server responds; onDisconnected fires later if the + * connection is lost or the server disconnects us. + * + * @param self The local client slot to connect. + * @param host The server's hostname or IP address. + * @param port The server's port. + * @param username This client's username, truncated to + * PACKET_HANDSHAKE_USERNAME_MAX - 1 characters. + * @param onConnected Called once the server accepts the handshake. + * @param onRejected Called if the server rejects the handshake. + * @param onDisconnected Called if the connection is lost or dropped + * after having connected. + * @param user Passed through to all three callbacks. + * @return An error if the socket could not be opened/connected. + */ +errorret_t localClientConnect( + localclient_t *self, + const char_t *host, + const uint16_t port, + const char_t *username, + void (*onConnected)(void *user), + void (*onRejected)( + const packethandshakerejectreason_t reason, void *user + ), + void (*onDisconnected)(errorret_t error, void *user), + void *user +); + +/** + * Connects to the offline (in-process, non-networked) server: no + * socket, no reader thread. Registers self's inbox with SERVER_OFFLINE + * and sends a handshake through the same queued, drained-on-update path + * as an online connection, so the two behave identically from here on. + * + * @param self The local client slot to connect. + * @param username This client's username, truncated to + * PACKET_HANDSHAKE_USERNAME_MAX - 1 characters. + * @param onConnected Called once the offline server accepts. + * @param onRejected Called if the offline server rejects (in practice + * this can't currently happen -- kept for API parity). + * @param onDisconnected Called if the offline session ends unexpectedly. + * @param user Passed through to all three callbacks. + * @return An error if the offline server isn't running + * (serverOfflineHost must be called first). + */ +errorret_t localClientConnectOffline( + localclient_t *self, + const char_t *username, + void (*onConnected)(void *user), + void (*onRejected)( + const packethandshakerejectreason_t reason, void *user + ), + void (*onDisconnected)(errorret_t error, void *user), + void *user +); + +/** + * Drains received packets, ticks the reliability channel, and checks + * for a connection timeout. Called once per frame. + * + * @param self The local client to update. + * @return An error if a fatal error occurred while processing. + */ +errorret_t localClientUpdate(localclient_t *self); + +/** + * Sends a packet to the server. + * + * @param self The local client sending the packet. + * @param type The packet type to send. + * @param payload The payload struct matching type's registered size. + * @param reliable If true, tracks the packet for ack + retry. + * @return An error if the payload doesn't match type's registered size, + * or the send failed. + */ +errorret_t localClientSend( + localclient_t *self, + const packettype_t type, + const void *payload, + const bool_t reliable +); + +/** + * Disconnects from the server, if connected: sends a best-effort + * disconnect notice, then immediately tears down. A dropped notice is + * not retried -- the server's own receive-timeout will eventually + * clean up the slot regardless, so this is a courtesy, not a + * requirement for correctness. + * + * @param self The local client to disconnect. + */ +void localClientDisconnect(localclient_t *self); + +/** + * Stops the reader thread, closes the socket, and disposes of the + * reliability channel and inbox mutex. Leaves self in + * LOCAL_CLIENT_STATE_DISCONNECTED. + * + * @param self The local client to tear down. + */ +void localClientTeardown(localclient_t *self); diff --git a/src/dusk/network/socket/client/remoteclient.h b/src/dusk/network/socket/client/remoteclient.h new file mode 100644 index 00000000..9fc25cd7 --- /dev/null +++ b/src/dusk/network/socket/client/remoteclient.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "util/uuid.h" +#include "network/socket/payload/packethandshake.h" +#include + +typedef struct { + uuid_t id; + char_t username[PACKET_HANDSHAKE_USERNAME_MAX]; + vec3 lastPosition; + vec3 lastRotation; + bool_t hasState; + uint32_t lastStateSequence; +} remoteclient_t; diff --git a/src/dusk/network/socket/packet.c b/src/dusk/network/socket/packet.c new file mode 100644 index 00000000..fd538f65 --- /dev/null +++ b/src/dusk/network/socket/packet.c @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "packet.h" +#include "network/socket/packetregistry.h" +#include "util/memory.h" +#include "util/endian.h" +#include "assert/assert.h" + +errorret_t packetEncode( + packet_t *out, + const packettype_t type, + const uint32_t sequence, + const void *payload +) { + assertNotNull(out, "out must not be NULL"); + assertTrue(type < PACKET_TYPE_COUNT, "type out of range"); + + const size_t size = PACKET_TYPE_INFO[type].size; + if(size > PACKET_DATA_SIZE_MAX) { + errorThrow("Packet type %d payload size %zu exceeds max %d", + type, size, PACKET_DATA_SIZE_MAX); + } + + out->type = type; + out->sequence = sequence; + out->length = (uint32_t)size; + + if(size > 0) memoryCopy(out->data, payload, size); + + errorOk(); +} + +errorret_t packetDecode(const packet_t *in, void *outPayload) { + assertNotNull(in, "in must not be NULL"); + assertNotNull(outPayload, "outPayload must not be NULL"); + assertTrue(in->type < PACKET_TYPE_COUNT, "in->type out of range"); + + const size_t expectedSize = PACKET_TYPE_INFO[in->type].size; + if(in->length != expectedSize) { + errorThrow( + "Packet type %d has length %u, expected %zu", + in->type, in->length, expectedSize + ); + } + + if(expectedSize > 0) memoryCopy(outPayload, in->data, expectedSize); + + errorOk(); +} + +errorret_t packetWireEncode( + const packet_t *packet, + uint8_t *outBuffer, + const size_t bufferSize, + size_t *outLength +) { + assertNotNull(packet, "packet must not be NULL"); + assertNotNull(outBuffer, "outBuffer must not be NULL"); + assertNotNull(outLength, "outLength must not be NULL"); + + const size_t totalLength = PACKET_WIRE_HEADER_SIZE + packet->length; + if(totalLength > bufferSize) { + errorThrow("Packet wire size %zu exceeds buffer size %zu", + totalLength, bufferSize); + } + + const uint32_t type = endianLittleToHost32((uint32_t)packet->type); + const uint32_t sequence = endianLittleToHost32(packet->sequence); + const uint32_t length = endianLittleToHost32(packet->length); + + memoryCopy(outBuffer, &type, sizeof(uint32_t)); + memoryCopy(outBuffer + sizeof(uint32_t), &sequence, sizeof(uint32_t)); + memoryCopy(outBuffer + sizeof(uint32_t) * 2, &length, sizeof(uint32_t)); + + if(packet->length > 0) { + memoryCopy( + outBuffer + PACKET_WIRE_HEADER_SIZE, packet->data, packet->length + ); + } + + *outLength = totalLength; + errorOk(); +} + +errorret_t packetWireDecode( + const uint8_t *buffer, + const size_t bufferLength, + packet_t *outPacket +) { + assertNotNull(buffer, "buffer must not be NULL"); + assertNotNull(outPacket, "outPacket must not be NULL"); + + if(bufferLength < PACKET_WIRE_HEADER_SIZE) { + errorThrow("Packet too small for header: %zu bytes", bufferLength); + } + + uint32_t type, sequence, length; + memoryCopy(&type, buffer, sizeof(uint32_t)); + memoryCopy(&sequence, buffer + sizeof(uint32_t), sizeof(uint32_t)); + memoryCopy(&length, buffer + sizeof(uint32_t) * 2, sizeof(uint32_t)); + + type = endianLittleToHost32(type); + sequence = endianLittleToHost32(sequence); + length = endianLittleToHost32(length); + + if(type >= PACKET_TYPE_COUNT) { + errorThrow("Packet has unknown type %u", type); + } + + if(length > PACKET_DATA_SIZE_MAX) { + errorThrow("Packet length %u exceeds max %d", length, + PACKET_DATA_SIZE_MAX); + } + + if(bufferLength < PACKET_WIRE_HEADER_SIZE + length) { + errorThrow("Packet buffer too small: expected %zu, got %zu", + PACKET_WIRE_HEADER_SIZE + length, bufferLength); + } + + if(length != PACKET_TYPE_INFO[type].size) { + errorThrow("Packet type %u length %u does not match expected %zu", + type, length, PACKET_TYPE_INFO[type].size); + } + + outPacket->type = (packettype_t)type; + outPacket->sequence = sequence; + outPacket->length = length; + + if(length > 0) { + memoryCopy(outPacket->data, buffer + PACKET_WIRE_HEADER_SIZE, length); + } + + errorOk(); +} diff --git a/src/dusk/network/socket/packet.h b/src/dusk/network/socket/packet.h new file mode 100644 index 00000000..158d3a42 --- /dev/null +++ b/src/dusk/network/socket/packet.h @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" +#include "error/error.h" +#include "network/socket/packettype.h" + +#define PACKET_DATA_SIZE_MAX 512 + +typedef struct { + packettype_t type; + uint32_t sequence; + uint32_t length; + uint8_t data[PACKET_DATA_SIZE_MAX]; +} packet_t; + +/** + * Encodes a payload struct into a packet envelope, validating its size + * against the registered size for type. + * + * @param out The packet to fill in. + * @param type The packet type being encoded. + * @param sequence The sequence number to stamp the packet with. + * @param payload The payload struct matching type's registered size. + * @return An error if payload's size doesn't match the registered size + * for type. + */ +errorret_t packetEncode( + packet_t *out, + const packettype_t type, + const uint32_t sequence, + const void *payload +); + +/** + * Decodes a packet's payload into a typed struct, validating the + * packet's length against the registered size for its type. + * + * @param in The packet to decode. + * @param outPayload Destination buffer, must be at least the registered + * size for in->type. + * @return An error if in->length doesn't match the registered size for + * in->type. + */ +errorret_t packetDecode(const packet_t *in, void *outPayload); + +#define PACKET_WIRE_HEADER_SIZE (sizeof(uint32_t) * 3) +#define PACKET_WIRE_SIZE_MAX (PACKET_WIRE_HEADER_SIZE + PACKET_DATA_SIZE_MAX) + +/** + * Serializes a packet's header + used portion of its payload into a + * wire-format buffer (fixed-width, little-endian header, followed by + * exactly packet->length bytes of payload) -- never the full fixed-size + * packet_t, which would waste bandwidth on the unused tail of data. + * + * @param packet The packet to serialize. + * @param outBuffer The destination buffer. + * @param bufferSize The size of outBuffer. + * @param outLength The number of bytes written is written here. + * @return An error if the serialized size exceeds bufferSize. + */ +errorret_t packetWireEncode( + const packet_t *packet, + uint8_t *outBuffer, + const size_t bufferSize, + size_t *outLength +); + +/** + * Deserializes a wire-format buffer back into a packet, defensively + * validating the type, length, and buffer size before trusting any of + * it -- the buffer may come from an unauthenticated peer. + * + * @param buffer The received bytes. + * @param bufferLength The number of bytes in buffer. + * @param outPacket The packet to fill in. + * @return An error if the buffer is malformed (bad type, length + * mismatch for its type, or too small for its declared length). + */ +errorret_t packetWireDecode( + const uint8_t *buffer, + const size_t bufferLength, + packet_t *outPacket +); diff --git a/src/dusk/network/socket/packetdata.h b/src/dusk/network/socket/packetdata.h new file mode 100644 index 00000000..c9fe36db --- /dev/null +++ b/src/dusk/network/socket/packetdata.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "network/socket/payload/packethandshake.h" +#include "network/socket/payload/packethandshakeaccept.h" +#include "network/socket/payload/packethandshakereject.h" +#include "network/socket/payload/packetdisconnect.h" +#include "network/socket/payload/packetack.h" +#include "network/socket/payload/packetping.h" +#include "network/socket/payload/packetplayerstate.h" +#include "network/socket/payload/packetplayerjoined.h" +#include "network/socket/payload/packetplayerleft.h" + +typedef union { + packethandshake_t handshake; + packethandshakeaccept_t handshakeAccept; + packethandshakereject_t handshakeReject; + packetdisconnect_t disconnect; + packetack_t ack; + packetping_t ping; + packetplayerstate_t playerState; + packetplayerjoined_t playerJoined; + packetplayerleft_t playerLeft; +} packetdata_t; diff --git a/src/dusk/network/socket/packetinbox.c b/src/dusk/network/socket/packetinbox.c new file mode 100644 index 00000000..42f4a2b4 --- /dev/null +++ b/src/dusk/network/socket/packetinbox.c @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "packetinbox.h" +#include "util/memory.h" + +void packetInboxInit(packetinbox_t *inbox) { + memoryZero(inbox, sizeof(packetinbox_t)); + threadMutexInit(&inbox->mutex); +} + +void packetInboxPush(packetinbox_t *inbox, const packet_t *packet) { + threadMutexLock(&inbox->mutex); + + for(uint32_t i = 0; i < PACKET_INBOX_SIZE; i++) { + if(!inbox->filled[i]) { + inbox->packets[i] = *packet; + inbox->filled[i] = true; + break; + } + } + + threadMutexUnlock(&inbox->mutex); +} + +uint32_t packetInboxDrain( + packetinbox_t *inbox, + packet_t *outPackets, + const uint32_t maxOut +) { + uint32_t count = 0; + + threadMutexLock(&inbox->mutex); + + for(uint32_t i = 0; i < PACKET_INBOX_SIZE && count < maxOut; i++) { + if(inbox->filled[i]) { + outPackets[count++] = inbox->packets[i]; + inbox->filled[i] = false; + } + } + + threadMutexUnlock(&inbox->mutex); + + return count; +} + +void packetInboxDispose(packetinbox_t *inbox) { + threadMutexDispose(&inbox->mutex); +} diff --git a/src/dusk/network/socket/packetinbox.h b/src/dusk/network/socket/packetinbox.h new file mode 100644 index 00000000..795f26e2 --- /dev/null +++ b/src/dusk/network/socket/packetinbox.h @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "thread/threadmutex.h" +#include "network/socket/packet.h" + +#define PACKET_INBOX_SIZE 16 + +typedef struct { + threadmutex_t mutex; + bool_t filled[PACKET_INBOX_SIZE]; + packet_t packets[PACKET_INBOX_SIZE]; +} packetinbox_t; + +/** + * Initializes an inbox's mutex and clears its slots. + * + * @param inbox The inbox to initialize. + */ +void packetInboxInit(packetinbox_t *inbox); + +/** + * Pushes a packet into the first free slot. Silently drops the packet + * if the inbox is full -- the caller (a reader thread, or a direct + * in-process delivery) has no useful way to report the failure back to + * the sender, and dropping is the same behavior an overwhelmed UDP + * receive buffer would produce anyway. + * + * @param inbox The inbox to push into. + * @param packet The packet to enqueue. + */ +void packetInboxPush(packetinbox_t *inbox, const packet_t *packet); + +/** + * Drains every filled slot into outPackets and frees them. + * + * @param inbox The inbox to drain. + * @param outPackets Destination array, must be at least PACKET_INBOX_SIZE. + * @param maxOut The capacity of outPackets. + * @return The number of packets written to outPackets. + */ +uint32_t packetInboxDrain( + packetinbox_t *inbox, + packet_t *outPackets, + const uint32_t maxOut +); + +/** + * Disposes of an inbox's mutex. + * + * @param inbox The inbox to dispose. + */ +void packetInboxDispose(packetinbox_t *inbox); diff --git a/src/dusk/network/socket/packetregistry.c b/src/dusk/network/socket/packetregistry.c new file mode 100644 index 00000000..73f92c2e --- /dev/null +++ b/src/dusk/network/socket/packetregistry.c @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "packetregistry.h" +#include "network/socket/payload/packethandshake.h" +#include "network/socket/payload/packethandshakeaccept.h" +#include "network/socket/payload/packethandshakereject.h" +#include "network/socket/payload/packetdisconnect.h" +#include "network/socket/payload/packetack.h" +#include "network/socket/payload/packetping.h" +#include "network/socket/payload/packetplayerstate.h" +#include "network/socket/payload/packetplayerjoined.h" +#include "network/socket/payload/packetplayerleft.h" + +packettypeinfo_t PACKET_TYPE_INFO[PACKET_TYPE_COUNT] = { + [PACKET_TYPE_NULL] = { .size = 0, .reliable = false }, + [PACKET_TYPE_HANDSHAKE] = { + .size = sizeof(packethandshake_t), .reliable = true + }, + [PACKET_TYPE_HANDSHAKE_ACCEPT] = { + .size = sizeof(packethandshakeaccept_t), .reliable = true + }, + [PACKET_TYPE_HANDSHAKE_REJECT] = { + .size = sizeof(packethandshakereject_t), .reliable = true + }, + [PACKET_TYPE_DISCONNECT] = { + .size = sizeof(packetdisconnect_t), .reliable = true + }, + [PACKET_TYPE_ACK] = { .size = sizeof(packetack_t), .reliable = false }, + [PACKET_TYPE_PING] = { .size = sizeof(packetping_t), .reliable = false }, + [PACKET_TYPE_PLAYER_STATE] = { + .size = sizeof(packetplayerstate_t), .reliable = false + }, + [PACKET_TYPE_PLAYER_JOINED] = { + .size = sizeof(packetplayerjoined_t), .reliable = true + }, + [PACKET_TYPE_PLAYER_LEFT] = { + .size = sizeof(packetplayerleft_t), .reliable = true + }, +}; diff --git a/src/dusk/network/socket/packetregistry.h b/src/dusk/network/socket/packetregistry.h new file mode 100644 index 00000000..da86f7ee --- /dev/null +++ b/src/dusk/network/socket/packetregistry.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" +#include "network/socket/packettype.h" + +typedef struct { + size_t size; + bool_t reliable; +} packettypeinfo_t; + +extern packettypeinfo_t PACKET_TYPE_INFO[PACKET_TYPE_COUNT]; diff --git a/src/dusk/network/socket/packetreliable.c b/src/dusk/network/socket/packetreliable.c new file mode 100644 index 00000000..5021f7e4 --- /dev/null +++ b/src/dusk/network/socket/packetreliable.c @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "packetreliable.h" +#include "util/memory.h" +#include "time/time.h" + +void packetReliableChannelInit(packetreliablechannel_t *channel) { + memoryZero(channel, sizeof(packetreliablechannel_t)); + threadMutexInit(&channel->mutex); +} + +uint32_t packetReliableChannelNextSequence(packetreliablechannel_t *channel) { + threadMutexLock(&channel->mutex); + const uint32_t sequence = channel->nextSequence++; + threadMutexUnlock(&channel->mutex); + + return sequence; +} + +errorret_t packetReliableChannelTrack( + packetreliablechannel_t *channel, + const packet_t *packet, + const networkaddr_t *destAddr +) { + threadMutexLock(&channel->mutex); + + packetreliablepending_t *slot = NULL; + for(uint32_t i = 0; i < PACKET_RELIABLE_PENDING_MAX; i++) { + if(!channel->pending[i].inUse) { + slot = &channel->pending[i]; + break; + } + } + + if(!slot) { + threadMutexUnlock(&channel->mutex); + errorThrow("No free reliable-pending slots"); + } + + slot->inUse = true; + slot->packet = *packet; + if(destAddr) slot->destAddr = *destAddr; + slot->retryCount = 0; + slot->nextRetryAt = TIME.time + PACKET_RELIABLE_RETRY_INTERVAL_SECONDS; + + threadMutexUnlock(&channel->mutex); + errorOk(); +} + +void packetReliableChannelAck( + packetreliablechannel_t *channel, + const uint32_t sequence +) { + threadMutexLock(&channel->mutex); + + for(uint32_t i = 0; i < PACKET_RELIABLE_PENDING_MAX; i++) { + packetreliablepending_t *slot = &channel->pending[i]; + if(slot->inUse && slot->packet.sequence == sequence) { + slot->inUse = false; + break; + } + } + + threadMutexUnlock(&channel->mutex); +} + +void packetReliableChannelAckType( + packetreliablechannel_t *channel, + const packettype_t type +) { + threadMutexLock(&channel->mutex); + + for(uint32_t i = 0; i < PACKET_RELIABLE_PENDING_MAX; i++) { + packetreliablepending_t *slot = &channel->pending[i]; + if(slot->inUse && slot->packet.type == type) slot->inUse = false; + } + + threadMutexUnlock(&channel->mutex); +} + +void packetReliableChannelTick( + packetreliablechannel_t *channel, + packetreliableresendcallback_t *resend, + void *user, + bool_t *outExpired +) { + *outExpired = false; + + threadMutexLock(&channel->mutex); + + for(uint32_t i = 0; i < PACKET_RELIABLE_PENDING_MAX; i++) { + packetreliablepending_t *slot = &channel->pending[i]; + if(!slot->inUse || slot->nextRetryAt > TIME.time) continue; + + if(slot->retryCount >= PACKET_RELIABLE_RETRY_COUNT_MAX) { + slot->inUse = false; + *outExpired = true; + continue; + } + + slot->retryCount++; + slot->nextRetryAt = TIME.time + PACKET_RELIABLE_RETRY_INTERVAL_SECONDS; + + errorret_t ret = resend(&slot->packet, &slot->destAddr, user); + if(errorIsNotOk(ret)) errorCatch(errorPrint(ret)); + } + + threadMutexUnlock(&channel->mutex); +} + +void packetReliableChannelDispose(packetreliablechannel_t *channel) { + threadMutexDispose(&channel->mutex); +} diff --git a/src/dusk/network/socket/packetreliable.h b/src/dusk/network/socket/packetreliable.h new file mode 100644 index 00000000..d8fefea8 --- /dev/null +++ b/src/dusk/network/socket/packetreliable.h @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "thread/threadmutex.h" +#include "network/networkaddr.h" +#include "network/socket/packet.h" + +#define PACKET_RELIABLE_PENDING_MAX 8 +#define PACKET_RELIABLE_RETRY_INTERVAL_SECONDS 0.25f +#define PACKET_RELIABLE_RETRY_COUNT_MAX 8 + +typedef struct { + bool_t inUse; + packet_t packet; + networkaddr_t destAddr; + uint32_t retryCount; + float_t nextRetryAt; +} packetreliablepending_t; + +typedef struct { + packetreliablepending_t pending[PACKET_RELIABLE_PENDING_MAX]; + threadmutex_t mutex; + uint32_t nextSequence; +} packetreliablechannel_t; + +typedef errorret_t (packetreliableresendcallback_t)( + const packet_t *packet, + const networkaddr_t *destAddr, + void *user +); + +/** + * Initializes a reliability channel's mutex and pending-slot pool. + * + * @param channel The channel to initialize. + */ +void packetReliableChannelInit(packetreliablechannel_t *channel); + +/** + * Allocates the next sequence number for this channel, used for both + * reliable and unreliable sends so the peer can order/dedupe by it. + * + * @param channel The channel to draw a sequence number from. + * @return The next sequence number. + */ +uint32_t packetReliableChannelNextSequence(packetreliablechannel_t *channel); + +/** + * Tracks a reliable packet as awaiting acknowledgement, to be resent by + * packetReliableChannelTick until acked or its retries are exhausted. + * + * @param channel The channel to track the packet on. + * @param packet The already-sequenced packet to track. + * @param destAddr The address to resend to (server usage); ignored by + * callers using a connected socket (client usage). + * @return An error if the pending pool is full. + */ +errorret_t packetReliableChannelTrack( + packetreliablechannel_t *channel, + const packet_t *packet, + const networkaddr_t *destAddr +); + +/** + * Marks a tracked packet as acknowledged, removing it from the pending + * pool. A no-op if no pending packet matches sequence (already acked, + * or a duplicate/unexpected ack). + * + * @param channel The channel the ack was received on. + * @param sequence The sequence number being acknowledged. + */ +void packetReliableChannelAck( + packetreliablechannel_t *channel, + const uint32_t sequence +); + +/** + * Marks every tracked packet of a given type as acknowledged. Used for + * responses that confirm a request was handled without echoing its + * exact sequence number back (e.g. HANDSHAKE_ACCEPT/REJECT implicitly + * satisfy the original HANDSHAKE send). + * + * @param channel The channel the response was received on. + * @param type The packet type to clear from the pending pool. + */ +void packetReliableChannelAckType( + packetreliablechannel_t *channel, + const packettype_t type +); + +/** + * Resends any pending packets whose retry interval has elapsed. + * + * @param channel The channel to tick. + * @param resend Called for each packet due for resend. + * @param user Passed through to resend. + * @param outExpired Set to true if any pending packet exceeded + * PACKET_RELIABLE_RETRY_COUNT_MAX -- the caller should treat the + * connection as dead. The expired entry is removed either way. + */ +void packetReliableChannelTick( + packetreliablechannel_t *channel, + packetreliableresendcallback_t *resend, + void *user, + bool_t *outExpired +); + +/** + * Disposes of a reliability channel's mutex. + * + * @param channel The channel to dispose. + */ +void packetReliableChannelDispose(packetreliablechannel_t *channel); diff --git a/src/dusk/network/socket/packettype.h b/src/dusk/network/socket/packettype.h new file mode 100644 index 00000000..1c3f466b --- /dev/null +++ b/src/dusk/network/socket/packettype.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once + +typedef enum { + PACKET_TYPE_NULL, + + PACKET_TYPE_HANDSHAKE, + PACKET_TYPE_HANDSHAKE_ACCEPT, + PACKET_TYPE_HANDSHAKE_REJECT, + PACKET_TYPE_DISCONNECT, + PACKET_TYPE_ACK, + PACKET_TYPE_PING, + PACKET_TYPE_PLAYER_STATE, + PACKET_TYPE_PLAYER_JOINED, + PACKET_TYPE_PLAYER_LEFT, + + PACKET_TYPE_COUNT +} packettype_t; diff --git a/src/dusk/network/socket/payload/packetack.h b/src/dusk/network/socket/payload/packetack.h new file mode 100644 index 00000000..cafbd58c --- /dev/null +++ b/src/dusk/network/socket/payload/packetack.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef struct { + uint32_t sequence; +} packetack_t; diff --git a/src/dusk/network/socket/payload/packetdisconnect.h b/src/dusk/network/socket/payload/packetdisconnect.h new file mode 100644 index 00000000..c7884dab --- /dev/null +++ b/src/dusk/network/socket/payload/packetdisconnect.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "util/uuid.h" + +typedef struct { + uuid_t id; +} packetdisconnect_t; diff --git a/src/dusk/network/socket/payload/packethandshake.h b/src/dusk/network/socket/payload/packethandshake.h new file mode 100644 index 00000000..8134d9a1 --- /dev/null +++ b/src/dusk/network/socket/payload/packethandshake.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +#define PACKET_HANDSHAKE_USERNAME_MAX 10 + +/** + * The multiplayer wire protocol version. Bump whenever a packet type's + * layout or semantics change in a way that breaks compatibility. + * Distinct from DUSK_VERSION, which is a build timestamp, not suitable + * for a compatibility check. + */ +#define NETWORK_PROTOCOL_VERSION 1 + +typedef struct { + uint32_t protocolVersion; + char_t username[PACKET_HANDSHAKE_USERNAME_MAX]; +} packethandshake_t; diff --git a/src/dusk/network/socket/payload/packethandshakeaccept.h b/src/dusk/network/socket/payload/packethandshakeaccept.h new file mode 100644 index 00000000..0bc9d2f3 --- /dev/null +++ b/src/dusk/network/socket/payload/packethandshakeaccept.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "util/uuid.h" + +typedef struct { + uuid_t id; +} packethandshakeaccept_t; diff --git a/src/dusk/network/socket/payload/packethandshakereject.h b/src/dusk/network/socket/payload/packethandshakereject.h new file mode 100644 index 00000000..0420d54f --- /dev/null +++ b/src/dusk/network/socket/payload/packethandshakereject.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef enum { + PACKET_HANDSHAKE_REJECT_REASON_VERSION_MISMATCH, + PACKET_HANDSHAKE_REJECT_REASON_SERVER_FULL, + PACKET_HANDSHAKE_REJECT_REASON_USERNAME_INVALID, + PACKET_HANDSHAKE_REJECT_REASON_USERNAME_TAKEN, +} packethandshakerejectreason_t; + +typedef struct { + packethandshakerejectreason_t reason; +} packethandshakereject_t; diff --git a/src/dusk/network/socket/payload/packetping.h b/src/dusk/network/socket/payload/packetping.h new file mode 100644 index 00000000..2f5a71da --- /dev/null +++ b/src/dusk/network/socket/payload/packetping.h @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once + +typedef struct { + uint8_t unused; +} packetping_t; diff --git a/src/dusk/network/socket/payload/packetplayerjoined.h b/src/dusk/network/socket/payload/packetplayerjoined.h new file mode 100644 index 00000000..e6475d31 --- /dev/null +++ b/src/dusk/network/socket/payload/packetplayerjoined.h @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "util/uuid.h" +#include "network/socket/payload/packethandshake.h" + +typedef struct { + uuid_t id; + char_t username[PACKET_HANDSHAKE_USERNAME_MAX]; +} packetplayerjoined_t; diff --git a/src/dusk/network/socket/payload/packetplayerleft.h b/src/dusk/network/socket/payload/packetplayerleft.h new file mode 100644 index 00000000..97d3e41d --- /dev/null +++ b/src/dusk/network/socket/payload/packetplayerleft.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "util/uuid.h" + +typedef struct { + uuid_t id; +} packetplayerleft_t; diff --git a/src/dusk/network/socket/payload/packetplayerstate.h b/src/dusk/network/socket/payload/packetplayerstate.h new file mode 100644 index 00000000..0337c7a6 --- /dev/null +++ b/src/dusk/network/socket/payload/packetplayerstate.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "util/uuid.h" +#include + +typedef struct { + uuid_t id; + vec3 position; + vec3 rotation; +} packetplayerstate_t; diff --git a/src/dusk/network/socket/server/CMakeLists.txt b/src/dusk/network/socket/server/CMakeLists.txt new file mode 100644 index 00000000..aaa5fcf2 --- /dev/null +++ b/src/dusk/network/socket/server/CMakeLists.txt @@ -0,0 +1,15 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + serveronline.c + serveroffline.c + serverclient.c + serverthread.c +) + +# Subdirs +add_subdirectory(handler) diff --git a/src/dusk/network/socket/server/handler/CMakeLists.txt b/src/dusk/network/socket/server/handler/CMakeLists.txt new file mode 100644 index 00000000..46b10f9f --- /dev/null +++ b/src/dusk/network/socket/server/handler/CMakeLists.txt @@ -0,0 +1,14 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + serverpackethandlerlist.c + serverpackethandlerhandshake.c + serverpackethandlerack.c + serverpackethandlerping.c + serverpackethandlerdisconnect.c + serverpackethandlerplayerstate.c +) diff --git a/src/dusk/network/socket/server/handler/serverpackethandlerack.c b/src/dusk/network/socket/server/handler/serverpackethandlerack.c new file mode 100644 index 00000000..b745a0a4 --- /dev/null +++ b/src/dusk/network/socket/server/handler/serverpackethandlerack.c @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "serverpackethandlerack.h" +#include "network/socket/payload/packetack.h" + +errorret_t serverPacketHandlerAck( + serverclient_t *client, + const networkaddr_t *from, + const packet_t *packet +) { + (void)from; + + packetack_t payload; + errorChain(packetDecode(packet, &payload)); + + packetReliableChannelAck(&client->remote.reliable, payload.sequence); + + errorOk(); +} diff --git a/src/dusk/network/socket/server/handler/serverpackethandlerack.h b/src/dusk/network/socket/server/handler/serverpackethandlerack.h new file mode 100644 index 00000000..accca6fa --- /dev/null +++ b/src/dusk/network/socket/server/handler/serverpackethandlerack.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "serverpackethandlerlist.h" + +/** + * Handles an ACK: marks the matching reliable-pending send as + * delivered. + * + * @param client The client the packet arrived from. + * @param from The sender's address. + * @param packet The received packet. + * @return An error if the payload failed to decode. + */ +errorret_t serverPacketHandlerAck( + serverclient_t *client, + const networkaddr_t *from, + const packet_t *packet +); diff --git a/src/dusk/network/socket/server/handler/serverpackethandlerdisconnect.c b/src/dusk/network/socket/server/handler/serverpackethandlerdisconnect.c new file mode 100644 index 00000000..348bfd5f --- /dev/null +++ b/src/dusk/network/socket/server/handler/serverpackethandlerdisconnect.c @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "serverpackethandlerdisconnect.h" +#include "network/socket/payload/packetdisconnect.h" +#include "network/socket/server/serveronline.h" + +errorret_t serverPacketHandlerDisconnect( + serverclient_t *client, + const networkaddr_t *from, + const packet_t *packet +) { + (void)from; + + packetdisconnect_t payload; + errorChain(packetDecode(packet, &payload)); + + serverClientDisconnect(&SERVER_ONLINE.socket, client, false); + + errorOk(); +} diff --git a/src/dusk/network/socket/server/handler/serverpackethandlerdisconnect.h b/src/dusk/network/socket/server/handler/serverpackethandlerdisconnect.h new file mode 100644 index 00000000..f1f3e6e3 --- /dev/null +++ b/src/dusk/network/socket/server/handler/serverpackethandlerdisconnect.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "serverpackethandlerlist.h" + +/** + * Handles a client-initiated DISCONNECT: frees the client's slot and + * broadcasts PLAYER_LEFT to everyone else. Unlike + * serverClientDisconnect (server-initiated kick), this does not send a + * disconnect notice back -- the client is the one leaving. + * + * @param client The client the packet arrived from. + * @param from The sender's address. + * @param packet The received packet. + * @return An error if the payload failed to decode. + */ +errorret_t serverPacketHandlerDisconnect( + serverclient_t *client, + const networkaddr_t *from, + const packet_t *packet +); diff --git a/src/dusk/network/socket/server/handler/serverpackethandlerhandshake.c b/src/dusk/network/socket/server/handler/serverpackethandlerhandshake.c new file mode 100644 index 00000000..354041a9 --- /dev/null +++ b/src/dusk/network/socket/server/handler/serverpackethandlerhandshake.c @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "serverpackethandlerhandshake.h" +#include "network/socket/payload/packethandshake.h" +#include "network/socket/payload/packethandshakeaccept.h" +#include "network/socket/payload/packetplayerjoined.h" +#include "util/uuid.h" +#include "util/string.h" +#include "util/memory.h" +#include "time/time.h" +#include "console/console.h" + +errorret_t serverSendHandshakeReject( + networkdgramsocketplatform_t *socket, + const networkaddr_t *to, + const packethandshakerejectreason_t reason +) { + packethandshakereject_t payload = { .reason = reason }; + + packet_t packet; + errorChain(packetEncode(&packet, PACKET_TYPE_HANDSHAKE_REJECT, 0, &payload)); + + uint8_t buffer[PACKET_WIRE_SIZE_MAX]; + size_t length = 0; + errorChain(packetWireEncode(&packet, buffer, sizeof(buffer), &length)); + + size_t sent = 0; + return networkDgramSocketPlatformSendTo(socket, buffer, length, to, &sent); +} + +errorret_t serverPacketHandlerHandshake( + serveronline_t *server, + const networkaddr_t *from, + const packet_t *packet +) { + packethandshake_t payload; + errorChain(packetDecode(packet, &payload)); + + threadMutexLock(&server->clientsMutex); + + serverclient_t *existing = serverOnlineFindClientByAddress(server, from); + if(existing) { + threadMutexUnlock(&server->clientsMutex); + + packethandshakeaccept_t accept = { .id = existing->remote.id }; + return serverClientSend( + &server->socket, existing, PACKET_TYPE_HANDSHAKE_ACCEPT, &accept, true + ); + } + + bool_t reject = false; + packethandshakerejectreason_t rejectReason = + PACKET_HANDSHAKE_REJECT_REASON_VERSION_MISMATCH; + + if(payload.protocolVersion != NETWORK_PROTOCOL_VERSION) { + reject = true; + rejectReason = PACKET_HANDSHAKE_REJECT_REASON_VERSION_MISMATCH; + } else if(payload.username[0] == '\0') { + reject = true; + rejectReason = PACKET_HANDSHAKE_REJECT_REASON_USERNAME_INVALID; + } else { + for(uint32_t i = 0; i < SERVER_CLIENT_COUNT_MAX; i++) { + serverclient_t *c = &server->clients[i]; + if( + c->type == SERVER_CLIENT_TYPE_REMOTE && + stringEquals(c->remote.username, payload.username) + ) { + reject = true; + rejectReason = PACKET_HANDSHAKE_REJECT_REASON_USERNAME_TAKEN; + break; + } + } + } + + serverclient_t *slot = NULL; + if(!reject) { + for(uint32_t i = 0; i < SERVER_CLIENT_COUNT_MAX; i++) { + if(server->clients[i].type == SERVER_CLIENT_TYPE_NULL) { + slot = &server->clients[i]; + break; + } + } + + if(!slot) { + reject = true; + rejectReason = PACKET_HANDSHAKE_REJECT_REASON_SERVER_FULL; + } + } + + if(reject) { + threadMutexUnlock(&server->clientsMutex); + return serverSendHandshakeReject(&server->socket, from, rejectReason); + } + + memoryZero(slot, sizeof(serverclient_t)); + slot->type = SERVER_CLIENT_TYPE_REMOTE; + slot->remote.address = *from; + uuidGenerate(&slot->remote.id); + stringCopy( + slot->remote.username, payload.username, PACKET_HANDSHAKE_USERNAME_MAX - 1 + ); + packetReliableChannelInit(&slot->remote.reliable); + packetInboxInit(&slot->remote.inbox); + slot->remote.lastReceivedAt = TIME.time; + slot->remote.lastSentAt = TIME.time; + + threadMutexUnlock(&server->clientsMutex); + + consolePrint("Client joined: %s", slot->remote.username); + + packethandshakeaccept_t accept = { .id = slot->remote.id }; + errorChain(serverClientSend( + &server->socket, slot, PACKET_TYPE_HANDSHAKE_ACCEPT, &accept, true + )); + + // Catch the new client up on everyone already connected -- otherwise + // they'd only ever learn about players who join after them. + for(uint32_t i = 0; i < SERVER_CLIENT_COUNT_MAX; i++) { + serverclient_t *existingClient = &server->clients[i]; + if(existingClient == slot) continue; + if(existingClient->type != SERVER_CLIENT_TYPE_REMOTE) continue; + + packetplayerjoined_t existingJoined = { .id = existingClient->remote.id }; + stringCopy( + existingJoined.username, existingClient->remote.username, + PACKET_HANDSHAKE_USERNAME_MAX - 1 + ); + errorret_t catchUpRet = serverClientSend( + &server->socket, slot, PACKET_TYPE_PLAYER_JOINED, &existingJoined, true + ); + if(errorIsNotOk(catchUpRet)) errorCatch(errorPrint(catchUpRet)); + } + + packetplayerjoined_t joined = { .id = slot->remote.id }; + stringCopy( + joined.username, slot->remote.username, PACKET_HANDSHAKE_USERNAME_MAX - 1 + ); + errorChain(serverOnlineBroadcast( + PACKET_TYPE_PLAYER_JOINED, &joined, true, slot + )); + + errorOk(); +} diff --git a/src/dusk/network/socket/server/handler/serverpackethandlerhandshake.h b/src/dusk/network/socket/server/handler/serverpackethandlerhandshake.h new file mode 100644 index 00000000..4cfd697a --- /dev/null +++ b/src/dusk/network/socket/server/handler/serverpackethandlerhandshake.h @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "network/networkaddr.h" +#include "network/networkdgramsocketplatform.h" +#include "network/socket/packet.h" +#include "network/socket/payload/packethandshakereject.h" +#include "network/socket/server/serveronline.h" + +/** + * Handles a HANDSHAKE from any address, called directly by the reader + * thread (not via SERVER_PACKET_HANDLERS, since -- unlike every other + * packet type -- a handshake may arrive from an address with no slot + * yet). If from already has a slot, re-sends HANDSHAKE_ACCEPT (the + * client is retrying after losing our first reply). Otherwise validates + * protocol version, username, and available capacity, then either + * claims a slot and broadcasts PLAYER_JOINED, or replies + * HANDSHAKE_REJECT. + * + * @param server The server the handshake arrived on. + * @param from The sender's address. + * @param packet The received packet. + * @return An error if the payload failed to decode, or a send failed. + */ +errorret_t serverPacketHandlerHandshake( + serveronline_t *server, + const networkaddr_t *from, + const packet_t *packet +); + +/** + * Sends a one-off, untracked HANDSHAKE_REJECT to an address with no + * slot. Not tracked for ack + retry: the client's own HANDSHAKE is + * reliable on its end, so a lost reject is naturally retried by the + * client resending the handshake, which re-runs this whole check. + * + * @param socket The server's bound socket to send from. + * @param to The address to reject. + * @param reason The reject reason to send. + * @return An error if the send failed. + */ +errorret_t serverSendHandshakeReject( + networkdgramsocketplatform_t *socket, + const networkaddr_t *to, + const packethandshakerejectreason_t reason +); diff --git a/src/dusk/network/socket/server/handler/serverpackethandlerlist.c b/src/dusk/network/socket/server/handler/serverpackethandlerlist.c new file mode 100644 index 00000000..9607fb84 --- /dev/null +++ b/src/dusk/network/socket/server/handler/serverpackethandlerlist.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "serverpackethandlerlist.h" +#include "serverpackethandlerack.h" +#include "serverpackethandlerping.h" +#include "serverpackethandlerdisconnect.h" +#include "serverpackethandlerplayerstate.h" + +serverpackethandlercallback_t * +SERVER_PACKET_HANDLERS[PACKET_TYPE_COUNT] = { + [PACKET_TYPE_ACK] = serverPacketHandlerAck, + [PACKET_TYPE_PING] = serverPacketHandlerPing, + [PACKET_TYPE_DISCONNECT] = serverPacketHandlerDisconnect, + [PACKET_TYPE_PLAYER_STATE] = serverPacketHandlerPlayerState, +}; diff --git a/src/dusk/network/socket/server/handler/serverpackethandlerlist.h b/src/dusk/network/socket/server/handler/serverpackethandlerlist.h new file mode 100644 index 00000000..ccbb8790 --- /dev/null +++ b/src/dusk/network/socket/server/handler/serverpackethandlerlist.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "network/networkaddr.h" +#include "network/socket/packet.h" +#include "network/socket/server/serverclient.h" + +/** + * Handler signature for every packet type the server can receive from + * an already-connected client. Unlike serverPacketHandlerHandshake, + * client is always non-NULL here -- these are only ever dispatched from + * a known client's drained inbox. + */ +typedef errorret_t (serverpackethandlercallback_t)( + serverclient_t *client, + const networkaddr_t *from, + const packet_t *packet +); + +extern serverpackethandlercallback_t * + SERVER_PACKET_HANDLERS[PACKET_TYPE_COUNT]; diff --git a/src/dusk/network/socket/server/handler/serverpackethandlerping.c b/src/dusk/network/socket/server/handler/serverpackethandlerping.c new file mode 100644 index 00000000..fbab7617 --- /dev/null +++ b/src/dusk/network/socket/server/handler/serverpackethandlerping.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "serverpackethandlerping.h" + +errorret_t serverPacketHandlerPing( + serverclient_t *client, + const networkaddr_t *from, + const packet_t *packet +) { + (void)client; + (void)from; + (void)packet; + + errorOk(); +} diff --git a/src/dusk/network/socket/server/handler/serverpackethandlerping.h b/src/dusk/network/socket/server/handler/serverpackethandlerping.h new file mode 100644 index 00000000..96403195 --- /dev/null +++ b/src/dusk/network/socket/server/handler/serverpackethandlerping.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "serverpackethandlerlist.h" + +/** + * Handles a PING: a no-op, since receiving any packet already refreshes + * client->remote.lastReceivedAt in the drain loop before dispatch. + * + * @param client The client the packet arrived from. + * @param from The sender's address. + * @param packet The received packet. + * @return Always ok. + */ +errorret_t serverPacketHandlerPing( + serverclient_t *client, + const networkaddr_t *from, + const packet_t *packet +); diff --git a/src/dusk/network/socket/server/handler/serverpackethandlerplayerstate.c b/src/dusk/network/socket/server/handler/serverpackethandlerplayerstate.c new file mode 100644 index 00000000..4e9e84fe --- /dev/null +++ b/src/dusk/network/socket/server/handler/serverpackethandlerplayerstate.c @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "serverpackethandlerplayerstate.h" +#include "network/socket/payload/packetplayerstate.h" +#include "network/socket/server/serveronline.h" +#include + +errorret_t serverPacketHandlerPlayerState( + serverclient_t *client, + const networkaddr_t *from, + const packet_t *packet +) { + (void)from; + + packetplayerstate_t payload; + errorChain(packetDecode(packet, &payload)); + + glm_vec3_copy(payload.position, client->remote.position); + glm_vec3_copy(payload.rotation, client->remote.rotation); + + packetplayerstate_t broadcast; + broadcast.id = client->remote.id; + glm_vec3_copy(client->remote.position, broadcast.position); + glm_vec3_copy(client->remote.rotation, broadcast.rotation); + + return serverOnlineBroadcast( + PACKET_TYPE_PLAYER_STATE, &broadcast, false, client + ); +} diff --git a/src/dusk/network/socket/server/handler/serverpackethandlerplayerstate.h b/src/dusk/network/socket/server/handler/serverpackethandlerplayerstate.h new file mode 100644 index 00000000..f7af0818 --- /dev/null +++ b/src/dusk/network/socket/server/handler/serverpackethandlerplayerstate.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "serverpackethandlerlist.h" + +/** + * Handles a PLAYER_STATE: records the reported position/rotation on the + * sender's slot, then relays it to every other connected client. + * + * @param client The client the packet arrived from. + * @param from The sender's address. + * @param packet The received packet. + * @return An error if the payload failed to decode. + */ +errorret_t serverPacketHandlerPlayerState( + serverclient_t *client, + const networkaddr_t *from, + const packet_t *packet +); diff --git a/src/dusk/network/socket/server/serverclient.c b/src/dusk/network/socket/server/serverclient.c new file mode 100644 index 00000000..79998df7 --- /dev/null +++ b/src/dusk/network/socket/server/serverclient.c @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "serverclient.h" +#include "network/socket/payload/packetdisconnect.h" +#include "network/socket/payload/packetplayerleft.h" +#include "network/socket/server/serveronline.h" +#include "util/memory.h" +#include "assert/assert.h" +#include "time/time.h" + +errorret_t serverClientResendCallback( + const packet_t *packet, + const networkaddr_t *destAddr, + void *user +) { + networkdgramsocketplatform_t *socket = (networkdgramsocketplatform_t *)user; + + uint8_t buffer[PACKET_WIRE_SIZE_MAX]; + size_t length = 0; + errorChain(packetWireEncode(packet, buffer, sizeof(buffer), &length)); + + size_t sent = 0; + return networkDgramSocketPlatformSendTo( + socket, buffer, length, destAddr, &sent + ); +} + +errorret_t serverClientSend( + networkdgramsocketplatform_t *socket, + serverclient_t *client, + const packettype_t type, + const void *payload, + const bool_t reliable +) { + assertNotNull(socket, "socket must not be NULL"); + assertNotNull(client, "client must not be NULL"); + assertTrue( + client->type == SERVER_CLIENT_TYPE_REMOTE, "client must be remote" + ); + + const uint32_t sequence = + packetReliableChannelNextSequence(&client->remote.reliable); + + packet_t packet; + errorChain(packetEncode(&packet, type, sequence, payload)); + + uint8_t buffer[PACKET_WIRE_SIZE_MAX]; + size_t length = 0; + errorChain(packetWireEncode(&packet, buffer, sizeof(buffer), &length)); + + size_t sent = 0; + errorChain(networkDgramSocketPlatformSendTo( + socket, buffer, length, &client->remote.address, &sent + )); + + client->remote.lastSentAt = TIME.time; + + if(reliable) { + errorChain(packetReliableChannelTrack( + &client->remote.reliable, &packet, &client->remote.address + )); + } + + errorOk(); +} + +void serverClientDisconnect( + networkdgramsocketplatform_t *socket, + serverclient_t *client, + const bool_t notifyPeer +) { + assertNotNull(socket, "socket must not be NULL"); + assertNotNull(client, "client must not be NULL"); + if(client->type != SERVER_CLIENT_TYPE_REMOTE) return; + + const uuid_t id = client->remote.id; + + if(notifyPeer) { + packetdisconnect_t disconnect = { .id = id }; + errorret_t ret = serverClientSend( + socket, client, PACKET_TYPE_DISCONNECT, &disconnect, false + ); + if(errorIsNotOk(ret)) errorCatch(errorPrint(ret)); + } + + packetReliableChannelDispose(&client->remote.reliable); + packetInboxDispose(&client->remote.inbox); + + memoryZero(client, sizeof(serverclient_t)); + client->type = SERVER_CLIENT_TYPE_NULL; + + packetplayerleft_t left = { .id = id }; + errorret_t ret = serverOnlineBroadcast( + PACKET_TYPE_PLAYER_LEFT, &left, true, NULL + ); + if(errorIsNotOk(ret)) errorCatch(errorPrint(ret)); +} diff --git a/src/dusk/network/socket/server/serverclient.h b/src/dusk/network/socket/server/serverclient.h new file mode 100644 index 00000000..49202e29 --- /dev/null +++ b/src/dusk/network/socket/server/serverclient.h @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "network/networkaddr.h" +#include "network/networkdgramsocketplatform.h" +#include "network/socket/packet.h" +#include "serverclientlocal.h" +#include "serverclientremote.h" + +typedef enum { + SERVER_CLIENT_TYPE_NULL, + SERVER_CLIENT_TYPE_LOCAL, + SERVER_CLIENT_TYPE_REMOTE, +} serverclienttype_t; + +typedef struct { + serverclienttype_t type; + union { + serverclientlocal_t local; + serverclientremote_t remote; + }; +} serverclient_t; + +/** + * Sends a packet to a connected remote client. + * + * @param socket The server's bound socket to send from. + * @param client The remote client to send to. + * @param type The packet type to send. + * @param payload The payload struct matching type's registered size. + * @param reliable If true, tracks the packet for ack + retry. + * @return An error if the payload doesn't match type's registered size, + * or the send failed. + */ +errorret_t serverClientSend( + networkdgramsocketplatform_t *socket, + serverclient_t *client, + const packettype_t type, + const void *payload, + const bool_t reliable +); + +/** + * Removes a connected remote client: optionally sends it a best-effort + * disconnect notice, broadcasts PLAYER_LEFT to every other connected + * client, then frees the slot. Does nothing if client isn't + * SERVER_CLIENT_TYPE_REMOTE. + * + * @param socket The server's bound socket to send from. + * @param client The remote client to remove. + * @param notifyPeer If true, sends client a disconnect notice first -- + * skip this when client already knows (it disconnected itself) + * or is unreachable (timed out). + */ +void serverClientDisconnect( + networkdgramsocketplatform_t *socket, + serverclient_t *client, + const bool_t notifyPeer +); + +/** + * packetReliableChannelTick resend callback: re-sends packet to + * destAddr via the socket pointed to by user. + * + * @param packet The packet to resend. + * @param destAddr The address to resend to. + * @param user The networkdgramsocketplatform_t to send from. + * @return An error if the send failed. + */ +errorret_t serverClientResendCallback( + const packet_t *packet, + const networkaddr_t *destAddr, + void *user +); diff --git a/src/dusk/network/socket/server/serverclientlocal.h b/src/dusk/network/socket/server/serverclientlocal.h new file mode 100644 index 00000000..32067f36 --- /dev/null +++ b/src/dusk/network/socket/server/serverclientlocal.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "util/uuid.h" +#include + +typedef struct { + uuid_t id; + vec3 position; + vec3 rotation; +} serverclientlocal_t; diff --git a/src/dusk/network/socket/server/serverclientremote.h b/src/dusk/network/socket/server/serverclientremote.h new file mode 100644 index 00000000..5e483f61 --- /dev/null +++ b/src/dusk/network/socket/server/serverclientremote.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "util/uuid.h" +#include "network/networkaddr.h" +#include "network/socket/packetinbox.h" +#include "network/socket/packetreliable.h" +#include "network/socket/payload/packethandshake.h" +#include + +typedef struct { + networkaddr_t address; + uuid_t id; + char_t username[PACKET_HANDSHAKE_USERNAME_MAX]; + vec3 position; + vec3 rotation; + packetreliablechannel_t reliable; + float_t lastReceivedAt; + float_t lastSentAt; + + packetinbox_t inbox; +} serverclientremote_t; diff --git a/src/dusk/network/socket/server/serveroffline.c b/src/dusk/network/socket/server/serveroffline.c new file mode 100644 index 00000000..63df3068 --- /dev/null +++ b/src/dusk/network/socket/server/serveroffline.c @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "serveroffline.h" +#include "network/socket/payload/packethandshake.h" +#include "network/socket/payload/packethandshakeaccept.h" +#include "network/socket/payload/packetplayerstate.h" +#include "network/socket/payload/packetping.h" +#include "util/memory.h" +#include "util/uuid.h" +#include "time/time.h" +#include + +serveroffline_t SERVER_OFFLINE; + +errorret_t serverOfflineHost(void) { + memoryZero(&SERVER_OFFLINE, sizeof(serveroffline_t)); + packetInboxInit(&SERVER_OFFLINE.inbox); + SERVER_OFFLINE.running = true; + + errorOk(); +} + +errorret_t serverOfflineRegisterClient(packetinbox_t *clientInbox) { + if(!SERVER_OFFLINE.running) errorThrow("Offline server is not running"); + + SERVER_OFFLINE.clientInbox = clientInbox; + + errorOk(); +} + +void serverOfflineUnregisterClient(packetinbox_t *clientInbox) { + if(SERVER_OFFLINE.clientInbox != clientInbox) return; + + SERVER_OFFLINE.clientInbox = NULL; + memoryZero(&SERVER_OFFLINE.client, sizeof(serverclient_t)); +} + +errorret_t serverOfflineReceiveFromClient(const packet_t *packet) { + if(!SERVER_OFFLINE.running) errorThrow("Offline server is not running"); + + packetInboxPush(&SERVER_OFFLINE.inbox, packet); + + errorOk(); +} + +errorret_t serverOfflineSendToClient( + const packettype_t type, + const void *payload +) { + if(!SERVER_OFFLINE.clientInbox) errorThrow("No local client connected"); + + packet_t packet; + errorChain( + packetEncode(&packet, type, SERVER_OFFLINE.nextSequence++, payload) + ); + + packetInboxPush(SERVER_OFFLINE.clientInbox, &packet); + SERVER_OFFLINE.lastSentAt = TIME.time; + + errorOk(); +} + +errorret_t serverOfflineHandleHandshake(const packet_t *packet) { + packethandshake_t payload; + errorChain(packetDecode(packet, &payload)); + + memoryZero(&SERVER_OFFLINE.client, sizeof(serverclient_t)); + SERVER_OFFLINE.client.type = SERVER_CLIENT_TYPE_LOCAL; + uuidGenerate(&SERVER_OFFLINE.client.local.id); + + packethandshakeaccept_t accept = { .id = SERVER_OFFLINE.client.local.id }; + errorChain(serverOfflineSendToClient(PACKET_TYPE_HANDSHAKE_ACCEPT, &accept)); + + errorOk(); +} + +errorret_t serverOfflineHandlePlayerState(const packet_t *packet) { + packetplayerstate_t payload; + errorChain(packetDecode(packet, &payload)); + + glm_vec3_copy(payload.position, SERVER_OFFLINE.client.local.position); + glm_vec3_copy(payload.rotation, SERVER_OFFLINE.client.local.rotation); + + errorOk(); +} + +void serverOfflineHandleDisconnect(void) { + SERVER_OFFLINE.clientInbox = NULL; + memoryZero(&SERVER_OFFLINE.client, sizeof(serverclient_t)); +} + +errorret_t serverOfflineUpdate(void) { + if(!SERVER_OFFLINE.running) errorOk(); + + packet_t drained[PACKET_INBOX_SIZE]; + const uint32_t drainedCount = + packetInboxDrain(&SERVER_OFFLINE.inbox, drained, PACKET_INBOX_SIZE); + + for(uint32_t i = 0; i < drainedCount; i++) { + const packet_t *packet = &drained[i]; + + if(packet->type == PACKET_TYPE_HANDSHAKE) { + errorret_t ret = serverOfflineHandleHandshake(packet); + if(errorIsNotOk(ret)) errorCatch(errorPrint(ret)); + continue; + } + + if(SERVER_OFFLINE.client.type != SERVER_CLIENT_TYPE_LOCAL) continue; + + if(packet->type == PACKET_TYPE_PLAYER_STATE) { + errorret_t ret = serverOfflineHandlePlayerState(packet); + if(errorIsNotOk(ret)) errorCatch(errorPrint(ret)); + } else if(packet->type == PACKET_TYPE_DISCONNECT) { + serverOfflineHandleDisconnect(); + } + // ACK/PING: no-op -- an in-memory push can't be lost, so nothing on + // this side is ever waiting on an ack or a liveness check. + } + + if( + SERVER_OFFLINE.client.type == SERVER_CLIENT_TYPE_LOCAL && + SERVER_OFFLINE.clientInbox != NULL && + TIME.time - SERVER_OFFLINE.lastSentAt > + SERVER_OFFLINE_PING_INTERVAL_SECONDS + ) { + packetping_t ping = { .unused = 0 }; + errorret_t ret = serverOfflineSendToClient(PACKET_TYPE_PING, &ping); + if(errorIsNotOk(ret)) errorCatch(errorPrint(ret)); + } + + errorOk(); +} + +errorret_t serverOfflineStop(void) { + if(!SERVER_OFFLINE.running) errorOk(); + + packetInboxDispose(&SERVER_OFFLINE.inbox); + SERVER_OFFLINE.clientInbox = NULL; + memoryZero(&SERVER_OFFLINE.client, sizeof(serverclient_t)); + SERVER_OFFLINE.running = false; + + errorOk(); +} diff --git a/src/dusk/network/socket/server/serveroffline.h b/src/dusk/network/socket/server/serveroffline.h new file mode 100644 index 00000000..9d43efe0 --- /dev/null +++ b/src/dusk/network/socket/server/serveroffline.h @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "network/socket/packet.h" +#include "network/socket/packetinbox.h" +#include "serverclient.h" + +#define SERVER_OFFLINE_PING_INTERVAL_SECONDS 2.0f + +typedef struct { + serverclient_t client; + + // Packets FROM the local client TO this offline server, drained by + // serverOfflineUpdate() -- same queued, drained-on-update timing as + // the online path, just never actually crossing a socket. + packetinbox_t inbox; + + // Where to deliver packets FROM this offline server TO the local + // client -- points at that localclient_t's own inbox. NULL if no + // client is currently registered. + packetinbox_t *clientInbox; + + uint32_t nextSequence; + float_t lastSentAt; + bool_t running; +} serveroffline_t; + +extern serveroffline_t SERVER_OFFLINE; + +/** + * Starts the offline (in-process, non-networked) server. Mirrors + * serverOnlineHost's role and timing model exactly, minus the socket -- + * there's nothing to bind, so no port is needed. + * + * @return Always ok. + */ +errorret_t serverOfflineHost(void); + +/** + * Drains the inbox, processes the handshake/state/disconnect flow for + * the single local client, and keeps its no-packets-received timer from + * falsely firing (the same idle-ping keepalive serverOnlineUpdate does + * for real connections). A no-op if not currently running. Called once + * per frame. + * + * @return An error if a fatal error occurred while processing. + */ +errorret_t serverOfflineUpdate(void); + +/** + * Stops the offline server, clearing any registered client. + * + * @return Always ok. + */ +errorret_t serverOfflineStop(void); + +/** + * Registers a local client's inbox as the delivery target for this + * offline server's responses. Called by localClientConnectOffline; + * there is deliberately no address/handle to look clients up by, since + * there is only ever one. + * + * @param clientInbox The connecting local client's own inbox. + * @return An error if the offline server isn't running. + */ +errorret_t serverOfflineRegisterClient(packetinbox_t *clientInbox); + +/** + * Unregisters a local client, if it's the currently registered one, and + * clears the connected client slot. + * + * @param clientInbox The disconnecting local client's own inbox. + */ +void serverOfflineUnregisterClient(packetinbox_t *clientInbox); + +/** + * Delivers a packet from the local client to this offline server -- + * the offline equivalent of a client's socket send. Never fails to + * "send" (it's a direct in-memory push), only to queue if not running. + * + * @param packet The packet to deliver. + * @return An error if the offline server isn't running. + */ +errorret_t serverOfflineReceiveFromClient(const packet_t *packet); + +/** + * Sends a packet to the registered local client. + * + * @param type The packet type to send. + * @param payload The payload struct matching type's registered size. + * @return An error if no client is registered, or the payload doesn't + * match type's registered size. + */ +errorret_t serverOfflineSendToClient( + const packettype_t type, + const void *payload +); + +/** + * Handles a HANDSHAKE from the local client: always accepted (a single + * trusted in-process player has no version skew or username to + * conflict with), assigns a fresh id, and replies HANDSHAKE_ACCEPT. + * + * @param packet The received packet. + * @return An error if the payload failed to decode, or the reply + * couldn't be queued. + */ +errorret_t serverOfflineHandleHandshake(const packet_t *packet); + +/** + * Handles a PLAYER_STATE from the local client: records the reported + * position/rotation on its slot. Nothing to relay it to offline. + * + * @param packet The received packet. + * @return An error if the payload failed to decode. + */ +errorret_t serverOfflineHandlePlayerState(const packet_t *packet); + +/** + * Handles a DISCONNECT from the local client: clears the connected + * client slot and its registered inbox. + */ +void serverOfflineHandleDisconnect(void); diff --git a/src/dusk/network/socket/server/serveronline.c b/src/dusk/network/socket/server/serveronline.c new file mode 100644 index 00000000..39066067 --- /dev/null +++ b/src/dusk/network/socket/server/serveronline.c @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "serveronline.h" +#include "serverthread.h" +#include "handler/serverpackethandlerlist.h" +#include "network/socket/packetregistry.h" +#include "network/socket/payload/packetack.h" +#include "network/socket/payload/packetping.h" +#include "util/memory.h" +#include "assert/assert.h" +#include "time/time.h" + +serveronline_t SERVER_ONLINE; + +errorret_t serverOnlineHost(const uint16_t port) { + memoryZero(&SERVER_ONLINE, sizeof(serveronline_t)); + threadMutexInit(&SERVER_ONLINE.clientsMutex); + + errorChain(networkDgramSocketPlatformOpen(&SERVER_ONLINE.socket)); + errorChain(networkDgramSocketPlatformBind(&SERVER_ONLINE.socket, port)); + + SERVER_ONLINE.port = port; + SERVER_ONLINE.running = true; + + threadInit(&SERVER_ONLINE.readerThread, serverThreadRun); + SERVER_ONLINE.readerThread.data = &SERVER_ONLINE; + threadStart(&SERVER_ONLINE.readerThread); + + errorOk(); +} + +serverclient_t * serverOnlineFindClientByAddress( + serveronline_t *server, + const networkaddr_t *addr +) { + for(uint32_t i = 0; i < SERVER_CLIENT_COUNT_MAX; i++) { + serverclient_t *client = &server->clients[i]; + if(client->type != SERVER_CLIENT_TYPE_REMOTE) continue; + if(networkAddrEquals(&client->remote.address, addr)) return client; + } + + return NULL; +} + +errorret_t serverOnlineUpdate(void) { + if(!SERVER_ONLINE.running) errorOk(); + + for(uint32_t i = 0; i < SERVER_CLIENT_COUNT_MAX; i++) { + serverclient_t *client = &SERVER_ONLINE.clients[i]; + if(client->type != SERVER_CLIENT_TYPE_REMOTE) continue; + + packet_t drained[PACKET_INBOX_SIZE]; + const uint32_t drainedCount = + packetInboxDrain(&client->remote.inbox, drained, PACKET_INBOX_SIZE); + + for(uint32_t j = 0; j < drainedCount; j++) { + if(client->type != SERVER_CLIENT_TYPE_REMOTE) break; + + client->remote.lastReceivedAt = TIME.time; + + const packettype_t type = drained[j].type; + + if(PACKET_TYPE_INFO[type].reliable) { + packetack_t ack = { .sequence = drained[j].sequence }; + errorret_t ackRet = serverClientSend( + &SERVER_ONLINE.socket, client, PACKET_TYPE_ACK, &ack, false + ); + if(errorIsNotOk(ackRet)) errorCatch(errorPrint(ackRet)); + } + + if(SERVER_PACKET_HANDLERS[type]) { + errorret_t ret = SERVER_PACKET_HANDLERS[type]( + client, &client->remote.address, &drained[j] + ); + if(errorIsNotOk(ret)) errorCatch(errorPrint(ret)); + } + } + + if(client->type != SERVER_CLIENT_TYPE_REMOTE) continue; + + bool_t expired = false; + packetReliableChannelTick( + &client->remote.reliable, serverClientResendCallback, + &SERVER_ONLINE.socket, &expired + ); + + if(expired) { + serverClientDisconnect(&SERVER_ONLINE.socket, client, false); + continue; + } + + if( + TIME.time - client->remote.lastReceivedAt > SERVER_CLIENT_TIMEOUT_SECONDS + ) { + serverClientDisconnect(&SERVER_ONLINE.socket, client, false); + continue; + } + + // Without this, a quiet client (no state/chat traffic) would only + // ever hear from the server once, at handshake, and would then + // falsely time itself out -- the server must keep sending + // something too, not just wait to receive. + if( + TIME.time - client->remote.lastSentAt > + SERVER_CLIENT_PING_INTERVAL_SECONDS + ) { + packetping_t ping = { .unused = 0 }; + errorret_t pingRet = serverClientSend( + &SERVER_ONLINE.socket, client, PACKET_TYPE_PING, &ping, false + ); + if(errorIsNotOk(pingRet)) errorCatch(errorPrint(pingRet)); + } + } + + errorOk(); +} + +errorret_t serverOnlineStop(void) { + if(!SERVER_ONLINE.running) errorOk(); + + for(uint32_t i = 0; i < SERVER_CLIENT_COUNT_MAX; i++) { + serverclient_t *client = &SERVER_ONLINE.clients[i]; + if(client->type == SERVER_CLIENT_TYPE_REMOTE) { + serverClientDisconnect(&SERVER_ONLINE.socket, client, true); + } + } + + threadStop(&SERVER_ONLINE.readerThread); + networkDgramSocketPlatformClose(&SERVER_ONLINE.socket); + threadMutexDispose(&SERVER_ONLINE.clientsMutex); + + SERVER_ONLINE.running = false; + + errorOk(); +} + +errorret_t serverOnlineBroadcast( + const packettype_t type, + const void *payload, + const bool_t reliable, + const serverclient_t *exclude +) { + for(uint32_t i = 0; i < SERVER_CLIENT_COUNT_MAX; i++) { + serverclient_t *client = &SERVER_ONLINE.clients[i]; + if(client->type != SERVER_CLIENT_TYPE_REMOTE) continue; + if(client == exclude) continue; + + errorret_t ret = serverClientSend( + &SERVER_ONLINE.socket, client, type, payload, reliable + ); + if(errorIsNotOk(ret)) errorCatch(errorPrint(ret)); + } + + errorOk(); +} diff --git a/src/dusk/network/socket/server/serveronline.h b/src/dusk/network/socket/server/serveronline.h new file mode 100644 index 00000000..e4361a3e --- /dev/null +++ b/src/dusk/network/socket/server/serveronline.h @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "thread/thread.h" +#include "thread/threadmutex.h" +#include "network/networkaddr.h" +#include "network/networkdgramsocketplatform.h" +#include "network/socket/packet.h" +#include "serverclient.h" + +#define SERVER_CLIENT_COUNT_MAX 16 +#define SERVER_CLIENT_TIMEOUT_SECONDS 10.0f +#define SERVER_CLIENT_PING_INTERVAL_SECONDS 2.0f + +typedef struct { + networkdgramsocketplatform_t socket; + thread_t readerThread; + + serverclient_t clients[SERVER_CLIENT_COUNT_MAX]; + threadmutex_t clientsMutex; + + uint16_t port; + bool_t running; +} serveronline_t; + +extern serveronline_t SERVER_ONLINE; + +/** + * Starts hosting: binds the UDP socket to port and starts the + * background reader thread. Does not block waiting for clients. + * + * @param port The local port to listen on. + * @return An error if the socket could not be opened/bound. + */ +errorret_t serverOnlineHost(const uint16_t port); + +/** + * Drains every connected client's inbox, ticks their reliability + * channels, and sweeps for timed-out clients. A no-op if not currently + * hosting. Called once per frame. + * + * @return An error if a fatal error occurred while processing. + */ +errorret_t serverOnlineUpdate(void); + +/** + * Stops hosting: disconnects every connected client, stops the reader + * thread, and closes the socket. + * + * @return An error if a fatal error occurred while stopping. + */ +errorret_t serverOnlineStop(void); + +/** + * Finds a connected remote client by its address. + * + * @param server The server to search. + * @param addr The address to search for. + * @return The matching client, or NULL if none is connected from addr. + */ +serverclient_t * serverOnlineFindClientByAddress( + serveronline_t *server, + const networkaddr_t *addr +); + +/** + * Sends a packet to every connected remote client, optionally skipping + * one. + * + * @param type The packet type to send. + * @param payload The payload struct matching type's registered size. + * @param reliable If true, tracks the packet per-client for ack+retry. + * @param exclude A client to skip, or NULL to send to everyone. + * @return An error if any send failed (already logged; broadcast + * continues to the remaining clients regardless). + */ +errorret_t serverOnlineBroadcast( + const packettype_t type, + const void *payload, + const bool_t reliable, + const serverclient_t *exclude +); diff --git a/src/dusk/network/socket/server/serverthread.c b/src/dusk/network/socket/server/serverthread.c new file mode 100644 index 00000000..ea603858 --- /dev/null +++ b/src/dusk/network/socket/server/serverthread.c @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "serverthread.h" +#include "serveronline.h" +#include "handler/serverpackethandlerhandshake.h" +#include "assert/assert.h" + +void serverThreadRun(thread_t *thread) { + assertNotMainThread("serverThreadRun must not run on the main thread."); + serveronline_t *server = (serveronline_t *)thread->data; + + uint8_t buffer[PACKET_WIRE_SIZE_MAX]; + + while(!threadShouldStop(thread)) { + size_t received = 0; + networkaddr_t from; + errorret_t ret = networkDgramSocketPlatformReceiveFrom( + &server->socket, buffer, sizeof(buffer), &received, &from + ); + + if(errorIsNotOk(ret)) { + errorCatch(errorPrint(ret)); + continue; + } + + if(received == 0) continue; + + packet_t packet; + errorret_t decodeRet = packetWireDecode(buffer, received, &packet); + if(errorIsNotOk(decodeRet)) { + errorCatch(errorPrint(decodeRet)); + continue; + } + + if(packet.type == PACKET_TYPE_HANDSHAKE) { + errorret_t handleRet = + serverPacketHandlerHandshake(server, &from, &packet); + if(errorIsNotOk(handleRet)) errorCatch(errorPrint(handleRet)); + continue; + } + + threadMutexLock(&server->clientsMutex); + serverclient_t *client = serverOnlineFindClientByAddress(server, &from); + threadMutexUnlock(&server->clientsMutex); + + if(!client) continue; + + serverThreadEnqueue(client, &packet); + } +} + +void serverThreadEnqueue(serverclient_t *client, const packet_t *packet) { + packetInboxPush(&client->remote.inbox, packet); +} diff --git a/src/dusk/network/socket/server/serverthread.h b/src/dusk/network/socket/server/serverthread.h new file mode 100644 index 00000000..b57184bd --- /dev/null +++ b/src/dusk/network/socket/server/serverthread.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "thread/thread.h" +#include "network/socket/packet.h" +#include "serverclient.h" + +/** + * Background reader thread body: blocks on the shared socket's receive + * with a short timeout. HANDSHAKE packets are handled immediately + * (regardless of whether the sender has a slot yet); every other type + * is matched to an existing client by address and enqueued into that + * client's inbox, or dropped if the address is unknown. + * thread->data must be the serveronline_t to read for. + * + * @param thread The thread this is running on. + */ +void serverThreadRun(thread_t *thread); + +/** + * Pushes a decoded packet into a client's inbox for the main thread to + * drain. Silently drops the packet if the inbox is full. + * + * @param client The client whose inbox to push into. + * @param packet The packet to enqueue. + */ +void serverThreadEnqueue(serverclient_t *client, const packet_t *packet); diff --git a/src/dusk/util/CMakeLists.txt b/src/dusk/util/CMakeLists.txt index 62fb7902..d205029e 100644 --- a/src/dusk/util/CMakeLists.txt +++ b/src/dusk/util/CMakeLists.txt @@ -15,4 +15,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} math.c sort.c ref.c + uuid.c ) \ No newline at end of file diff --git a/src/dusk/util/uuid.c b/src/dusk/util/uuid.c new file mode 100644 index 00000000..7e22872b --- /dev/null +++ b/src/dusk/util/uuid.c @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uuid.h" +#include "util/memory.h" +#include "util/random.h" +#include "util/string.h" + +const uuid_t UUID_NULL = { .bytes = { 0 } }; + +void uuidGenerate(uuid_t *out) { + for(uint8_t i = 0; i < UUID_BYTE_COUNT; i++) { + out->bytes[i] = (uint8_t)randomInt(0, 256); + } + + out->bytes[6] = (uint8_t)((out->bytes[6] & 0x0F) | 0x40); + out->bytes[8] = (uint8_t)((out->bytes[8] & 0x3F) | 0x80); +} + +bool_t uuidEquals(const uuid_t *a, const uuid_t *b) { + return memoryCompare(a->bytes, b->bytes, UUID_BYTE_COUNT) == 0; +} + +void uuidToString(const uuid_t *id, char_t *dest, const size_t destSize) { + const uint8_t *b = id->bytes; + + stringFormat( + dest, destSize, + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], + b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15] + ); +} diff --git a/src/dusk/util/uuid.h b/src/dusk/util/uuid.h new file mode 100644 index 00000000..be68d04e --- /dev/null +++ b/src/dusk/util/uuid.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +#define UUID_BYTE_COUNT 16 +#define UUID_STRING_LENGTH 36 + +typedef struct { + uint8_t bytes[UUID_BYTE_COUNT]; +} uuid_t; + +extern const uuid_t UUID_NULL; + +/** + * Generates a random (v4-style) UUID. + * + * @param out The UUID to fill in. + */ +void uuidGenerate(uuid_t *out); + +/** + * Compares two UUIDs for equality. + * + * @param a The first UUID. + * @param b The second UUID. + * @return true if every byte matches, false otherwise. + */ +bool_t uuidEquals(const uuid_t *a, const uuid_t *b); + +/** + * Formats a UUID as a lowercase hyphenated string, e.g. + * "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx". + * + * @param id The UUID to format. + * @param dest The destination buffer. + * @param destSize The size of dest, excl. null terminator. Must be at + * least UUID_STRING_LENGTH. + */ +void uuidToString(const uuid_t *id, char_t *dest, const size_t destSize); diff --git a/src/dusklinux/network/CMakeLists.txt b/src/dusklinux/network/CMakeLists.txt index ca2770f2..1023e2da 100644 --- a/src/dusklinux/network/CMakeLists.txt +++ b/src/dusklinux/network/CMakeLists.txt @@ -7,4 +7,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC networklinux.c networksocketlinux.c + networkdgramsocketlinux.c ) diff --git a/src/dusklinux/network/networkdgramsocketlinux.c b/src/dusklinux/network/networkdgramsocketlinux.c new file mode 100644 index 00000000..250f3993 --- /dev/null +++ b/src/dusklinux/network/networkdgramsocketlinux.c @@ -0,0 +1,196 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "networkdgramsocketlinux.h" +#include "util/memory.h" +#include "util/string.h" +#include "assert/assert.h" +#include +#include +#include +#include +#include +#include + +errorret_t networkDgramSocketLinuxOpen(networkdgramsocketlinux_t *sock) { + assertNotNull(sock, "sock must not be NULL"); + + const int_t fd = socket(AF_INET, SOCK_DGRAM, 0); + if(fd < 0) errorThrow("Failed to create UDP socket: %s", strerror(errno)); + + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = NETWORK_DGRAM_SOCKET_LINUX_RECEIVE_TIMEOUT_MS * 1000; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + + sock->fd = fd; + errorOk(); +} + +errorret_t networkDgramSocketLinuxBind( + networkdgramsocketlinux_t *sock, + const uint16_t port +) { + assertNotNull(sock, "sock must not be NULL"); + + struct sockaddr_in addr; + memoryZero(&addr, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(port); + + if(bind(sock->fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + errorThrow("Failed to bind UDP socket to port %u: %s", + port, strerror(errno)); + } + + errorOk(); +} + +errorret_t networkDgramSocketLinuxConnect( + networkdgramsocketlinux_t *sock, + const char_t *host, + const uint16_t port +) { + assertNotNull(sock, "sock must not be NULL"); + assertNotNull(host, "host must not be NULL"); + + char_t portStr[8]; + stringFormat(portStr, sizeof(portStr) - 1, "%u", port); + + struct addrinfo hints; + memoryZero(&hints, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_DGRAM; + + struct addrinfo *result = NULL; + const int_t gaiRet = getaddrinfo(host, portStr, &hints, &result); + if(gaiRet != 0) { + errorThrow("Failed to resolve host %s: %s", host, gai_strerror(gaiRet)); + } + + const int_t connectRet = connect( + sock->fd, result->ai_addr, result->ai_addrlen + ); + freeaddrinfo(result); + + if(connectRet != 0) { + errorThrow("Failed to connect UDP socket to %s:%u: %s", + host, port, strerror(errno)); + } + + errorOk(); +} + +errorret_t networkDgramSocketLinuxSend( + networkdgramsocketlinux_t *sock, + const uint8_t *data, + const size_t length, + size_t *outSent +) { + assertNotNull(sock, "sock must not be NULL"); + assertNotNull(data, "data must not be NULL"); + assertNotNull(outSent, "outSent must not be NULL"); + + const ssize_t sent = send(sock->fd, data, length, 0); + if(sent < 0) errorThrow("Failed to send datagram: %s", strerror(errno)); + + *outSent = (size_t)sent; + errorOk(); +} + +errorret_t networkDgramSocketLinuxSendTo( + networkdgramsocketlinux_t *sock, + const uint8_t *data, + const size_t length, + const networkaddr_t *addr, + size_t *outSent +) { + assertNotNull(sock, "sock must not be NULL"); + assertNotNull(data, "data must not be NULL"); + assertNotNull(addr, "addr must not be NULL"); + assertNotNull(outSent, "outSent must not be NULL"); + + struct sockaddr_in dest; + memoryZero(&dest, sizeof(dest)); + dest.sin_family = AF_INET; + memoryCopy(&dest.sin_addr.s_addr, addr->ip.ip, sizeof(dest.sin_addr.s_addr)); + dest.sin_port = htons(addr->port); + + const ssize_t sent = sendto( + sock->fd, data, length, 0, (struct sockaddr *)&dest, sizeof(dest) + ); + if(sent < 0) errorThrow("Failed to send datagram: %s", strerror(errno)); + + *outSent = (size_t)sent; + errorOk(); +} + +errorret_t networkDgramSocketLinuxReceive( + networkdgramsocketlinux_t *sock, + uint8_t *buffer, + const size_t bufferSize, + size_t *outReceived +) { + assertNotNull(sock, "sock must not be NULL"); + assertNotNull(buffer, "buffer must not be NULL"); + assertNotNull(outReceived, "outReceived must not be NULL"); + + const ssize_t received = recv(sock->fd, buffer, bufferSize, 0); + if(received < 0) { + if(errno == EAGAIN || errno == EWOULDBLOCK) { + *outReceived = 0; + errorOk(); + } + + errorThrow("Failed to receive datagram: %s", strerror(errno)); + } + + *outReceived = (size_t)received; + errorOk(); +} + +errorret_t networkDgramSocketLinuxReceiveFrom( + networkdgramsocketlinux_t *sock, + uint8_t *buffer, + const size_t bufferSize, + size_t *outReceived, + networkaddr_t *outAddr +) { + assertNotNull(sock, "sock must not be NULL"); + assertNotNull(buffer, "buffer must not be NULL"); + assertNotNull(outReceived, "outReceived must not be NULL"); + assertNotNull(outAddr, "outAddr must not be NULL"); + + struct sockaddr_in from; + socklen_t fromLength = sizeof(from); + memoryZero(&from, sizeof(from)); + + const ssize_t received = recvfrom( + sock->fd, buffer, bufferSize, 0, (struct sockaddr *)&from, &fromLength + ); + if(received < 0) { + if(errno == EAGAIN || errno == EWOULDBLOCK) { + *outReceived = 0; + errorOk(); + } + + errorThrow("Failed to receive datagram: %s", strerror(errno)); + } + + memoryCopy(outAddr->ip.ip, &from.sin_addr.s_addr, sizeof(outAddr->ip.ip)); + outAddr->port = ntohs(from.sin_port); + + *outReceived = (size_t)received; + errorOk(); +} + +void networkDgramSocketLinuxClose(networkdgramsocketlinux_t *sock) { + assertNotNull(sock, "sock must not be NULL"); + if(sock->fd >= 0) close(sock->fd); + sock->fd = -1; +} diff --git a/src/dusklinux/network/networkdgramsocketlinux.h b/src/dusklinux/network/networkdgramsocketlinux.h new file mode 100644 index 00000000..8ff4e8ba --- /dev/null +++ b/src/dusklinux/network/networkdgramsocketlinux.h @@ -0,0 +1,135 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "network/networkaddr.h" + +#define NETWORK_DGRAM_SOCKET_LINUX_RECEIVE_TIMEOUT_MS 100 + +typedef struct { + int_t fd; +} networkdgramsocketlinux_t; + +/** + * Creates the underlying UDP socket, unbound and unconnected. Also sets + * a short receive timeout so callers can poll a stop condition between + * blocking receive calls. + * + * @param sock The socket structure to initialize. + * @return An error if the socket could not be created. + */ +errorret_t networkDgramSocketLinuxOpen(networkdgramsocketlinux_t *sock); + +/** + * Binds the socket to a local port, for accepting datagrams from any + * peer (server usage). + * + * @param sock The socket to bind. + * @param port The local port to bind to. + * @return An error if the bind failed. + */ +errorret_t networkDgramSocketLinuxBind( + networkdgramsocketlinux_t *sock, + const uint16_t port +); + +/** + * Resolves host and sets it as the socket's default peer, enabling + * networkDgramSocketLinuxSend/Receive (client usage). + * + * @param sock The socket to connect. + * @param host The hostname or IP address of the peer. + * @param port The port of the peer. + * @return An error if resolution failed. + */ +errorret_t networkDgramSocketLinuxConnect( + networkdgramsocketlinux_t *sock, + const char_t *host, + const uint16_t port +); + +/** + * Sends a datagram to the socket's connected peer. + * + * @param sock The connected socket. + * @param data The data to send. + * @param length The number of bytes in data. + * @param outSent The number of bytes actually sent is written here. + * @return An error if the send failed. + */ +errorret_t networkDgramSocketLinuxSend( + networkdgramsocketlinux_t *sock, + const uint8_t *data, + const size_t length, + size_t *outSent +); + +/** + * Sends a datagram to a specific address, regardless of the socket's + * connected peer (server usage). + * + * @param sock The bound socket. + * @param data The data to send. + * @param length The number of bytes in data. + * @param addr The destination address. + * @param outSent The number of bytes actually sent is written here. + * @return An error if the send failed. + */ +errorret_t networkDgramSocketLinuxSendTo( + networkdgramsocketlinux_t *sock, + const uint8_t *data, + const size_t length, + const networkaddr_t *addr, + size_t *outSent +); + +/** + * Receives a datagram from the socket's connected peer. Blocks for up to + * NETWORK_DGRAM_SOCKET_LINUX_RECEIVE_TIMEOUT_MS; if nothing arrives in + * that window, outReceived is set to 0 without an error. + * + * @param sock The connected socket. + * @param buffer The buffer to receive into. + * @param bufferSize The size of buffer. + * @param outReceived The number of bytes actually received is written + * here; 0 means nothing arrived within the timeout. + * @return An error if the receive failed. + */ +errorret_t networkDgramSocketLinuxReceive( + networkdgramsocketlinux_t *sock, + uint8_t *buffer, + const size_t bufferSize, + size_t *outReceived +); + +/** + * Receives a datagram from any sender, capturing its address. Same + * timeout behavior as networkDgramSocketLinuxReceive. + * + * @param sock The bound socket. + * @param buffer The buffer to receive into. + * @param bufferSize The size of buffer. + * @param outReceived The number of bytes actually received is written + * here; 0 means nothing arrived within the timeout. + * @param outAddr The sender's address is written here. + * @return An error if the receive failed. + */ +errorret_t networkDgramSocketLinuxReceiveFrom( + networkdgramsocketlinux_t *sock, + uint8_t *buffer, + const size_t bufferSize, + size_t *outReceived, + networkaddr_t *outAddr +); + +/** + * Closes the socket. + * + * @param sock The socket to close. + */ +void networkDgramSocketLinuxClose(networkdgramsocketlinux_t *sock); diff --git a/src/dusklinux/network/networkdgramsocketplatform.h b/src/dusklinux/network/networkdgramsocketplatform.h new file mode 100644 index 00000000..61ab5ea1 --- /dev/null +++ b/src/dusklinux/network/networkdgramsocketplatform.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "networkdgramsocketlinux.h" + +typedef networkdgramsocketlinux_t networkdgramsocketplatform_t; + +#define networkDgramSocketPlatformOpen networkDgramSocketLinuxOpen +#define networkDgramSocketPlatformBind networkDgramSocketLinuxBind +#define networkDgramSocketPlatformConnect networkDgramSocketLinuxConnect +#define networkDgramSocketPlatformSend networkDgramSocketLinuxSend +#define networkDgramSocketPlatformSendTo networkDgramSocketLinuxSendTo +#define networkDgramSocketPlatformReceive networkDgramSocketLinuxReceive +#define networkDgramSocketPlatformReceiveFrom networkDgramSocketLinuxReceiveFrom +#define networkDgramSocketPlatformClose networkDgramSocketLinuxClose diff --git a/test/network/CMakeLists.txt b/test/network/CMakeLists.txt index e138f72b..2ff75944 100644 --- a/test/network/CMakeLists.txt +++ b/test/network/CMakeLists.txt @@ -3,4 +3,9 @@ # This software is released under the MIT License. # https://opensource.org/licenses/MIT +include(dusktest) + +dusktest(test_network.c) + add_subdirectory(http) +add_subdirectory(socket) diff --git a/test/network/socket/CMakeLists.txt b/test/network/socket/CMakeLists.txt new file mode 100644 index 00000000..583f146e --- /dev/null +++ b/test/network/socket/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +include(dusktest) + +dusktest(test_networksocket.c) +dusktest(test_serveroffline.c) diff --git a/test/network/socket/test_networksocket.c b/test/network/socket/test_networksocket.c new file mode 100644 index 00000000..ffd07978 --- /dev/null +++ b/test/network/socket/test_networksocket.c @@ -0,0 +1,258 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "dusktest.h" +#include "network/socket/client/client.h" +#include "network/socket/client/clientroster.h" +#include "network/socket/server/serveronline.h" +#include "network/socket/payload/packetplayerstate.h" +#include "util/memory.h" +#include "util/string.h" +#include "time/time.h" +#include + +#define TEST_PORT 58211 + +typedef struct { + bool_t connected; + bool_t rejected; + bool_t disconnected; +} clientevents_t; + +static void onConnected(void *user) { + ((clientevents_t *)user)->connected = true; +} + +static void onRejected( + const packethandshakerejectreason_t reason, + void *user +) { + (void)reason; + ((clientevents_t *)user)->rejected = true; +} + +static void onDisconnected(errorret_t error, void *user) { + errorCatch(error); + ((clientevents_t *)user)->disconnected = true; +} + +static void pumpServerAndClients( + localclient_t *clients[], + const uint32_t clientCount, + const int32_t ms +) { + for(int32_t i = 0; i < ms; i++) { + for(uint32_t c = 0; c < clientCount; c++) { + errorCatch(localClientUpdate(clients[c])); + } + errorCatch(serverOnlineUpdate()); + usleep(1000); + } +} + +static uint32_t countServerRemotes(void) { + uint32_t count = 0; + for(uint32_t i = 0; i < SERVER_CLIENT_COUNT_MAX; i++) { + if(SERVER_ONLINE.clients[i].type == SERVER_CLIENT_TYPE_REMOTE) count++; + } + return count; +} + +static int socket_setup(void **state) { + clientRosterInit(); + return 0; +} + +static int socket_teardown(void **state) { + if(SERVER_ONLINE.running) errorCatch(serverOnlineStop()); + clientRosterInit(); + return 0; +} + +static void test_host_connects_local_client_to_itself(void **state) { + assert_true(errorIsOk(serverOnlineHost(TEST_PORT))); + + client_t *local = clientRosterClaim(CLIENT_TYPE_LOCAL); + assert_non_null(local); + + clientevents_t events; + memoryZero(&events, sizeof(events)); + + const errorret_t ret = localClientConnect( + &local->local, "127.0.0.1", TEST_PORT, "host", + onConnected, onRejected, onDisconnected, &events + ); + assert_true(errorIsOk(ret)); + + localclient_t *clients[] = { &local->local }; + pumpServerAndClients(clients, 1, 2000); + + assert_true(events.connected); + assert_false(events.rejected); + assert_int_equal(local->local.state, LOCAL_CLIENT_STATE_CONNECTED); + + assert_int_equal(countServerRemotes(), 1); + + serverclient_t *remote = NULL; + for(uint32_t i = 0; i < SERVER_CLIENT_COUNT_MAX; i++) { + if(SERVER_ONLINE.clients[i].type == SERVER_CLIENT_TYPE_REMOTE) { + remote = &SERVER_ONLINE.clients[i]; + } + } + assert_non_null(remote); + assert_true(uuidEquals(&remote->remote.id, &local->local.id)); + assert_string_equal(remote->remote.username, "host"); + + localClientDisconnect(&local->local); + clientRosterRemove(local); + + pumpServerAndClients(NULL, 0, 200); + assert_int_equal(countServerRemotes(), 0); + + errorCatch(serverOnlineStop()); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +// Regression test: a quiet connection (no state/chat traffic) must not +// falsely time itself out. The server has to keep sending the client +// something (a PING) on its own idle timer -- otherwise the client only +// ever hears from the server once, at handshake, and its own 10s +// no-packets-received timeout eventually fires even though the +// connection is healthy. +static void test_server_keepalive_prevents_client_timeout(void **state) { + assert_true(errorIsOk(serverOnlineHost(TEST_PORT))); + + client_t *local = clientRosterClaim(CLIENT_TYPE_LOCAL); + assert_non_null(local); + + clientevents_t events; + memoryZero(&events, sizeof(events)); + + assert_true(errorIsOk(localClientConnect( + &local->local, "127.0.0.1", TEST_PORT, "host", + onConnected, onRejected, onDisconnected, &events + ))); + + localclient_t *clients[] = { &local->local }; + pumpServerAndClients(clients, 1, 500); + assert_true(events.connected); + + // Simulate several ping intervals of elapsed game time -- well past + // SERVER_CLIENT_TIMEOUT_SECONDS in total -- jumping the logical clock + // directly (no real 10s wait) but still pumping for real in between + // each jump so any keepalive packets actually go out over the real + // loopback socket and get processed. + for(int32_t i = 0; i < 6; i++) { + TIME.time += 3.0f; + pumpServerAndClients(clients, 1, 100); + } + + assert_false(events.disconnected); + assert_int_equal(local->local.state, LOCAL_CLIENT_STATE_CONNECTED); + assert_int_equal(countServerRemotes(), 1); + + localClientDisconnect(&local->local); + clientRosterRemove(local); + + pumpServerAndClients(NULL, 0, 200); + errorCatch(serverOnlineStop()); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_second_client_joins_and_state_relays(void **state) { + assert_true(errorIsOk(serverOnlineHost(TEST_PORT))); + + client_t *alice = clientRosterClaim(CLIENT_TYPE_LOCAL); + client_t *bob = clientRosterClaim(CLIENT_TYPE_LOCAL); + assert_non_null(alice); + assert_non_null(bob); + + clientevents_t aliceEvents, bobEvents; + memoryZero(&aliceEvents, sizeof(aliceEvents)); + memoryZero(&bobEvents, sizeof(bobEvents)); + + assert_true(errorIsOk(localClientConnect( + &alice->local, "127.0.0.1", TEST_PORT, "alice", + onConnected, onRejected, onDisconnected, &aliceEvents + ))); + + localclient_t *clients[] = { &alice->local, &bob->local }; + pumpServerAndClients(clients, 1, 500); + assert_true(aliceEvents.connected); + + assert_true(errorIsOk(localClientConnect( + &bob->local, "127.0.0.1", TEST_PORT, "bob", + onConnected, onRejected, onDisconnected, &bobEvents + ))); + + pumpServerAndClients(clients, 2, 500); + assert_true(bobEvents.connected); + assert_int_equal(countServerRemotes(), 2); + + // Alice reports a position; the server should track it on her slot and + // relay it to bob (who doesn't know alice's id yet, so his handler + // claims a fresh roster slot for her -- exactly what a real second + // process would do). + packetplayerstate_t playerState; + memoryZero(&playerState, sizeof(playerState)); + playerState.position[0] = 1.0f; + playerState.position[1] = 2.0f; + playerState.position[2] = 3.0f; + + assert_true(errorIsOk(localClientSend( + &alice->local, PACKET_TYPE_PLAYER_STATE, &playerState, false + ))); + + pumpServerAndClients(clients, 2, 500); + + serverclient_t *aliceRemote = NULL; + for(uint32_t i = 0; i < SERVER_CLIENT_COUNT_MAX; i++) { + if( + SERVER_ONLINE.clients[i].type == SERVER_CLIENT_TYPE_REMOTE && + stringEquals(SERVER_ONLINE.clients[i].remote.username, "alice") + ) { + aliceRemote = &SERVER_ONLINE.clients[i]; + } + } + assert_non_null(aliceRemote); + assert_true(aliceRemote->remote.position[0] == 1.0f); + assert_true(aliceRemote->remote.position[1] == 2.0f); + assert_true(aliceRemote->remote.position[2] == 3.0f); + + client_t *bobsViewOfAlice = clientRosterFindById(&alice->local.id); + assert_non_null(bobsViewOfAlice); + assert_true(bobsViewOfAlice->remote.hasState); + assert_true(bobsViewOfAlice->remote.lastPosition[0] == 1.0f); + + localClientDisconnect(&alice->local); + localClientDisconnect(&bob->local); + clientRosterRemove(alice); + clientRosterRemove(bob); + + pumpServerAndClients(NULL, 0, 200); + errorCatch(serverOnlineStop()); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown( + test_host_connects_local_client_to_itself, socket_setup, socket_teardown + ), + cmocka_unit_test_setup_teardown( + test_second_client_joins_and_state_relays, socket_setup, socket_teardown + ), + cmocka_unit_test_setup_teardown( + test_server_keepalive_prevents_client_timeout, + socket_setup, socket_teardown + ), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/test/network/socket/test_serveroffline.c b/test/network/socket/test_serveroffline.c new file mode 100644 index 00000000..e3f308f8 --- /dev/null +++ b/test/network/socket/test_serveroffline.c @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "dusktest.h" +#include "network/socket/client/localclient.h" +#include "network/socket/server/serveroffline.h" +#include "network/socket/payload/packetplayerstate.h" +#include "util/memory.h" +#include "time/time.h" + +typedef struct { + bool_t connected; + bool_t rejected; + bool_t disconnected; +} clientevents_t; + +static void onConnected(void *user) { + ((clientevents_t *)user)->connected = true; +} + +static void onRejected( + const packethandshakerejectreason_t reason, + void *user +) { + (void)reason; + ((clientevents_t *)user)->rejected = true; +} + +static void onDisconnected(errorret_t error, void *user) { + errorCatch(error); + ((clientevents_t *)user)->disconnected = true; +} + +static void pumpOffline(localclient_t *client, const int32_t iterations) { + for(int32_t i = 0; i < iterations; i++) { + errorCatch(serverOfflineUpdate()); + errorCatch(localClientUpdate(client)); + } +} + +static int offline_setup(void **state) { + return 0; +} + +static int offline_teardown(void **state) { + if(SERVER_OFFLINE.running) errorCatch(serverOfflineStop()); + return 0; +} + +static void test_offline_client_connects_without_networking(void **state) { + assert_true(errorIsOk(serverOfflineHost())); + + localclient_t client; + clientevents_t events; + memoryZero(&events, sizeof(events)); + + assert_true(errorIsOk(localClientConnectOffline( + &client, "player", onConnected, onRejected, onDisconnected, &events + ))); + + // Handshake -> accept takes exactly two drain cycles, same as online: + // one for the server to see the HANDSHAKE, one for the client to see + // the HANDSHAKE_ACCEPT. + pumpOffline(&client, 4); + + assert_true(events.connected); + assert_false(events.rejected); + assert_int_equal(client.state, LOCAL_CLIENT_STATE_CONNECTED); + + assert_int_equal(SERVER_OFFLINE.client.type, SERVER_CLIENT_TYPE_LOCAL); + assert_true(uuidEquals(&SERVER_OFFLINE.client.local.id, &client.id)); + + packetplayerstate_t playerState; + memoryZero(&playerState, sizeof(playerState)); + playerState.position[0] = 4.0f; + playerState.position[1] = 5.0f; + playerState.position[2] = 6.0f; + + assert_true(errorIsOk(localClientSend( + &client, PACKET_TYPE_PLAYER_STATE, &playerState, false + ))); + pumpOffline(&client, 2); + + assert_true(SERVER_OFFLINE.client.local.position[0] == 4.0f); + assert_true(SERVER_OFFLINE.client.local.position[1] == 5.0f); + assert_true(SERVER_OFFLINE.client.local.position[2] == 6.0f); + + // Simulate well past LOCAL_CLIENT_TIMEOUT_SECONDS of elapsed game time + // -- the offline server's own idle-ping keepalive must prevent the + // client from falsely timing itself out, same as the online fix. + for(int32_t i = 0; i < 6; i++) { + TIME.time += 3.0f; + pumpOffline(&client, 2); + } + + assert_false(events.disconnected); + assert_int_equal(client.state, LOCAL_CLIENT_STATE_CONNECTED); + + localClientDisconnect(&client); + pumpOffline(&client, 2); + + assert_int_equal(SERVER_OFFLINE.client.type, SERVER_CLIENT_TYPE_NULL); + + errorCatch(serverOfflineStop()); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown( + test_offline_client_connects_without_networking, + offline_setup, offline_teardown + ), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/test/network/test_network.c b/test/network/test_network.c new file mode 100644 index 00000000..bc230f67 --- /dev/null +++ b/test/network/test_network.c @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "dusktest.h" +#include "network/network.h" +#include "network/networkinfo.h" +#include "util/memory.h" + +typedef struct { + bool_t connected; + bool_t failed; +} networkevents_t; + +static void onConnected(void *user) { + ((networkevents_t *)user)->connected = true; +} + +static void onFailed(errorret_t error, void *user) { + errorCatch(error); + ((networkevents_t *)user)->failed = true; +} + +static void onDisconnect(errorret_t error, void *user) { + errorCatch(error); +} + +// Regression test: networkIsConnected() reports physical connectivity and +// is NOT the same thing as NETWORK.state -- networkGetInfo() asserts on +// the latter. Code must gate networkGetInfo() on NETWORK.state, reached +// only via networkRequestConnection(), not on networkIsConnected() alone. +static void test_request_connection_enables_get_info(void **state) { + assert_true(errorIsOk(networkInit())); + + networkevents_t events; + memoryZero(&events, sizeof(events)); + + // Resolves synchronously on Linux (no networkPlatformRequestConnection). + networkRequestConnection(onConnected, onFailed, onDisconnect, &events); + assert_true(events.connected || events.failed); + + if(events.connected) { + assert_int_equal(NETWORK.state, NETWORK_STATE_CONNECTED); + const networkinfo_t info = networkGetInfo(); // must not assert/crash + (void)info; + } else { + assert_int_equal(NETWORK.state, NETWORK_STATE_DISCONNECTED); + } + + assert_true(errorIsOk(networkDispose())); + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_request_connection_enables_get_info), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +}