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
handlers for the different packet types.
10. Add a dedicated multiplayer entity type, `clientplayer`, alongside
the existing `npc` and `player` types. Limit to 8 (defined
constant) for now.
the existing `npc` and `player` types. Limit to 16 (defined
constant, see SERVER_CLIENT_COUNT_MAX/CLIENT_COUNT_MAX in
src/dusk/network/socket/) for now.
11. Send and receive `clientplayer` position over the network.
12. Create a UI menu for creating a server and joining a server. For
now, join IPs are hard-coded (testing against a fixed IP of
+10
View File
@@ -18,6 +18,9 @@
#include "ui/ui.h"
#include "assert/assert.h"
#include "network/network.h"
#include "network/socket/client/client.h"
#include "network/socket/client/clientroster.h"
#include "network/socket/server/serveronline.h"
#include "game/game.h"
#include "system/system.h"
#include "console/console.h"
@@ -67,6 +70,8 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorret_t engineUpdate(void) {
// Order here is important.
errorChain(networkUpdate());
errorChain(clientUpdate());
errorChain(serverOnlineUpdate());
timeUpdate();
const systemdialogtype_t dialogType = systemGetActiveDialogType();
@@ -99,6 +104,11 @@ void engineExit(void) {
errorret_t engineDispose(void) {
errorChain(gameDispose());
errorChain(sceneDispose());
errorChain(serverOnlineStop());
client_t *localClient = clientRosterFindLocal();
if(localClient) localClientDisconnect(&localClient->local);
errorChain(networkDispose());
localeManagerDispose();
errorChain(uiDispose());
+2
View File
@@ -7,7 +7,9 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
network.c
networkinfo.c
networkaddr.c
)
# Subdirs
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
sort.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
networklinux.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.
# https://opensource.org/licenses/MIT
include(dusktest)
dusktest(test_network.c)
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);
}