f8607d114c
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.
122 lines
3.4 KiB
C
122 lines
3.4 KiB
C
/**
|
|
* 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);
|
|
}
|