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:
+3
-2
@@ -33,8 +33,9 @@ for a memory/CPU optimization survey specifically targeting the PSP build
|
|||||||
9. Build the socket server and client implementation, including
|
9. Build the socket server and client implementation, including
|
||||||
handlers for the different packet types.
|
handlers for the different packet types.
|
||||||
10. Add a dedicated multiplayer entity type, `clientplayer`, alongside
|
10. Add a dedicated multiplayer entity type, `clientplayer`, alongside
|
||||||
the existing `npc` and `player` types. Limit to 8 (defined
|
the existing `npc` and `player` types. Limit to 16 (defined
|
||||||
constant) for now.
|
constant, see SERVER_CLIENT_COUNT_MAX/CLIENT_COUNT_MAX in
|
||||||
|
src/dusk/network/socket/) for now.
|
||||||
11. Send and receive `clientplayer` position over the network.
|
11. Send and receive `clientplayer` position over the network.
|
||||||
12. Create a UI menu for creating a server and joining a server. For
|
12. Create a UI menu for creating a server and joining a server. For
|
||||||
now, join IPs are hard-coded (testing against a fixed IP of
|
now, join IPs are hard-coded (testing against a fixed IP of
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
#include "ui/ui.h"
|
#include "ui/ui.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "network/network.h"
|
#include "network/network.h"
|
||||||
|
#include "network/socket/client/client.h"
|
||||||
|
#include "network/socket/client/clientroster.h"
|
||||||
|
#include "network/socket/server/serveronline.h"
|
||||||
#include "game/game.h"
|
#include "game/game.h"
|
||||||
#include "system/system.h"
|
#include "system/system.h"
|
||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
@@ -67,6 +70,8 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
errorret_t engineUpdate(void) {
|
errorret_t engineUpdate(void) {
|
||||||
// Order here is important.
|
// Order here is important.
|
||||||
errorChain(networkUpdate());
|
errorChain(networkUpdate());
|
||||||
|
errorChain(clientUpdate());
|
||||||
|
errorChain(serverOnlineUpdate());
|
||||||
timeUpdate();
|
timeUpdate();
|
||||||
|
|
||||||
const systemdialogtype_t dialogType = systemGetActiveDialogType();
|
const systemdialogtype_t dialogType = systemGetActiveDialogType();
|
||||||
@@ -99,6 +104,11 @@ void engineExit(void) {
|
|||||||
errorret_t engineDispose(void) {
|
errorret_t engineDispose(void) {
|
||||||
errorChain(gameDispose());
|
errorChain(gameDispose());
|
||||||
errorChain(sceneDispose());
|
errorChain(sceneDispose());
|
||||||
|
|
||||||
|
errorChain(serverOnlineStop());
|
||||||
|
client_t *localClient = clientRosterFindLocal();
|
||||||
|
if(localClient) localClientDisconnect(&localClient->local);
|
||||||
|
|
||||||
errorChain(networkDispose());
|
errorChain(networkDispose());
|
||||||
localeManagerDispose();
|
localeManagerDispose();
|
||||||
errorChain(uiDispose());
|
errorChain(uiDispose());
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
PUBLIC
|
PUBLIC
|
||||||
network.c
|
network.c
|
||||||
networkinfo.c
|
networkinfo.c
|
||||||
|
networkaddr.c
|
||||||
)
|
)
|
||||||
|
|
||||||
# Subdirs
|
# Subdirs
|
||||||
add_subdirectory(http)
|
add_subdirectory(http)
|
||||||
|
add_subdirectory(socket)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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)
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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];
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -15,4 +15,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
math.c
|
math.c
|
||||||
sort.c
|
sort.c
|
||||||
ref.c
|
ref.c
|
||||||
|
uuid.c
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uuid.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/random.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
|
||||||
|
const uuid_t UUID_NULL = { .bytes = { 0 } };
|
||||||
|
|
||||||
|
void uuidGenerate(uuid_t *out) {
|
||||||
|
for(uint8_t i = 0; i < UUID_BYTE_COUNT; i++) {
|
||||||
|
out->bytes[i] = (uint8_t)randomInt(0, 256);
|
||||||
|
}
|
||||||
|
|
||||||
|
out->bytes[6] = (uint8_t)((out->bytes[6] & 0x0F) | 0x40);
|
||||||
|
out->bytes[8] = (uint8_t)((out->bytes[8] & 0x3F) | 0x80);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t uuidEquals(const uuid_t *a, const uuid_t *b) {
|
||||||
|
return memoryCompare(a->bytes, b->bytes, UUID_BYTE_COUNT) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uuidToString(const uuid_t *id, char_t *dest, const size_t destSize) {
|
||||||
|
const uint8_t *b = id->bytes;
|
||||||
|
|
||||||
|
stringFormat(
|
||||||
|
dest, destSize,
|
||||||
|
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
||||||
|
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
|
||||||
|
b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -7,4 +7,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
PUBLIC
|
PUBLIC
|
||||||
networklinux.c
|
networklinux.c
|
||||||
networksocketlinux.c
|
networksocketlinux.c
|
||||||
|
networkdgramsocketlinux.c
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "networkdgramsocketlinux.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
errorret_t networkDgramSocketLinuxOpen(networkdgramsocketlinux_t *sock) {
|
||||||
|
assertNotNull(sock, "sock must not be NULL");
|
||||||
|
|
||||||
|
const int_t fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||||
|
if(fd < 0) errorThrow("Failed to create UDP socket: %s", strerror(errno));
|
||||||
|
|
||||||
|
struct timeval timeout;
|
||||||
|
timeout.tv_sec = 0;
|
||||||
|
timeout.tv_usec = NETWORK_DGRAM_SOCKET_LINUX_RECEIVE_TIMEOUT_MS * 1000;
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||||
|
|
||||||
|
sock->fd = fd;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkDgramSocketLinuxBind(
|
||||||
|
networkdgramsocketlinux_t *sock,
|
||||||
|
const uint16_t port
|
||||||
|
) {
|
||||||
|
assertNotNull(sock, "sock must not be NULL");
|
||||||
|
|
||||||
|
struct sockaddr_in addr;
|
||||||
|
memoryZero(&addr, sizeof(addr));
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
addr.sin_addr.s_addr = INADDR_ANY;
|
||||||
|
addr.sin_port = htons(port);
|
||||||
|
|
||||||
|
if(bind(sock->fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
|
||||||
|
errorThrow("Failed to bind UDP socket to port %u: %s",
|
||||||
|
port, strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkDgramSocketLinuxConnect(
|
||||||
|
networkdgramsocketlinux_t *sock,
|
||||||
|
const char_t *host,
|
||||||
|
const uint16_t port
|
||||||
|
) {
|
||||||
|
assertNotNull(sock, "sock must not be NULL");
|
||||||
|
assertNotNull(host, "host must not be NULL");
|
||||||
|
|
||||||
|
char_t portStr[8];
|
||||||
|
stringFormat(portStr, sizeof(portStr) - 1, "%u", port);
|
||||||
|
|
||||||
|
struct addrinfo hints;
|
||||||
|
memoryZero(&hints, sizeof(hints));
|
||||||
|
hints.ai_family = AF_INET;
|
||||||
|
hints.ai_socktype = SOCK_DGRAM;
|
||||||
|
|
||||||
|
struct addrinfo *result = NULL;
|
||||||
|
const int_t gaiRet = getaddrinfo(host, portStr, &hints, &result);
|
||||||
|
if(gaiRet != 0) {
|
||||||
|
errorThrow("Failed to resolve host %s: %s", host, gai_strerror(gaiRet));
|
||||||
|
}
|
||||||
|
|
||||||
|
const int_t connectRet = connect(
|
||||||
|
sock->fd, result->ai_addr, result->ai_addrlen
|
||||||
|
);
|
||||||
|
freeaddrinfo(result);
|
||||||
|
|
||||||
|
if(connectRet != 0) {
|
||||||
|
errorThrow("Failed to connect UDP socket to %s:%u: %s",
|
||||||
|
host, port, strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkDgramSocketLinuxSend(
|
||||||
|
networkdgramsocketlinux_t *sock,
|
||||||
|
const uint8_t *data,
|
||||||
|
const size_t length,
|
||||||
|
size_t *outSent
|
||||||
|
) {
|
||||||
|
assertNotNull(sock, "sock must not be NULL");
|
||||||
|
assertNotNull(data, "data must not be NULL");
|
||||||
|
assertNotNull(outSent, "outSent must not be NULL");
|
||||||
|
|
||||||
|
const ssize_t sent = send(sock->fd, data, length, 0);
|
||||||
|
if(sent < 0) errorThrow("Failed to send datagram: %s", strerror(errno));
|
||||||
|
|
||||||
|
*outSent = (size_t)sent;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkDgramSocketLinuxSendTo(
|
||||||
|
networkdgramsocketlinux_t *sock,
|
||||||
|
const uint8_t *data,
|
||||||
|
const size_t length,
|
||||||
|
const networkaddr_t *addr,
|
||||||
|
size_t *outSent
|
||||||
|
) {
|
||||||
|
assertNotNull(sock, "sock must not be NULL");
|
||||||
|
assertNotNull(data, "data must not be NULL");
|
||||||
|
assertNotNull(addr, "addr must not be NULL");
|
||||||
|
assertNotNull(outSent, "outSent must not be NULL");
|
||||||
|
|
||||||
|
struct sockaddr_in dest;
|
||||||
|
memoryZero(&dest, sizeof(dest));
|
||||||
|
dest.sin_family = AF_INET;
|
||||||
|
memoryCopy(&dest.sin_addr.s_addr, addr->ip.ip, sizeof(dest.sin_addr.s_addr));
|
||||||
|
dest.sin_port = htons(addr->port);
|
||||||
|
|
||||||
|
const ssize_t sent = sendto(
|
||||||
|
sock->fd, data, length, 0, (struct sockaddr *)&dest, sizeof(dest)
|
||||||
|
);
|
||||||
|
if(sent < 0) errorThrow("Failed to send datagram: %s", strerror(errno));
|
||||||
|
|
||||||
|
*outSent = (size_t)sent;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkDgramSocketLinuxReceive(
|
||||||
|
networkdgramsocketlinux_t *sock,
|
||||||
|
uint8_t *buffer,
|
||||||
|
const size_t bufferSize,
|
||||||
|
size_t *outReceived
|
||||||
|
) {
|
||||||
|
assertNotNull(sock, "sock must not be NULL");
|
||||||
|
assertNotNull(buffer, "buffer must not be NULL");
|
||||||
|
assertNotNull(outReceived, "outReceived must not be NULL");
|
||||||
|
|
||||||
|
const ssize_t received = recv(sock->fd, buffer, bufferSize, 0);
|
||||||
|
if(received < 0) {
|
||||||
|
if(errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||||
|
*outReceived = 0;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorThrow("Failed to receive datagram: %s", strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
|
*outReceived = (size_t)received;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkDgramSocketLinuxReceiveFrom(
|
||||||
|
networkdgramsocketlinux_t *sock,
|
||||||
|
uint8_t *buffer,
|
||||||
|
const size_t bufferSize,
|
||||||
|
size_t *outReceived,
|
||||||
|
networkaddr_t *outAddr
|
||||||
|
) {
|
||||||
|
assertNotNull(sock, "sock must not be NULL");
|
||||||
|
assertNotNull(buffer, "buffer must not be NULL");
|
||||||
|
assertNotNull(outReceived, "outReceived must not be NULL");
|
||||||
|
assertNotNull(outAddr, "outAddr must not be NULL");
|
||||||
|
|
||||||
|
struct sockaddr_in from;
|
||||||
|
socklen_t fromLength = sizeof(from);
|
||||||
|
memoryZero(&from, sizeof(from));
|
||||||
|
|
||||||
|
const ssize_t received = recvfrom(
|
||||||
|
sock->fd, buffer, bufferSize, 0, (struct sockaddr *)&from, &fromLength
|
||||||
|
);
|
||||||
|
if(received < 0) {
|
||||||
|
if(errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||||
|
*outReceived = 0;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorThrow("Failed to receive datagram: %s", strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
|
memoryCopy(outAddr->ip.ip, &from.sin_addr.s_addr, sizeof(outAddr->ip.ip));
|
||||||
|
outAddr->port = ntohs(from.sin_port);
|
||||||
|
|
||||||
|
*outReceived = (size_t)received;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void networkDgramSocketLinuxClose(networkdgramsocketlinux_t *sock) {
|
||||||
|
assertNotNull(sock, "sock must not be NULL");
|
||||||
|
if(sock->fd >= 0) close(sock->fd);
|
||||||
|
sock->fd = -1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
#include "network/networkaddr.h"
|
||||||
|
|
||||||
|
#define NETWORK_DGRAM_SOCKET_LINUX_RECEIVE_TIMEOUT_MS 100
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int_t fd;
|
||||||
|
} networkdgramsocketlinux_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the underlying UDP socket, unbound and unconnected. Also sets
|
||||||
|
* a short receive timeout so callers can poll a stop condition between
|
||||||
|
* blocking receive calls.
|
||||||
|
*
|
||||||
|
* @param sock The socket structure to initialize.
|
||||||
|
* @return An error if the socket could not be created.
|
||||||
|
*/
|
||||||
|
errorret_t networkDgramSocketLinuxOpen(networkdgramsocketlinux_t *sock);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds the socket to a local port, for accepting datagrams from any
|
||||||
|
* peer (server usage).
|
||||||
|
*
|
||||||
|
* @param sock The socket to bind.
|
||||||
|
* @param port The local port to bind to.
|
||||||
|
* @return An error if the bind failed.
|
||||||
|
*/
|
||||||
|
errorret_t networkDgramSocketLinuxBind(
|
||||||
|
networkdgramsocketlinux_t *sock,
|
||||||
|
const uint16_t port
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves host and sets it as the socket's default peer, enabling
|
||||||
|
* networkDgramSocketLinuxSend/Receive (client usage).
|
||||||
|
*
|
||||||
|
* @param sock The socket to connect.
|
||||||
|
* @param host The hostname or IP address of the peer.
|
||||||
|
* @param port The port of the peer.
|
||||||
|
* @return An error if resolution failed.
|
||||||
|
*/
|
||||||
|
errorret_t networkDgramSocketLinuxConnect(
|
||||||
|
networkdgramsocketlinux_t *sock,
|
||||||
|
const char_t *host,
|
||||||
|
const uint16_t port
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a datagram to the socket's connected peer.
|
||||||
|
*
|
||||||
|
* @param sock The connected socket.
|
||||||
|
* @param data The data to send.
|
||||||
|
* @param length The number of bytes in data.
|
||||||
|
* @param outSent The number of bytes actually sent is written here.
|
||||||
|
* @return An error if the send failed.
|
||||||
|
*/
|
||||||
|
errorret_t networkDgramSocketLinuxSend(
|
||||||
|
networkdgramsocketlinux_t *sock,
|
||||||
|
const uint8_t *data,
|
||||||
|
const size_t length,
|
||||||
|
size_t *outSent
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a datagram to a specific address, regardless of the socket's
|
||||||
|
* connected peer (server usage).
|
||||||
|
*
|
||||||
|
* @param sock The bound socket.
|
||||||
|
* @param data The data to send.
|
||||||
|
* @param length The number of bytes in data.
|
||||||
|
* @param addr The destination address.
|
||||||
|
* @param outSent The number of bytes actually sent is written here.
|
||||||
|
* @return An error if the send failed.
|
||||||
|
*/
|
||||||
|
errorret_t networkDgramSocketLinuxSendTo(
|
||||||
|
networkdgramsocketlinux_t *sock,
|
||||||
|
const uint8_t *data,
|
||||||
|
const size_t length,
|
||||||
|
const networkaddr_t *addr,
|
||||||
|
size_t *outSent
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives a datagram from the socket's connected peer. Blocks for up to
|
||||||
|
* NETWORK_DGRAM_SOCKET_LINUX_RECEIVE_TIMEOUT_MS; if nothing arrives in
|
||||||
|
* that window, outReceived is set to 0 without an error.
|
||||||
|
*
|
||||||
|
* @param sock The connected socket.
|
||||||
|
* @param buffer The buffer to receive into.
|
||||||
|
* @param bufferSize The size of buffer.
|
||||||
|
* @param outReceived The number of bytes actually received is written
|
||||||
|
* here; 0 means nothing arrived within the timeout.
|
||||||
|
* @return An error if the receive failed.
|
||||||
|
*/
|
||||||
|
errorret_t networkDgramSocketLinuxReceive(
|
||||||
|
networkdgramsocketlinux_t *sock,
|
||||||
|
uint8_t *buffer,
|
||||||
|
const size_t bufferSize,
|
||||||
|
size_t *outReceived
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives a datagram from any sender, capturing its address. Same
|
||||||
|
* timeout behavior as networkDgramSocketLinuxReceive.
|
||||||
|
*
|
||||||
|
* @param sock The bound socket.
|
||||||
|
* @param buffer The buffer to receive into.
|
||||||
|
* @param bufferSize The size of buffer.
|
||||||
|
* @param outReceived The number of bytes actually received is written
|
||||||
|
* here; 0 means nothing arrived within the timeout.
|
||||||
|
* @param outAddr The sender's address is written here.
|
||||||
|
* @return An error if the receive failed.
|
||||||
|
*/
|
||||||
|
errorret_t networkDgramSocketLinuxReceiveFrom(
|
||||||
|
networkdgramsocketlinux_t *sock,
|
||||||
|
uint8_t *buffer,
|
||||||
|
const size_t bufferSize,
|
||||||
|
size_t *outReceived,
|
||||||
|
networkaddr_t *outAddr
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the socket.
|
||||||
|
*
|
||||||
|
* @param sock The socket to close.
|
||||||
|
*/
|
||||||
|
void networkDgramSocketLinuxClose(networkdgramsocketlinux_t *sock);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "networkdgramsocketlinux.h"
|
||||||
|
|
||||||
|
typedef networkdgramsocketlinux_t networkdgramsocketplatform_t;
|
||||||
|
|
||||||
|
#define networkDgramSocketPlatformOpen networkDgramSocketLinuxOpen
|
||||||
|
#define networkDgramSocketPlatformBind networkDgramSocketLinuxBind
|
||||||
|
#define networkDgramSocketPlatformConnect networkDgramSocketLinuxConnect
|
||||||
|
#define networkDgramSocketPlatformSend networkDgramSocketLinuxSend
|
||||||
|
#define networkDgramSocketPlatformSendTo networkDgramSocketLinuxSendTo
|
||||||
|
#define networkDgramSocketPlatformReceive networkDgramSocketLinuxReceive
|
||||||
|
#define networkDgramSocketPlatformReceiveFrom networkDgramSocketLinuxReceiveFrom
|
||||||
|
#define networkDgramSocketPlatformClose networkDgramSocketLinuxClose
|
||||||
@@ -3,4 +3,9 @@
|
|||||||
# This software is released under the MIT License.
|
# This software is released under the MIT License.
|
||||||
# https://opensource.org/licenses/MIT
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
include(dusktest)
|
||||||
|
|
||||||
|
dusktest(test_network.c)
|
||||||
|
|
||||||
add_subdirectory(http)
|
add_subdirectory(http)
|
||||||
|
add_subdirectory(socket)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user