Skip to content

Commit f34dce5

Browse files
authored
Merge pull request #63 from neyoneit/dl-source-fallback
Add dl_source2: dual download source with speed test and fallback
2 parents 656e65c + 4bee2de commit f34dce5

4 files changed

Lines changed: 284 additions & 2 deletions

File tree

code/client/cl_curl.c

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,148 @@ static size_t Com_DL_HeaderCallback( void *ptr, size_t size, size_t nmemb, void
896896
}
897897

898898

899+
/*
900+
===============================================================
901+
Com_DL_SpeedTest()
902+
903+
Run a parallel 1-second speed test against two download sources.
904+
Returns 1 if source1 is faster/equal, 2 if source2 is faster.
905+
Returns 0 on error (use source1 as default).
906+
Both URLs should have %m already replaced with the actual filename.
907+
===============================================================
908+
*/
909+
typedef struct {
910+
int bytesReceived;
911+
} speedTestData_t;
912+
913+
static size_t Com_DL_SpeedTestWrite( void *ptr, size_t size, size_t nmemb, void *userdata )
914+
{
915+
speedTestData_t *data = (speedTestData_t *)userdata;
916+
data->bytesReceived += (int)( size * nmemb );
917+
return size * nmemb;
918+
}
919+
920+
int Com_DL_SpeedTest( download_t *dl, const char *url1, const char *url2 )
921+
{
922+
CURL *easy1, *easy2;
923+
CURLM *multi;
924+
speedTestData_t data1, data2;
925+
int running, startTime, elapsed;
926+
float speed1, speed2;
927+
928+
if ( !Com_DL_Init( dl ) ) {
929+
Com_Printf( S_COLOR_YELLOW "Speed test: failed to init cURL\n" );
930+
return 0;
931+
}
932+
933+
easy1 = dl->func.easy_init();
934+
easy2 = dl->func.easy_init();
935+
multi = dl->func.multi_init();
936+
937+
if ( !easy1 || !easy2 || !multi ) {
938+
Com_Printf( S_COLOR_YELLOW "Speed test: failed to create cURL handles\n" );
939+
if ( easy1 ) dl->func.easy_cleanup( easy1 );
940+
if ( easy2 ) dl->func.easy_cleanup( easy2 );
941+
if ( multi ) dl->func.multi_cleanup( multi );
942+
Com_DL_Done( dl );
943+
return 0;
944+
}
945+
946+
memset( &data1, 0, sizeof( data1 ) );
947+
memset( &data2, 0, sizeof( data2 ) );
948+
949+
// setup source 1
950+
dl->func.easy_setopt( easy1, CURLOPT_URL, url1 );
951+
dl->func.easy_setopt( easy1, CURLOPT_WRITEFUNCTION, Com_DL_SpeedTestWrite );
952+
dl->func.easy_setopt( easy1, CURLOPT_WRITEDATA, &data1 );
953+
dl->func.easy_setopt( easy1, CURLOPT_FOLLOWLOCATION, 1 );
954+
dl->func.easy_setopt( easy1, CURLOPT_MAXREDIRS, 5 );
955+
dl->func.easy_setopt( easy1, CURLOPT_FAILONERROR, 1 );
956+
dl->func.easy_setopt( easy1, CURLOPT_NOPROGRESS, 1 );
957+
dl->func.easy_setopt( easy1, CURLOPT_CONNECTTIMEOUT, 3L );
958+
dl->func.easy_setopt( easy1, CURLOPT_USERAGENT, Q3_VERSION );
959+
#if CURL_AT_LEAST_VERSION(7, 85, 0)
960+
dl->func.easy_setopt( easy1, CURLOPT_PROTOCOLS_STR, ALLOWED_PROTOCOLS_STR );
961+
#else
962+
dl->func.easy_setopt( easy1, CURLOPT_PROTOCOLS, ALLOWED_PROTOCOLS );
963+
#endif
964+
965+
// setup source 2
966+
dl->func.easy_setopt( easy2, CURLOPT_URL, url2 );
967+
dl->func.easy_setopt( easy2, CURLOPT_WRITEFUNCTION, Com_DL_SpeedTestWrite );
968+
dl->func.easy_setopt( easy2, CURLOPT_WRITEDATA, &data2 );
969+
dl->func.easy_setopt( easy2, CURLOPT_FOLLOWLOCATION, 1 );
970+
dl->func.easy_setopt( easy2, CURLOPT_MAXREDIRS, 5 );
971+
dl->func.easy_setopt( easy2, CURLOPT_FAILONERROR, 1 );
972+
dl->func.easy_setopt( easy2, CURLOPT_NOPROGRESS, 1 );
973+
dl->func.easy_setopt( easy2, CURLOPT_CONNECTTIMEOUT, 3L );
974+
dl->func.easy_setopt( easy2, CURLOPT_USERAGENT, Q3_VERSION );
975+
#if CURL_AT_LEAST_VERSION(7, 85, 0)
976+
dl->func.easy_setopt( easy2, CURLOPT_PROTOCOLS_STR, ALLOWED_PROTOCOLS_STR );
977+
#else
978+
dl->func.easy_setopt( easy2, CURLOPT_PROTOCOLS, ALLOWED_PROTOCOLS );
979+
#endif
980+
981+
dl->func.multi_add_handle( multi, easy1 );
982+
dl->func.multi_add_handle( multi, easy2 );
983+
984+
Com_Printf( "Speed test: testing download sources...\n" );
985+
986+
startTime = Sys_Milliseconds();
987+
988+
// run both downloads for 2 seconds
989+
do {
990+
dl->func.multi_perform( multi, &running );
991+
elapsed = Sys_Milliseconds() - startTime;
992+
if ( running == 0 )
993+
break;
994+
} while ( elapsed < 3000 );
995+
996+
elapsed = Sys_Milliseconds() - startTime;
997+
if ( elapsed < 1 ) elapsed = 1;
998+
999+
speed1 = (float)data1.bytesReceived / ( (float)elapsed / 1000.0f );
1000+
speed2 = (float)data2.bytesReceived / ( (float)elapsed / 1000.0f );
1001+
1002+
// log results
1003+
if ( speed1 >= 1024.0f * 1024.0f )
1004+
Com_Printf( " dl_source: %.1f MB/s\n", speed1 / ( 1024.0f * 1024.0f ) );
1005+
else
1006+
Com_Printf( " dl_source: %.0f KB/s\n", speed1 / 1024.0f );
1007+
1008+
if ( speed2 >= 1024.0f * 1024.0f )
1009+
Com_Printf( " dl_source2: %.1f MB/s\n", speed2 / ( 1024.0f * 1024.0f ) );
1010+
else
1011+
Com_Printf( " dl_source2: %.0f KB/s\n", speed2 / 1024.0f );
1012+
1013+
// cleanup
1014+
dl->func.multi_remove_handle( multi, easy1 );
1015+
dl->func.multi_remove_handle( multi, easy2 );
1016+
dl->func.easy_cleanup( easy1 );
1017+
dl->func.easy_cleanup( easy2 );
1018+
dl->func.multi_cleanup( multi );
1019+
Com_DL_Done( dl );
1020+
1021+
if ( speed2 > speed1 && data2.bytesReceived > 0 ) {
1022+
Com_Printf( S_COLOR_GREEN "Speed test: dl_source2 is faster, using as primary.\n" );
1023+
return 2;
1024+
}
1025+
1026+
if ( data1.bytesReceived > 0 ) {
1027+
Com_Printf( S_COLOR_GREEN "Speed test: dl_source is faster, using as primary.\n" );
1028+
return 1;
1029+
}
1030+
1031+
if ( data2.bytesReceived > 0 ) {
1032+
Com_Printf( S_COLOR_YELLOW "Speed test: dl_source failed, using dl_source2.\n" );
1033+
return 2;
1034+
}
1035+
1036+
Com_Printf( S_COLOR_RED "Speed test: both sources failed.\n" );
1037+
return 0;
1038+
}
1039+
1040+
8991041
/*
9001042
===============================================================
9011043
Com_DL_Begin()
@@ -1124,12 +1266,32 @@ qboolean Com_DL_Perform( download_t *dl )
11241266
else
11251267
{
11261268
qboolean autoDownload = dl->mapAutoDownload;
1269+
char fallbackURL[MAX_OSPATH];
1270+
char originalName[MAX_OSPATH];
1271+
11271272
dl->func.easy_getinfo( msg->easy_handle, CURLINFO_RESPONSE_CODE, &code );
11281273
Com_Printf( S_COLOR_RED "Download Error: %s Code: %ld\n",
11291274
dl->func.easy_strerror( msg->data.result ), code );
1275+
1276+
// save fallback info before cleanup
1277+
Q_strncpyz( fallbackURL, dl->fallbackURL, sizeof( fallbackURL ) );
1278+
Q_strncpyz( originalName, dl->originalName, sizeof( originalName ) );
1279+
11301280
strcpy( name, dl->TempName );
11311281
Com_DL_Cleanup( dl );
11321282
FS_Remove( name );
1283+
1284+
// try fallback source if available
1285+
if ( fallbackURL[0] != '\0' && originalName[0] != '\0' )
1286+
{
1287+
Com_Printf( S_COLOR_YELLOW "Trying fallback source...\n" );
1288+
if ( Com_DL_Begin( dl, originalName, fallbackURL, autoDownload ) )
1289+
{
1290+
dl->fallbackURL[0] = '\0'; // don't fallback again
1291+
return qtrue;
1292+
}
1293+
}
1294+
11331295
if ( autoDownload )
11341296
{
11351297
if ( cls.state == CA_CONNECTED )

code/client/cl_curl.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ typedef struct download_s {
113113
qboolean headerCheck;
114114
qboolean mapAutoDownload;
115115

116+
// fallback source support
117+
char fallbackURL[MAX_OSPATH];
118+
char originalName[MAX_OSPATH];
119+
116120
struct func_s {
117121
char* (*version)(void);
118122
char * (*easy_escape)(CURL *curl, const char *string, int length);

code/client/cl_main.c

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ cvar_t *cl_lanForcePackets;
6868
cvar_t *cl_guidServerUniq;
6969

7070
cvar_t *dl_source;
71+
cvar_t *dl_source2;
7172
cvar_t *dl_usebaseq3;
7273

7374
cvar_t *cl_reconnectArgs;
@@ -3988,7 +3989,67 @@ void CL_Init( void ) {
39883989
Cvar_SetDescription( cl_guidServerUniq, "Makes cl_guid unique for each server." );
39893990

39903991
dl_source = Cvar_Get( "dl_source", "http://ws.q3df.org/maps/download/%m", CVAR_ARCHIVE );
3991-
Cvar_SetDescription( dl_source, "Cvar must point to download location." );
3992+
Cvar_SetDescription( dl_source, "Primary download source URL. Use %m for map/pak name placeholder." );
3993+
3994+
dl_source2 = Cvar_Get( "dl_source2", "https://defrag.racing/maps/download/%m", CVAR_ARCHIVE );
3995+
Cvar_SetDescription( dl_source2, "Secondary download source URL. If set, a 3-second speed test picks the faster source automatically. Use %m for map/pak name placeholder." );
3996+
3997+
// auto-fill empty sources: ensure both known mirrors are available
3998+
{
3999+
#define DL_DEFRAG_RACING "https://defrag.racing/maps/download/%m"
4000+
#define DL_WS_Q3DF "http://ws.q3df.org/maps/download/%m"
4001+
4002+
qboolean src1_empty = ( dl_source->string[0] == '\0' );
4003+
qboolean src2_empty = ( dl_source2->string[0] == '\0' );
4004+
qboolean src1_is_defrag = ( strstr( dl_source->string, "defrag.racing" ) != NULL );
4005+
qboolean src1_is_ws = ( strstr( dl_source->string, "ws.q3df.org" ) != NULL );
4006+
qboolean src2_is_defrag = ( strstr( dl_source2->string, "defrag.racing" ) != NULL );
4007+
qboolean src2_is_ws = ( strstr( dl_source2->string, "ws.q3df.org" ) != NULL );
4008+
4009+
if ( src1_empty && src2_empty ) {
4010+
// both empty: set defaults
4011+
Cvar_Set( "dl_source", DL_WS_Q3DF );
4012+
Cvar_Set( "dl_source2", DL_DEFRAG_RACING );
4013+
} else if ( src1_empty ) {
4014+
// dl_source empty: fill with whichever mirror dl_source2 is NOT
4015+
Cvar_Set( "dl_source", src2_is_defrag ? DL_WS_Q3DF : DL_DEFRAG_RACING );
4016+
} else if ( src2_empty ) {
4017+
// dl_source2 empty: fill with whichever mirror dl_source is NOT
4018+
Cvar_Set( "dl_source2", src1_is_defrag ? DL_WS_Q3DF : DL_DEFRAG_RACING );
4019+
}
4020+
4021+
// fix duplicate sources: if both point to the same server, set the other one
4022+
if ( !src1_empty && !src2_empty ) {
4023+
// re-read after potential changes above
4024+
src1_is_defrag = ( strstr( dl_source->string, "defrag.racing" ) != NULL );
4025+
src1_is_ws = ( strstr( dl_source->string, "ws.q3df.org" ) != NULL );
4026+
src2_is_defrag = ( strstr( dl_source2->string, "defrag.racing" ) != NULL );
4027+
src2_is_ws = ( strstr( dl_source2->string, "ws.q3df.org" ) != NULL );
4028+
4029+
if ( src1_is_defrag && src2_is_defrag ) {
4030+
Cvar_Set( "dl_source2", DL_WS_Q3DF );
4031+
} else if ( src1_is_ws && src2_is_ws ) {
4032+
Cvar_Set( "dl_source2", DL_DEFRAG_RACING );
4033+
}
4034+
}
4035+
4036+
// fix common misconfiguration: ws.q3df.org only works over HTTP, not HTTPS
4037+
if ( strstr( dl_source->string, "https://ws.q3df.org" ) != NULL ) {
4038+
char fixed[MAX_CVAR_VALUE_STRING];
4039+
Q_strncpyz( fixed, dl_source->string, sizeof( fixed ) );
4040+
Q_replace( "https://ws.q3df.org", "http://ws.q3df.org", fixed, sizeof( fixed ) );
4041+
Cvar_Set( "dl_source", fixed );
4042+
}
4043+
if ( strstr( dl_source2->string, "https://ws.q3df.org" ) != NULL ) {
4044+
char fixed[MAX_CVAR_VALUE_STRING];
4045+
Q_strncpyz( fixed, dl_source2->string, sizeof( fixed ) );
4046+
Q_replace( "https://ws.q3df.org", "http://ws.q3df.org", fixed, sizeof( fixed ) );
4047+
Cvar_Set( "dl_source2", fixed );
4048+
}
4049+
4050+
#undef DL_DEFRAG_RACING
4051+
#undef DL_WS_Q3DF
4052+
}
39924053

39934054
dl_usebaseq3 = Cvar_Get( "dl_usebaseq3", "0", CVAR_ARCHIVE_ND );
39944055
Cvar_CheckRange( dl_usebaseq3, "0", "1", CV_INTEGER );
@@ -5043,11 +5104,27 @@ static void CL_ShowIP_f( void ) {
50435104

50445105
#ifdef USE_CURL
50455106

5107+
static int dl_preferredSource = 0; // 0=not tested, 1=dl_source, 2=dl_source2
5108+
5109+
static const char *CL_DL_BuildURL( char *buf, int bufSize, const char *sourceURL, const char *pakname )
5110+
{
5111+
// simple %m replacement without needing full CURL context
5112+
Q_strncpyz( buf, sourceURL, bufSize );
5113+
if ( !Q_replace( "%m", pakname, buf, bufSize ) ) {
5114+
if ( buf[strlen(buf)-1] != '/' )
5115+
Q_strcat( buf, bufSize, "/" );
5116+
Q_strcat( buf, bufSize, pakname );
5117+
}
5118+
return buf;
5119+
}
5120+
50465121
qboolean CL_Download( const char *cmd, const char *pakname, qboolean autoDownload )
50475122
{
50485123
char url[MAX_OSPATH];
50495124
char name[MAX_CVAR_VALUE_STRING];
50505125
const char *s;
5126+
const char *primarySource;
5127+
const char *fallbackSource;
50515128

50525129
if ( dl_source->string[0] == '\0' )
50535130
{
@@ -5084,7 +5161,45 @@ qboolean CL_Download( const char *cmd, const char *pakname, qboolean autoDownloa
50845161
}
50855162
}
50865163

5087-
return Com_DL_Begin( &download, pakname, dl_source->string, autoDownload );
5164+
// speed test on first download if both sources are set
5165+
if ( dl_preferredSource == 0 && dl_source2->string[0] != '\0' )
5166+
{
5167+
char url1[MAX_OSPATH], url2[MAX_OSPATH];
5168+
CL_DL_BuildURL( url1, sizeof( url1 ), dl_source->string, pakname );
5169+
CL_DL_BuildURL( url2, sizeof( url2 ), dl_source2->string, pakname );
5170+
dl_preferredSource = Com_DL_SpeedTest( &download, url1, url2 );
5171+
if ( dl_preferredSource == 0 )
5172+
dl_preferredSource = 1; // both failed, default to dl_source
5173+
}
5174+
5175+
// select primary and fallback sources
5176+
if ( dl_preferredSource == 2 ) {
5177+
primarySource = dl_source2->string;
5178+
fallbackSource = dl_source->string;
5179+
} else {
5180+
primarySource = dl_source->string;
5181+
fallbackSource = ( dl_source2->string[0] != '\0' ) ? dl_source2->string : NULL;
5182+
}
5183+
5184+
if ( Com_DL_Begin( &download, pakname, primarySource, autoDownload ) ) {
5185+
// set fallback info for mid-download failure recovery
5186+
if ( fallbackSource ) {
5187+
Q_strncpyz( download.fallbackURL, fallbackSource, sizeof( download.fallbackURL ) );
5188+
Q_strncpyz( download.originalName, pakname, sizeof( download.originalName ) );
5189+
} else {
5190+
download.fallbackURL[0] = '\0';
5191+
download.originalName[0] = '\0';
5192+
}
5193+
return qtrue;
5194+
}
5195+
5196+
// primary failed to start, try fallback
5197+
if ( fallbackSource ) {
5198+
Com_Printf( S_COLOR_YELLOW "Primary source failed, trying fallback...\n" );
5199+
return Com_DL_Begin( &download, pakname, fallbackSource, autoDownload );
5200+
}
5201+
5202+
return qfalse;
50885203
}
50895204

50905205

code/client/client.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ void Com_DL_Cleanup( download_t *dl );
372372
qboolean Com_DL_Begin( download_t *dl, const char *localName, const char *remoteURL, qboolean autoDownload );
373373
qboolean Com_DL_InProgress( const download_t *dl );
374374
qboolean Com_DL_ValidFileName( const char *fileName );
375+
int Com_DL_SpeedTest( download_t *dl, const char *url1, const char *url2 );
375376
qboolean CL_Download( const char *cmd, const char *pakname, qboolean autoDownload );
376377

377378
#endif

0 commit comments

Comments
 (0)