engine: implement GoldSrc A2S query for NetAPI

As Xash also supports GoldSrc like queries, it's netinfo is dropped.
This commit is contained in:
Sergey Degtyar
2026-07-22 09:39:20 +05:00
committed by Alibek Omarov
parent e0ab44213a
commit 2127735456
5 changed files with 288 additions and 115 deletions

View File

@@ -2073,12 +2073,47 @@ static void CL_ParseStatusMessage( netadr_t from, sizebuf_t *msg )
UI_AddServerToList( from, infostring ); UI_AddServerToList( from, infostring );
} }
static net_request_t *CL_NetRequestFind( netadr_t from, int type )
{
for( int i = 0; i < ARRAYSIZE( clgame.net_requests ); i++ )
{
net_request_t *nr = &clgame.net_requests[i];
if( !nr->pfnFunc || nr->resp.type != type )
continue;
if( NET_CompareAdr( nr->resp.remote_address, from ))
return nr;
// broadcast requests accept a response from anyone
if( NET_NetadrType( &nr->resp.remote_address ) == NA_BROADCAST )
return nr;
}
return NULL;
}
static void CL_NetRequestComplete( net_request_t *nr, netadr_t from, const char *response )
{
netadr_t request_adr = nr->resp.remote_address;
nr->resp.response = (void *)response;
nr->resp.remote_address = from;
nr->resp.error = NET_SUCCESS;
nr->resp.ping = host.realtime - nr->timesend;
nr->pfnFunc( &nr->resp );
if( FBitSet( nr->flags, FNETAPI_MULTIPLE_RESPONSE ))
nr->resp.remote_address = request_adr;
else memset( nr, 0, sizeof( *nr )); // done
}
static void CL_ParseGoldSrcStatusMessage( netadr_t from, sizebuf_t *msg, qboolean legacy_format ) static void CL_ParseGoldSrcStatusMessage( netadr_t from, sizebuf_t *msg, qboolean legacy_format )
{ {
static char s[512+8]; static char s[512+8];
int p, numcl, maxcl, password, remaining, bots; int p, numcl, maxcl, password, bots;
string host, map, gamedir, version; string host, map, gamedir, version;
char *replace;
// set to beginning but skip header // set to beginning but skip header
MSG_SeekToBit( msg, (sizeof( uint32_t ) + sizeof( uint8_t )) << 3, SEEK_SET ); MSG_SeekToBit( msg, (sizeof( uint32_t ) + sizeof( uint8_t )) << 3, SEEK_SET );
@@ -2163,7 +2198,7 @@ static void CL_ParseGoldSrcStatusMessage( netadr_t from, sizebuf_t *msg, qboolea
// write host last so we can try to cut off too long hostnames // write host last so we can try to cut off too long hostnames
// TODO: value size limit for infostrings // TODO: value size limit for infostrings
remaining = sizeof( s ) - Q_strlen( s ) - sizeof( "\\host\\" ) - 1; int remaining = sizeof( s ) - Q_strlen( s ) - sizeof( "\\host\\" ) - 1;
if( remaining < 0 ) if( remaining < 0 )
{ {
// should never happen? // should never happen?
@@ -2171,103 +2206,205 @@ static void CL_ParseGoldSrcStatusMessage( netadr_t from, sizebuf_t *msg, qboolea
return; return;
} }
char *replace;
while(( replace = Q_strpbrk( host, "\\\"" ))) while(( replace = Q_strpbrk( host, "\\\"" )))
{
*replace = ' '; // find a better replacement? *replace = ' '; // find a better replacement?
}
Info_SetValueForKey( s, "host", host, sizeof( s )); Info_SetValueForKey( s, "host", host, sizeof( s ));
// tell mainui about server
UI_AddServerToList( from, s ); UI_AddServerToList( from, s );
// now process NetAPI requst
net_request_t *nr = CL_NetRequestFind( from, NETAPI_REQUEST_DETAILS );
if( !nr )
return;
s[0] = 0;
Info_SetValueForKey( s, "gamedir", gamedir, sizeof( s ));
Info_SetValueForKeyf( s, "current", sizeof( s ), "%i", numcl );
Info_SetValueForKeyf( s, "max", sizeof( s ), "%i", maxcl );
Info_SetValueForKey( s, "map", map, sizeof( s ));
Info_SetValueForKey( s, "hostname", host, sizeof( s ));
CL_NetRequestComplete( nr, from, s );
} }
/* /*
================= =================
CL_ParseNETInfoMessage CL_NetRequestSend
Handle a reply from a netinfo
================= =================
*/ */
static void CL_ParseNETInfoMessage( netadr_t from, const char *s ) qboolean CL_NetRequestSend( net_request_t *nr )
{ {
net_request_t *nr = NULL; byte buf[64];
static char infostring[MAX_PRINT_MSG]; sizebuf_t msg;
int i, context, type;
int errorBits = 0;
const char *val;
context = Q_atoi( Cmd_Argv( 1 )); if( nr->resp.type == NETAPI_REQUEST_PING )
type = Q_atoi( Cmd_Argv( 2 ));
// find request with specified context and type
for( i = 0; i < MAX_REQUESTS; i++ )
{ {
if( clgame.net_requests[i].resp.context == context && clgame.net_requests[i].resp.type == type ) Netchan_OutOfBandPrint( NS_CLIENT, nr->resp.remote_address, A2A_GOLDSRC_PING );
{ return true;
nr = &clgame.net_requests[i];
break;
}
} }
// not found, ignore MSG_Init( &msg, "NetRequest", buf, sizeof( buf ));
if( nr == NULL )
return;
// find the payload switch( nr->resp.type )
s = Q_strchr( s, ' ' ); // skip netinfo
if( !s )
return;
s = Q_strchr( s + 1, ' ' ); // skip challenge
if( !s )
return;
s = Q_strchr( s + 1, ' ' ); // skip type
if( s )
s++; // skip final whitespace
else if( type != NETAPI_REQUEST_PING ) // ping have no payload, and that's ok
return;
if( s )
{ {
if( s[0] == '\\' ) case NETAPI_REQUEST_DETAILS:
{ MSG_WriteString( &msg, A2S_GOLDSRC_INFO );
// check for errors break;
val = Info_ValueForKey( s, "neterror" ); case NETAPI_REQUEST_PLAYERS:
MSG_WriteByte( &msg, A2S_GOLDSRC_PLAYERS );
if( !Q_stricmp( val, "protocol" )) break;
SetBits( errorBits, NET_ERROR_PROTO_UNSUPPORTED ); case NETAPI_REQUEST_RULES:
else if( !Q_stricmp( val, "undefined" )) MSG_WriteByte( &msg, A2S_GOLDSRC_RULES );
SetBits( errorBits, NET_ERROR_UNDEFINED ); break;
else if( !Q_stricmp( val, "forbidden" )) default:
SetBits( errorBits, NET_ERROR_FORBIDDEN ); return false;
CL_FixupColorStringsForInfoString( s, infostring, sizeof( infostring ));
}
else
{
Q_strncpy( infostring, s, sizeof( infostring ));
}
}
else
{
infostring[0] = 0;
} }
// setup the answer MSG_WriteLong( &msg, nr->challenge );
nr->resp.response = infostring;
nr->resp.remote_address = from;
nr->resp.error = NET_SUCCESS;
nr->resp.ping = host.realtime - nr->timesend;
if( nr->timeout <= host.realtime ) Netchan_OutOfBand( NS_CLIENT, nr->resp.remote_address, MSG_GetNumBytesWritten( &msg ), MSG_GetData( &msg ));
SetBits( nr->resp.error, NET_ERROR_TIMEOUT );
SetBits( nr->resp.error, errorBits ); // misc error bits
nr->pfnFunc( &nr->resp ); return true;
}
if( !FBitSet( nr->flags, FNETAPI_MULTIPLE_RESPONSE )) /*
memset( nr, 0, sizeof( *nr )); // done =================
CL_HasActiveNetRequest
=================
*/
qboolean CL_HasActiveNetRequest( netadr_t from )
{
for( int i = 0; i < ARRAYSIZE( clgame.net_requests ); i++ )
{
const net_request_t *nr = &clgame.net_requests[i];
if( !nr->pfnFunc )
continue;
if( nr->resp.type != NETAPI_REQUEST_RULES && nr->resp.type != NETAPI_REQUEST_PLAYERS )
continue;
if( NET_CompareAdr( nr->resp.remote_address, from ))
return true;
}
return false;
}
/*
=================
CL_NetRequestChallenge
=================
*/
static void CL_NetRequestChallenge( netadr_t from, sizebuf_t *msg )
{
MSG_SeekToBit( msg, ( sizeof( uint32_t ) + sizeof( uint8_t )) << 3, SEEK_SET );
int challenge = MSG_ReadLong( msg );
// give the challenge to the first request without one,
// the server sends a challenge per received query
for( int i = 0; i < ARRAYSIZE( clgame.net_requests ); i++ )
{
net_request_t *nr = &clgame.net_requests[i];
if( !nr->pfnFunc || nr->resp.type == NETAPI_REQUEST_PING )
continue;
if( nr->challenge != -1 )
continue;
if( !NET_CompareAdr( nr->resp.remote_address, from ))
continue;
nr->challenge = challenge;
CL_NetRequestSend( nr );
break;
}
}
/*
=================
CL_NetRequestPing
=================
*/
static void CL_NetRequestPing( netadr_t from )
{
net_request_t *nr = CL_NetRequestFind( from, NETAPI_REQUEST_PING );
if( !nr )
return;
CL_NetRequestComplete( nr, from, "" );
}
/*
=================
CL_NetRequestPlayers
=================
*/
static void CL_NetRequestPlayers( netadr_t from, sizebuf_t *msg )
{
static char s[MAX_PRINT_MSG];
net_request_t *nr = CL_NetRequestFind( from, NETAPI_REQUEST_PLAYERS );
if( !nr )
return;
MSG_SeekToBit( msg, ( sizeof( uint32_t ) + sizeof( uint8_t )) << 3, SEEK_SET );
int count = MSG_ReadByte( msg );
s[0] = 0;
for( int i = 0; i < count && !MSG_CheckOverflow( msg ); i++ )
{
MSG_ReadByte( msg ); // index, GoldSrc servers always send 0 here
Info_SetValueForKey( s, va( "p%iname", i ), MSG_ReadString( msg ), sizeof( s ));
Info_SetValueForKeyf( s, va( "p%ifrags", i ), sizeof( s ), "%i", MSG_ReadLong( msg ));
Info_SetValueForKeyf( s, va( "p%itime", i ), sizeof( s ), "%f", MSG_ReadFloat( msg ));
}
Info_SetValueForKeyf( s, "players", sizeof( s ), "%i", count );
if( MSG_CheckOverflow( msg ))
return;
CL_NetRequestComplete( nr, from, s );
}
/*
=================
CL_NetRequestRules
=================
*/
static void CL_NetRequestRules( netadr_t from, sizebuf_t *msg )
{
static char s[MAX_PRINT_MSG];
net_request_t *nr = CL_NetRequestFind( from, NETAPI_REQUEST_RULES );
if( !nr )
return;
MSG_SeekToBit( msg, ( sizeof( uint32_t ) + sizeof( uint8_t )) << 3, SEEK_SET );
int count = MSG_ReadShort( msg );
s[0] = 0;
for( int i = 0; i < count && !MSG_CheckOverflow( msg ); i++ )
{
string key;
Q_strncpy( key, MSG_ReadString( msg ), sizeof( key ));
Info_SetValueForKey( s, key, MSG_ReadString( msg ), sizeof( s ));
}
Info_SetValueForKeyf( s, "rules", sizeof( s ), "%i", count );
if( MSG_CheckOverflow( msg ))
return;
CL_NetRequestComplete( nr, from, s );
} }
/* /*
@@ -2279,14 +2416,13 @@ check for timeouts
*/ */
static void CL_ProcessNetRequests( void ) static void CL_ProcessNetRequests( void )
{ {
net_request_t *nr;
int i;
// find a request with specified context // find a request with specified context
for( i = 0; i < MAX_REQUESTS; i++ ) for( int i = 0; i < ARRAYSIZE( clgame.net_requests ); i++ )
{ {
nr = &clgame.net_requests[i]; net_request_t *nr = &clgame.net_requests[i];
if( !nr->pfnFunc ) continue; // not used
if( !nr->pfnFunc )
continue; // not used
if( nr->timeout <= host.realtime ) if( nr->timeout <= host.realtime )
{ {
@@ -2524,8 +2660,16 @@ static void CL_Print( const char *c, const char *args, netadr_t from, sizebuf_t
Con_Printf( "%s%c", s, s[Q_strlen( s ) - 1] != '\n' ? '\n' : '\0' ); Con_Printf( "%s%c", s, s[Q_strlen( s ) - 1] != '\n' ? '\n' : '\0' );
} }
static void CL_Challenge( const char *c, netadr_t from ) static void CL_Challenge( const char *c, netadr_t from, sizebuf_t *msg )
{ {
// connection challenge is text and always comes as literal A00000000,
// query challenge is binary: challenge type byte and the challenge value
if( c[0] == S2C_GOLDSRC_CHALLENGE[0] && Q_strcmp( c, S2C_GOLDSRC_CHALLENGE ))
{
CL_NetRequestChallenge( from, msg );
return;
}
if( cls.state != ca_connecting ) if( cls.state != ca_connecting )
return; return;
@@ -2727,9 +2871,13 @@ static void CL_ConnectionlessPacket( netadr_t from, sizebuf_t *msg )
{ {
CL_ParseGoldSrcStatusMessage( from, msg, true ); CL_ParseGoldSrcStatusMessage( from, msg, true );
} }
else if( !Q_strcmp( c, A2A_NETINFO )) else if( c[0] == S2A_GOLDSRC_PLAYERS )
{ {
CL_ParseNETInfoMessage( from, args ); // server responding to a status broadcast CL_NetRequestPlayers( from, msg );
}
else if( c[0] == S2A_GOLDSRC_RULES )
{
CL_NetRequestRules( from, msg );
} }
else if( c[0] == A2C_GOLDSRC_PRINT || !Q_strcmp( c, A2C_PRINT )) else if( c[0] == A2C_GOLDSRC_PRINT || !Q_strcmp( c, A2C_PRINT ))
{ {
@@ -2749,11 +2897,11 @@ static void CL_ConnectionlessPacket( netadr_t from, sizebuf_t *msg )
} }
else if( !Q_strcmp( c, A2A_ACK ) || !Q_strcmp( c, A2A_GOLDSRC_ACK )) else if( !Q_strcmp( c, A2A_ACK ) || !Q_strcmp( c, A2A_GOLDSRC_ACK ))
{ {
// no-op CL_NetRequestPing( from ); // server responding to a NetAPI ping
} }
else if( !Q_strcmp( c, S2C_CHALLENGE ) || !Q_strcmp( c, S2C_GOLDSRC_CHALLENGE )) else if( !Q_strcmp( c, S2C_CHALLENGE ) || c[0] == S2C_GOLDSRC_CHALLENGE[0] )
{ {
CL_Challenge( c, from ); CL_Challenge( c, from, msg );
} }
else if( !Q_strcmp( c, S2C_REJECT ) || c[0] == S2C_GOLDSRC_REJECT || c[0] == S2C_GOLDSRC_REJECT_BADPASSWORD ) else if( !Q_strcmp( c, S2C_REJECT ) || c[0] == S2C_GOLDSRC_REJECT || c[0] == S2C_GOLDSRC_REJECT_BADPASSWORD )
{ {

View File

@@ -445,6 +445,7 @@ typedef struct
double timeout; double timeout;
double timesend; // time when request was sended double timesend; // time when request was sended
int flags; // FNETAPI_MULTIPLE_RESPONSE etc int flags; // FNETAPI_MULTIPLE_RESPONSE etc
int challenge; // GoldSrc query challenge, -1 until received
} net_request_t; } net_request_t;
// new versions of client dlls have a single export with all callbacks // new versions of client dlls have a single export with all callbacks
@@ -793,6 +794,7 @@ void CL_ClearState( void );
void CL_SetCheatState( qboolean multiplayer, qboolean allow_cheats ); void CL_SetCheatState( qboolean multiplayer, qboolean allow_cheats );
void CL_SendGoldSrcConnectPacket( netadr_t adr, int challenge, const void *ticket, size_t ticketlen ); void CL_SendGoldSrcConnectPacket( netadr_t adr, int challenge, const void *ticket, size_t ticketlen );
void CL_NotifyServerListResponse( void ); void CL_NotifyServerListResponse( void );
qboolean CL_NetRequestSend( net_request_t *nr );
// //
// cl_demo.c // cl_demo.c

View File

@@ -3364,44 +3364,60 @@ NetAPI_SendRequest
*/ */
static void GAME_EXPORT NetAPI_SendRequest( int context, int request, int flags, double timeout, netadr_t *remote_address, net_api_response_func_t response ) static void GAME_EXPORT NetAPI_SendRequest( int context, int request, int flags, double timeout, netadr_t *remote_address, net_api_response_func_t response )
{ {
net_request_t *nr = NULL;
int i;
if( !response ) if( !response )
{ {
Con_DPrintf( S_ERROR "%s: no callbcak specified for request with context %i!\n", __func__, context ); Con_DPrintf( S_ERROR "%s: no callback specified for request with context %i!\n", __func__, context );
return; return;
} }
// FIXME: call pfnFunc and tell client.dll about errors?
if( NET_NetadrType( remote_address ) == NA_IPX || NET_NetadrType( remote_address ) == NA_BROADCAST_IPX ) if( NET_NetadrType( remote_address ) == NA_IPX || NET_NetadrType( remote_address ) == NA_BROADCAST_IPX )
return; // IPX no longer support return; // IPX no longer support
if( request == NETAPI_REQUEST_SERVERLIST ) switch( request )
{
case NETAPI_REQUEST_SERVERLIST:
return; // no support for server list requests return; // no support for server list requests
case NETAPI_REQUEST_PING:
case NETAPI_REQUEST_RULES:
case NETAPI_REQUEST_PLAYERS:
case NETAPI_REQUEST_DETAILS:
break;
default:
Con_Printf( S_ERROR "%s: unknown request type %d, context %i\n", __func__, request, context );
return;
}
// find a free request // find a free request
for( i = 0; i < MAX_REQUESTS; i++ ) net_request_t *nr = NULL, *oldest_nr = NULL;
int i;
double max_timeout = 0;
for( i = 0; i < ARRAYSIZE( clgame.net_requests ); i++ )
{ {
nr = &clgame.net_requests[i]; nr = &clgame.net_requests[i];
if( !nr->pfnFunc ) break;
}
if( i == MAX_REQUESTS ) if(( host.realtime - nr->timesend ) > max_timeout )
{
double max_timeout = 0;
// no free requests? use oldest
for( i = 0, nr = NULL; i < MAX_REQUESTS; i++ )
{ {
if(( host.realtime - clgame.net_requests[i].timesend ) > max_timeout ) max_timeout = host.realtime - nr->timesend;
{ oldest_nr = nr;
max_timeout = host.realtime - clgame.net_requests[i].timesend;
nr = &clgame.net_requests[i];
}
} }
if( !nr->pfnFunc )
break;
} }
Assert( nr != NULL ); if( i == ARRAYSIZE( clgame.net_requests ))
{
// no free requests? use oldest
nr = oldest_nr;
}
if( !nr )
{
Con_Printf( S_ERROR "%s: no free requests\n", __func__ );
return;
}
// clear slot // clear slot
memset( nr, 0, sizeof( *nr )); memset( nr, 0, sizeof( *nr ));
@@ -3414,9 +3430,10 @@ static void GAME_EXPORT NetAPI_SendRequest( int context, int request, int flags,
nr->resp.type = request; nr->resp.type = request;
nr->resp.remote_address = *remote_address; nr->resp.remote_address = *remote_address;
nr->flags = flags; nr->flags = flags;
nr->challenge = -1;
// local servers request if( !CL_NetRequestSend( nr ))
Netchan_OutOfBandPrint( NS_CLIENT, nr->resp.remote_address, A2A_NETINFO" %i %i %i", PROTOCOL_VERSION, context, request ); Con_Printf( S_ERROR "%s: failed to send net request for type %d with context %i\n", __func__, request, context );
} }
/* /*
@@ -3428,7 +3445,7 @@ NetAPI_CancelRequest
static void GAME_EXPORT NetAPI_CancelRequest( int context ) static void GAME_EXPORT NetAPI_CancelRequest( int context )
{ {
// find a specified request // find a specified request
for( int i = 0; i < MAX_REQUESTS; i++ ) for( int i = 0; i < ARRAYSIZE( clgame.net_requests ); i++ )
{ {
net_request_t *nr = &clgame.net_requests[i]; net_request_t *nr = &clgame.net_requests[i];
@@ -3456,7 +3473,7 @@ NetAPI_CancelAllRequests
void GAME_EXPORT NetAPI_CancelAllRequests( void ) void GAME_EXPORT NetAPI_CancelAllRequests( void )
{ {
// tell the user about cancel // tell the user about cancel
for( int i = 0; i < MAX_REQUESTS; i++ ) for( int i = 0; i < ARRAYSIZE( clgame.net_requests ); i++ )
{ {
net_request_t *nr = &clgame.net_requests[i]; net_request_t *nr = &clgame.net_requests[i];
if( !nr->pfnFunc ) continue; // not used if( !nr->pfnFunc ) continue; // not used

View File

@@ -1364,9 +1364,14 @@ static qboolean NET_QueuePacket( int net_socket, netsrc_t sock, netadr_t *from,
// check for split message // check for split message
if( sock == NS_CLIENT && *(int *)data == NET_HEADER_SPLITPACKET ) if( sock == NS_CLIENT && *(int *)data == NET_HEADER_SPLITPACKET )
{ {
if( !CL_IsFromConnectingServer( *from )) if( CL_IsFromConnectingServer( *from ))
return false; return NET_GetLong( data, ret, length, CL_GetSplitSize( ), CL_Protocol( ));
return NET_GetLong( data, ret, length, CL_GetSplitSize( ), CL_Protocol( ));
// GoldSrc servers (e.g. ReUnion) can split large query responses
if( CL_HasActiveNetRequest( *from ))
return NET_GetLong( data, ret, length, 1400, PROTO_GOLDSRC );
return false;
} }
#endif #endif

View File

@@ -89,6 +89,7 @@ void NET_GetLocalAddress( netadr_t *ip4, netadr_t *ip6 );
#if !XASH_DEDICATED #if !XASH_DEDICATED
size_t CL_GetSplitSize( void ); size_t CL_GetSplitSize( void );
qboolean CL_IsFromConnectingServer( netadr_t from ); qboolean CL_IsFromConnectingServer( netadr_t from );
qboolean CL_HasActiveNetRequest( netadr_t from );
#endif #endif
void HTTP_AddCustomServer( const char *url ); void HTTP_AddCustomServer( const char *url );