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.
This commit is contained in:
2026-08-02 21:06:23 -05:00
parent 56230dd340
commit f8607d114c
90 changed files with 4247 additions and 2 deletions
+3 -2
View File
@@ -33,8 +33,9 @@ for a memory/CPU optimization survey specifically targeting the PSP build
9. Build the socket server and client implementation, including 9. Build the socket server and client implementation, including
handlers for the different packet types. handlers for the different packet types.
10. Add a dedicated multiplayer entity type, `clientplayer`, alongside 10. Add a dedicated multiplayer entity type, `clientplayer`, alongside
the existing `npc` and `player` types. Limit to 8 (defined the existing `npc` and `player` types. Limit to 16 (defined
constant) for now. 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. 11. Send and receive `clientplayer` position over the network.
12. Create a UI menu for creating a server and joining a server. For 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 now, join IPs are hard-coded (testing against a fixed IP of
+10
View File
@@ -18,6 +18,9 @@
#include "ui/ui.h" #include "ui/ui.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "network/network.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 "game/game.h"
#include "system/system.h" #include "system/system.h"
#include "console/console.h" #include "console/console.h"
@@ -67,6 +70,8 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorret_t engineUpdate(void) { errorret_t engineUpdate(void) {
// Order here is important. // Order here is important.
errorChain(networkUpdate()); errorChain(networkUpdate());
errorChain(clientUpdate());
errorChain(serverOnlineUpdate());
timeUpdate(); timeUpdate();
const systemdialogtype_t dialogType = systemGetActiveDialogType(); const systemdialogtype_t dialogType = systemGetActiveDialogType();
@@ -99,6 +104,11 @@ void engineExit(void) {
errorret_t engineDispose(void) { errorret_t engineDispose(void) {
errorChain(gameDispose()); errorChain(gameDispose());
errorChain(sceneDispose()); errorChain(sceneDispose());
errorChain(serverOnlineStop());
client_t *localClient = clientRosterFindLocal();
if(localClient) localClientDisconnect(&localClient->local);
errorChain(networkDispose()); errorChain(networkDispose());
localeManagerDispose(); localeManagerDispose();
errorChain(uiDispose()); errorChain(uiDispose());
+2
View File
@@ -7,7 +7,9 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
network.c network.c
networkinfo.c networkinfo.c
networkaddr.c
) )
# Subdirs # Subdirs
add_subdirectory(http) add_subdirectory(http)
add_subdirectory(socket)
+16
View File
@@ -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;
}
+23
View File
@@ -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);
+16
View File
@@ -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)
@@ -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)
+16
View File
@@ -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);
}
+34
View File
@@ -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);
@@ -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;
}
@@ -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);
@@ -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);
}
@@ -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);
@@ -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
)
@@ -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();
}
@@ -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
);
@@ -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();
}
@@ -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
);
@@ -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();
}
@@ -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
);
@@ -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();
}
@@ -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
);
@@ -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,
};
@@ -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];
@@ -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();
}
@@ -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
);
@@ -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();
}
@@ -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
);
@@ -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();
}
@@ -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
);
@@ -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 <cglm/vec3.h>
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();
}
@@ -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
);
@@ -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;
}
@@ -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);
@@ -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 <cglm/types.h>
typedef struct {
uuid_t id;
char_t username[PACKET_HANDSHAKE_USERNAME_MAX];
vec3 lastPosition;
vec3 lastRotation;
bool_t hasState;
uint32_t lastStateSequence;
} remoteclient_t;
+139
View File
@@ -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();
}
+89
View File
@@ -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
);
+29
View File
@@ -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;
+53
View File
@@ -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);
}
+58
View File
@@ -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);
+44
View File
@@ -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
},
};
+17
View File
@@ -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];
+118
View File
@@ -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);
}
+119
View File
@@ -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);
+24
View File
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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 <cglm/types.h>
typedef struct {
uuid_t id;
vec3 position;
vec3 rotation;
} packetplayerstate_t;
@@ -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)
@@ -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
)
@@ -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();
}
@@ -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
);
@@ -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();
}
@@ -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
);
@@ -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();
}
@@ -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
);
@@ -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,
};
@@ -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];
@@ -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();
}
@@ -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
);
@@ -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 <cglm/vec3.h>
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
);
}
@@ -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
);
@@ -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));
}
@@ -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
);
@@ -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 <cglm/types.h>
typedef struct {
uuid_t id;
vec3 position;
vec3 rotation;
} serverclientlocal_t;
@@ -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 <cglm/types.h>
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;
@@ -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 <cglm/vec3.h>
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();
}
@@ -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);
@@ -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();
}
@@ -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
);
@@ -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);
}
@@ -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);
+1
View File
@@ -15,4 +15,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
math.c math.c
sort.c sort.c
ref.c ref.c
uuid.c
) )
+37
View File
@@ -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]
);
}
+45
View File
@@ -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);
+1
View File
@@ -7,4 +7,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
networklinux.c networklinux.c
networksocketlinux.c networksocketlinux.c
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 <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
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;
}
@@ -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);
@@ -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
+5
View File
@@ -3,4 +3,9 @@
# This software is released under the MIT License. # This software is released under the MIT License.
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
include(dusktest)
dusktest(test_network.c)
add_subdirectory(http) add_subdirectory(http)
add_subdirectory(socket)
+9
View File
@@ -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)
+258
View File
@@ -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 <unistd.h>
#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);
}
+121
View File
@@ -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);
}
+62
View File
@@ -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);
}