/** * 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); }