From f071dcc80d434d38ad32876b22c3876e4d9747bc Mon Sep 17 00:00:00 2001 From: Glenn Fiedler Date: Thu, 9 Jul 2026 19:40:14 -0400 Subject: [PATCH] Add per-client slot disconnect reason to the server The connect_disconnect_callback only reports (client_index, connected), so server code could not tell why a client disconnected: did it leave cleanly (disconnect packet), time out, or get disconnected by server code calling netcode_server_disconnect_client? New function int netcode_server_client_disconnect_reason( server, client_index ) returns NETCODE_SERVER_CLIENT_DISCONNECT_REASON_NONE / TIMED_OUT / CLIENT_DISCONNECT / SERVER_DISCONNECT for the client that last occupied the slot. Tracked per-client slot: reset to NONE at server start and when a new client connects to the slot, and recorded before the connect_disconnect_callback fires, so the reason can be queried from inside that callback. Existing functions are unchanged. Tests: new test_server_client_disconnect_reason covers none -> timed out -> cleared on reconnect, and the existing client-side and server-side disconnect tests now also assert the recorded reason, directly verifying their stated intent ("disconnects cleanly, rather than timing out"). Co-Authored-By: Claude Fable 5 --- netcode.c | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- netcode.h | 11 ++++ 2 files changed, 178 insertions(+), 6 deletions(-) diff --git a/netcode.c b/netcode.c index 483ccb9..b257255 100755 --- a/netcode.c +++ b/netcode.c @@ -3868,6 +3868,7 @@ struct netcode_server_t int client_timeout[NETCODE_MAX_CLIENTS]; int client_loopback[NETCODE_MAX_CLIENTS]; int client_confirmed[NETCODE_MAX_CLIENTS]; + int client_disconnect_reason[NETCODE_MAX_CLIENTS]; int client_encryption_index[NETCODE_MAX_CLIENTS]; uint64_t client_id[NETCODE_MAX_CLIENTS]; uint64_t client_sequence[NETCODE_MAX_CLIENTS]; @@ -4101,6 +4102,11 @@ void netcode_server_start( struct netcode_server_t * server, int max_clients ) { netcode_packet_queue_init( &server->client_packet_queue[i], server->config.allocator_context, server->config.allocate_function, server->config.free_function ); } + + for ( i = 0; i < NETCODE_MAX_CLIENTS; i++ ) + { + server->client_disconnect_reason[i] = NETCODE_SERVER_CLIENT_DISCONNECT_REASON_NONE; + } } void netcode_server_send_global_packet( struct netcode_server_t * server, void * packet, struct netcode_address_t * to, uint8_t * packet_key ) @@ -4196,7 +4202,7 @@ static void netcode_server_reset_client_slot( struct netcode_server_t * server, netcode_assert( server->num_connected_clients >= 0 ); } -void netcode_server_disconnect_client_internal( struct netcode_server_t * server, int client_index, int send_disconnect_packets ) +void netcode_server_disconnect_client_internal( struct netcode_server_t * server, int client_index, int send_disconnect_packets, int disconnect_reason ) { netcode_assert( server ); netcode_assert( server->running ); @@ -4208,6 +4214,10 @@ void netcode_server_disconnect_client_internal( struct netcode_server_t * server netcode_printf( NETCODE_LOG_LEVEL_INFO, "server disconnected client %d\n", client_index ); + // record why before the callback fires, so the reason can be queried from inside the callback + + server->client_disconnect_reason[client_index] = disconnect_reason; + if ( server->config.connect_disconnect_callback ) { server->config.connect_disconnect_callback( server->config.callback_context, client_index, 0 ); @@ -4259,7 +4269,7 @@ void netcode_server_disconnect_client( struct netcode_server_t * server, int cli if ( server->client_loopback[client_index] ) return; - netcode_server_disconnect_client_internal( server, client_index, 1 ); + netcode_server_disconnect_client_internal( server, client_index, 1, NETCODE_SERVER_CLIENT_DISCONNECT_REASON_SERVER_DISCONNECT ); } void netcode_server_disconnect_all_clients( struct netcode_server_t * server ) @@ -4274,7 +4284,7 @@ void netcode_server_disconnect_all_clients( struct netcode_server_t * server ) { if ( server->client_connected[i] && !server->client_loopback[i] ) { - netcode_server_disconnect_client_internal( server, i, 1 ); + netcode_server_disconnect_client_internal( server, i, 1, NETCODE_SERVER_CLIENT_DISCONNECT_REASON_SERVER_DISCONNECT ); } } } @@ -4495,7 +4505,8 @@ void netcode_server_connect_client( struct netcode_server_t * server, server->client_id[client_index] = client_id; server->client_sequence[client_index] = 0; server->client_address[client_index] = *address; - + server->client_disconnect_reason[client_index] = NETCODE_SERVER_CLIENT_DISCONNECT_REASON_NONE; + netcode_assert( netcode_server_find_client_index_by_id( server, client_id ) == client_index ); netcode_assert( netcode_server_find_client_index_by_address( server, address ) == client_index ); @@ -4661,7 +4672,7 @@ void netcode_server_process_packet_internal( struct netcode_server_t * server, if ( client_index != -1 ) { netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server received disconnect packet from client %d\n", client_index ); - netcode_server_disconnect_client_internal( server, client_index, 0 ); + netcode_server_disconnect_client_internal( server, client_index, 0, NETCODE_SERVER_CLIENT_DISCONNECT_REASON_CLIENT_DISCONNECT ); } } break; @@ -4872,7 +4883,7 @@ void netcode_server_check_for_timeouts( struct netcode_server_t * server ) if ( server->client_last_packet_receive_time[i] + server->client_timeout[i] <= server->time ) { netcode_printf( NETCODE_LOG_LEVEL_INFO, "server timed out client %d\n", i ); - netcode_server_disconnect_client_internal( server, i, 0 ); + netcode_server_disconnect_client_internal( server, i, 0, NETCODE_SERVER_CLIENT_DISCONNECT_REASON_TIMED_OUT ); } } } @@ -4890,6 +4901,19 @@ int netcode_server_client_connected( struct netcode_server_t * server, int clien return server->client_connected[client_index]; } +int netcode_server_client_disconnect_reason( struct netcode_server_t * server, int client_index ) +{ + netcode_assert( server ); + + if ( !server->running ) + return NETCODE_SERVER_CLIENT_DISCONNECT_REASON_NONE; + + if ( client_index < 0 || client_index >= server->max_clients ) + return NETCODE_SERVER_CLIENT_DISCONNECT_REASON_NONE; + + return server->client_disconnect_reason[client_index]; +} + uint64_t netcode_server_client_id( struct netcode_server_t * server, int client_index ) { netcode_assert( server ); @@ -5102,6 +5126,7 @@ void netcode_server_connect_loopback_client( struct netcode_server_t * server, i server->client_encryption_index[client_index] = -1; server->client_id[client_index] = client_id; server->client_sequence[client_index] = 0; + server->client_disconnect_reason[client_index] = NETCODE_SERVER_CLIENT_DISCONNECT_REASON_NONE; memset( &server->client_address[client_index], 0, sizeof( struct netcode_address_t ) ); server->client_last_packet_send_time[client_index] = server->time; server->client_last_packet_receive_time[client_index] = server->time; @@ -5144,6 +5169,8 @@ void netcode_server_disconnect_loopback_client( struct netcode_server_t * server netcode_printf( NETCODE_LOG_LEVEL_INFO, "server disconnected loopback client %d\n", client_index ); + server->client_disconnect_reason[client_index] = NETCODE_SERVER_CLIENT_DISCONNECT_REASON_SERVER_DISCONNECT; + if ( server->config.connect_disconnect_callback ) { server->config.connect_disconnect_callback( server->config.callback_context, client_index, 0 ); @@ -8403,6 +8430,7 @@ void test_client_side_disconnect() check( netcode_server_client_connected( server, 0 ) == 0 ); check( netcode_server_num_connected_clients( server ) == 0 ); + check( netcode_server_client_disconnect_reason( server, 0 ) == NETCODE_SERVER_CLIENT_DISCONNECT_REASON_CLIENT_DISCONNECT ); netcode_server_destroy( server ); @@ -8498,6 +8526,138 @@ void test_server_side_disconnect() check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_DISCONNECTED ); check( netcode_server_client_connected( server, 0 ) == 0 ); check( netcode_server_num_connected_clients( server ) == 0 ); + check( netcode_server_client_disconnect_reason( server, 0 ) == NETCODE_SERVER_CLIENT_DISCONNECT_REASON_SERVER_DISCONNECT ); + + netcode_server_destroy( server ); + + netcode_client_destroy( client ); + + netcode_network_simulator_destroy( network_simulator ); +} + +void test_server_client_disconnect_reason() +{ + struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL ); + + // start a server and connect one client + + double time = 0.0; + double delta_time = 1.0 / 10.0; + + struct netcode_client_config_t client_config; + netcode_default_client_config( &client_config ); + client_config.network_simulator = network_simulator; + + struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, time ); + + check( client ); + + struct netcode_server_config_t server_config; + netcode_default_server_config( &server_config ); + server_config.protocol_id = TEST_PROTOCOL_ID; + server_config.network_simulator = network_simulator; + memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES ); + + struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, time ); + + check( server ); + + netcode_server_start( server, 1 ); + + // no disconnect has happened yet, so the client slot reason is none + + check( netcode_server_client_disconnect_reason( server, 0 ) == NETCODE_SERVER_CLIENT_DISCONNECT_REASON_NONE ); + + NETCODE_CONST char * server_address = "[::1]:40000"; + + uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES]; + + uint64_t client_id = 0; + netcode_random_bytes( (uint8_t*) &client_id, 8 ); + + uint8_t user_data[NETCODE_USER_DATA_BYTES]; + netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES); + + check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) ); + + netcode_client_connect( client, connect_token ); + + while ( 1 ) + { + netcode_network_simulator_update( network_simulator, time ); + + netcode_client_update( client, time ); + + netcode_server_update( server, time ); + + if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED ) + break; + + if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED ) + break; + + time += delta_time; + } + + check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED ); + check( netcode_server_client_connected( server, 0 ) == 1 ); + check( netcode_server_client_disconnect_reason( server, 0 ) == NETCODE_SERVER_CLIENT_DISCONNECT_REASON_NONE ); + + // stop updating the client so it goes silent. the server should time it out + // and record that as the disconnect reason, distinct from a clean disconnect + + int i; + for ( i = 0; i < 200; i++ ) + { + netcode_network_simulator_update( network_simulator, time ); + + netcode_server_update( server, time ); + + if ( !netcode_server_client_connected( server, 0 ) ) + break; + + time += delta_time; + } + + check( netcode_server_client_connected( server, 0 ) == 0 ); + check( netcode_server_num_connected_clients( server ) == 0 ); + check( netcode_server_client_disconnect_reason( server, 0 ) == NETCODE_SERVER_CLIENT_DISCONNECT_REASON_TIMED_OUT ); + + // reconnect. a new client connecting to the slot clears the reason back to none + + netcode_client_disconnect( client ); + + // catch the client's internal clock up to the current time before reconnecting, since it + // was deliberately not updated above. otherwise the first update after connect sees the + // whole timeout leg as elapsed time and immediately times out the connection request. + netcode_client_update( client, time ); + + netcode_network_simulator_reset( network_simulator ); + + check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) ); + + netcode_client_connect( client, connect_token ); + + while ( 1 ) + { + netcode_network_simulator_update( network_simulator, time ); + + netcode_client_update( client, time ); + + netcode_server_update( server, time ); + + if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED ) + break; + + if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED ) + break; + + time += delta_time; + } + + check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED ); + check( netcode_server_client_connected( server, 0 ) == 1 ); + check( netcode_server_client_disconnect_reason( server, 0 ) == NETCODE_SERVER_CLIENT_DISCONNECT_REASON_NONE ); netcode_server_destroy( server ); @@ -9319,6 +9479,7 @@ void netcode_test() RUN_TEST( test_client_error_connection_denied ); RUN_TEST( test_client_side_disconnect ); RUN_TEST( test_server_side_disconnect ); + RUN_TEST( test_server_client_disconnect_reason ); RUN_TEST( test_client_reconnect ); RUN_TEST( test_disable_timeout ); RUN_TEST( test_loopback ); diff --git a/netcode.h b/netcode.h index 3373e14..bf8932d 100755 --- a/netcode.h +++ b/netcode.h @@ -92,6 +92,15 @@ #define NETCODE_CLIENT_STATE_SENDING_CONNECTION_RESPONSE 2 #define NETCODE_CLIENT_STATE_CONNECTED 3 +// The reason the client in a server slot was last disconnected. Tracked per-client slot: +// reset to NONE when the server starts and when a new client connects to the slot, and +// recorded before the connect_disconnect_callback fires, so it can be queried from inside +// that callback via netcode_server_client_disconnect_reason. +#define NETCODE_SERVER_CLIENT_DISCONNECT_REASON_NONE 0 +#define NETCODE_SERVER_CLIENT_DISCONNECT_REASON_TIMED_OUT 1 +#define NETCODE_SERVER_CLIENT_DISCONNECT_REASON_CLIENT_DISCONNECT 2 +#define NETCODE_SERVER_CLIENT_DISCONNECT_REASON_SERVER_DISCONNECT 3 + #define NETCODE_CLIENT_CREATE_ERROR_NONE 0 #define NETCODE_CLIENT_CREATE_ERROR_PARSE_ADDRESS_FAILED 1 #define NETCODE_CLIENT_CREATE_ERROR_PARSE_ADDRESS2_FAILED 2 @@ -285,6 +294,8 @@ void netcode_server_update( struct netcode_server_t * server, double time ); int netcode_server_client_connected( struct netcode_server_t * server, int client_index ); +int netcode_server_client_disconnect_reason( struct netcode_server_t * server, int client_index ); + uint64_t netcode_server_client_id( struct netcode_server_t * server, int client_index ); struct netcode_address_t * netcode_server_client_address( struct netcode_server_t * server, int client_index );