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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user