@@ -68,6 +68,7 @@ cvar_t *cl_lanForcePackets;
6868cvar_t * cl_guidServerUniq ;
6969
7070cvar_t * dl_source ;
71+ cvar_t * dl_source2 ;
7172cvar_t * dl_usebaseq3 ;
7273
7374cvar_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+
50465121qboolean 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
0 commit comments