/* gameinfo.c - gameinfo.txt / liblist.gam parsing for filesystem Copyright (C) 2003-2006 Mathieu Olivier Copyright (C) 2000-2007 DarkPlaces contributors Copyright (C) 2007 Uncle Mike Copyright (C) 2015-2023 Xash3D FWGS contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. */ #include "build.h" #include #include #include "defaults.h" #include "crtlib.h" #include "filesystem.h" #include "filesystem_internal.h" #include "xash3d_mathlib.h" #include "common/com_strings.h" #include "common/protocol.h" #include "library_suffix.h" #define SAVE_AGED_COUNT 2 // the default count of quick and auto saves /* ================ FS_WriteGameInfo assume GameInfo is valid ================ */ static qboolean FS_WriteGameInfo( const char *filepath, const gameinfo_t *GameInfo ) { file_t *f = FS_Open( filepath, "w", false ); // we in binary-mode int write_ambients = false; if( !f ) return false; FS_Printf( f, "// generated by " XASH_ENGINE_NAME " " XASH_VERSION "-%s (%s-%s)\n\n\n", g_buildcommit, Q_buildos(), Q_buildarch() ); if( !COM_StringEmpty( GameInfo->basedir )) FS_Printf( f, "basedir\t\t\"%s\"\n", GameInfo->basedir ); // DEPRECATED: gamedir key isn't supported by FWGS fork // but write it anyway to keep compability with original Xash3D if( !COM_StringEmpty( GameInfo->gamefolder )) FS_Printf( f, "gamedir\t\t\"%s\"\n", GameInfo->gamefolder ); if( !COM_StringEmpty( GameInfo->falldir )) FS_Printf( f, "fallback_dir\t\"%s\"\n", GameInfo->falldir ); if( !COM_StringEmpty( GameInfo->title )) FS_Printf( f, "title\t\t\"%s\"\n", GameInfo->title ); if( !COM_StringEmpty( GameInfo->startmap )) FS_Printf( f, "startmap\t\t\"%s\"\n", GameInfo->startmap ); if( !COM_StringEmpty( GameInfo->trainmap )) FS_Printf( f, "trainmap\t\t\"%s\"\n", GameInfo->trainmap ); if( GameInfo->version != 0.0f ) FS_Printf( f, "version\t\t%g\n", GameInfo->version ); if( GameInfo->size != 0 ) FS_Printf( f, "size\t\t%zu\n", GameInfo->size ); if( !COM_StringEmpty( GameInfo->game_url )) FS_Printf( f, "url_info\t\t\"%s\"\n", GameInfo->game_url ); if( !COM_StringEmpty( GameInfo->update_url )) FS_Printf( f, "url_update\t\t\"%s\"\n", GameInfo->update_url ); if( !COM_StringEmpty( GameInfo->type )) FS_Printf( f, "type\t\t\"%s\"\n", GameInfo->type ); if( !COM_StringEmpty( GameInfo->date )) FS_Printf( f, "date\t\t\"%s\"\n", GameInfo->date ); if( !COM_StringEmpty( GameInfo->dll_path )) FS_Printf( f, "dllpath\t\t\"%s\"\n", GameInfo->dll_path ); if( !COM_StringEmpty( GameInfo->game_dll )) FS_Printf( f, "gamedll\t\t\"%s\"\n", GameInfo->game_dll ); if( !COM_StringEmpty( GameInfo->game_dll_linux )) FS_Printf( f, "gamedll_linux\t\t\"%s\"\n", GameInfo->game_dll_linux ); if( !COM_StringEmpty( GameInfo->game_dll_osx )) FS_Printf( f, "gamedll_osx\t\t\"%s\"\n", GameInfo->game_dll_osx ); if( !COM_StringEmpty( GameInfo->iconpath )) FS_Printf( f, "icon\t\t\"%s\"\n", GameInfo->iconpath ); switch( GameInfo->gamemode ) { case 1: FS_Print( f, "gamemode\t\t\"singleplayer_only\"\n" ); break; case 2: FS_Print( f, "gamemode\t\t\"multiplayer_only\"\n" ); break; } if( !COM_StringEmpty( GameInfo->sp_entity )) FS_Printf( f, "sp_entity\t\t\"%s\"\n", GameInfo->sp_entity ); if( !COM_StringEmpty( GameInfo->mp_entity )) FS_Printf( f, "mp_entity\t\t\"%s\"\n", GameInfo->mp_entity ); if( !COM_StringEmpty( GameInfo->mp_filter )) FS_Printf( f, "mp_filter\t\t\"%s\"\n", GameInfo->mp_filter ); if( GameInfo->secure ) FS_Printf( f, "secure\t\t\"%i\"\n", GameInfo->secure ); if( GameInfo->nomodels ) FS_Printf( f, "nomodels\t\t\"%i\"\n", GameInfo->nomodels ); if( GameInfo->max_edicts > 0 ) FS_Printf( f, "max_edicts\t%i\n", GameInfo->max_edicts ); if( GameInfo->max_tents > 0 ) FS_Printf( f, "max_tempents\t%i\n", GameInfo->max_tents ); if( GameInfo->max_beams > 0 ) FS_Printf( f, "max_beams\t\t%i\n", GameInfo->max_beams ); if( GameInfo->max_particles > 0 ) FS_Printf( f, "max_particles\t%i\n", GameInfo->max_particles ); for( int i = 0; i < NUM_AMBIENTS; i++ ) { if( *GameInfo->ambientsound[i] ) { if( !write_ambients ) { FS_Print( f, "\n" ); write_ambients = true; } FS_Printf( f, "ambient%i\t\t%s\n", i, GameInfo->ambientsound[i] ); } } if( GameInfo->noskills ) FS_Printf( f, "noskills\t\t\"%i\"\n", GameInfo->noskills ); if( GameInfo->quicksave_aged_count != SAVE_AGED_COUNT ) FS_Printf( f, "quicksave_aged_count\t\t%d\n", GameInfo->quicksave_aged_count ); if( GameInfo->autosave_aged_count != SAVE_AGED_COUNT ) FS_Printf( f, "autosave_aged_count\t\t%d\n", GameInfo->autosave_aged_count ); // HL25 compatibility if( GameInfo->animated_title ) FS_Printf( f, "animated_title\t\t%i\n", GameInfo->animated_title ); if( GameInfo->hd_background ) FS_Printf( f, "hd_background\t\t%i\n", GameInfo->hd_background ); // always expose our extensions :) FS_Printf( f, "internal_vgui_support\t\t%i\n", GameInfo->internal_vgui_support ); FS_Printf( f, "render_picbutton_text\t\t%i\n", GameInfo->render_picbutton_text ); if( !COM_StringEmpty( GameInfo->demomap )) FS_Printf( f, "demomap\t\t\"%s\"\n", GameInfo->demomap ); FS_Close( f ); // all done return true; } void FS_MakeGameInfo( void ) { if( FS_WriteGameInfo( "gameinfo.txt", FI.GameInfo )) Con_Printf( "Successfully generated %s/gameinfo.txt\n", FI.GameInfo->gamefolder ); else Con_Printf( S_ERROR "Can't open %s/gameinfo.txt for write\n", FI.GameInfo->gamefolder ); } static void FS_InitGameInfo( gameinfo_t *GameInfo, const char *gamedir, qboolean quake, time_t mtime ) { memset( GameInfo, 0, sizeof( *GameInfo )); // filesystem info GameInfo->mtime = mtime; Q_strncpy( GameInfo->gamefolder, gamedir, sizeof( GameInfo->gamefolder )); Q_strncpy( GameInfo->sp_entity, "info_player_start", sizeof( GameInfo->sp_entity )); Q_strncpy( GameInfo->mp_entity, "info_player_deathmatch", sizeof( GameInfo->mp_entity )); Q_strncpy( GameInfo->iconpath, "game.ico", sizeof( GameInfo->iconpath )); if( quake ) { Q_strncpy( GameInfo->basedir, "id1", sizeof( GameInfo->basedir )); Q_strncpy( GameInfo->falldir, "qwrap", sizeof( GameInfo->falldir )); Q_strncpy( GameInfo->title, gamedir, sizeof( GameInfo->title )); Q_strncpy( GameInfo->startmap, "start", sizeof( GameInfo->startmap )); Q_strncpy( GameInfo->dll_path, "bin", sizeof( GameInfo->dll_path )); Q_strncpy( GameInfo->game_dll, "bin/progs.dll", sizeof( GameInfo->game_dll )); Q_strncpy( GameInfo->game_dll_linux, "bin/progs.so", sizeof( GameInfo->game_dll_linux )); Q_strncpy( GameInfo->game_dll_osx, "bin/progs.dylib", sizeof( GameInfo->game_dll_osx )); } else { Q_strncpy( GameInfo->basedir, fs_basedir, sizeof( GameInfo->basedir )); Q_strncpy( GameInfo->title, gamedir, sizeof( GameInfo->title )); Q_strncpy( GameInfo->startmap, "c0a0", sizeof( GameInfo->startmap )); Q_strncpy( GameInfo->dll_path, "cl_dlls", sizeof( GameInfo->dll_path )); Q_strncpy( GameInfo->game_dll, "dlls/hl.dll", sizeof( GameInfo->game_dll )); Q_strncpy( GameInfo->game_dll_linux, "dlls/hl.so", sizeof( GameInfo->game_dll_linux )); Q_strncpy( GameInfo->game_dll_osx, "dlls/hl.dylib", sizeof( GameInfo->game_dll_osx )); } GameInfo->max_edicts = DEFAULT_MAX_EDICTS; // default value if not specified GameInfo->max_tents = 500; GameInfo->max_beams = 128; GameInfo->max_particles = 4096; GameInfo->version = 1.0f; GameInfo->quicksave_aged_count = SAVE_AGED_COUNT; GameInfo->autosave_aged_count = SAVE_AGED_COUNT; } static void FS_ParseGenericGameInfo( gameinfo_t *GameInfo, const char *buf, const qboolean isGameInfo ) { char *pfile = (char*) buf; qboolean found_linux = false, found_osx = false; string token; while(( pfile = COM_ParseFile( pfile, token, sizeof( token ))) != NULL ) { // different names in liblist/gameinfo if( !Q_stricmp( token, isGameInfo ? "title" : "game" )) { pfile = COM_ParseFile( pfile, GameInfo->title, sizeof( GameInfo->title )); } // valid for both else if( !Q_stricmp( token, "fallback_dir" )) { pfile = COM_ParseFile( pfile, GameInfo->falldir, sizeof( GameInfo->falldir )); } // valid for both else if( !Q_stricmp( token, "startmap" )) { pfile = COM_ParseFile( pfile, GameInfo->startmap, sizeof( GameInfo->startmap )); COM_StripExtension( GameInfo->startmap ); // HQ2:Amen has extension .bsp } // only trainmap is valid for gameinfo else if( !Q_stricmp( token, "trainmap" ) || (!isGameInfo && !Q_stricmp( token, "trainingmap" ))) { pfile = COM_ParseFile( pfile, GameInfo->trainmap, sizeof( GameInfo->trainmap )); COM_StripExtension( GameInfo->trainmap ); // HQ2:Amen has extension .bsp } // valid for both else if( !Q_stricmp( token, "url_info" )) { pfile = COM_ParseFile( pfile, GameInfo->game_url, sizeof( GameInfo->game_url )); } // different names else if( !Q_stricmp( token, isGameInfo ? "url_update" : "url_dl" )) { pfile = COM_ParseFile( pfile, GameInfo->update_url, sizeof( GameInfo->update_url )); } // valid for both else if( !Q_stricmp( token, "gamedll" )) { pfile = COM_ParseFile( pfile, GameInfo->game_dll, sizeof( GameInfo->game_dll )); COM_FixSlashes( GameInfo->game_dll ); } // valid for both else if( !Q_stricmp( token, "gamedll_linux" )) { pfile = COM_ParseFile( pfile, GameInfo->game_dll_linux, sizeof( GameInfo->game_dll_linux )); found_linux = true; } // valid for both else if( !Q_stricmp( token, "gamedll_osx" )) { pfile = COM_ParseFile( pfile, GameInfo->game_dll_osx, sizeof( GameInfo->game_dll_osx )); found_osx = true; } // valid for both else if( !Q_stricmp( token, "icon" )) { pfile = COM_ParseFile( pfile, GameInfo->iconpath, sizeof( GameInfo->iconpath )); COM_FixSlashes( GameInfo->iconpath ); COM_DefaultExtension( GameInfo->iconpath, ".ico", sizeof( GameInfo->iconpath )); } else if( !Q_stricmp( token, "type" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); if( isGameInfo ) { Q_strncpy( GameInfo->type, token, sizeof( GameInfo->type )); } else { if( !Q_stricmp( token, "singleplayer_only" )) { // TODO: Remove this ugly hack too. // This was made because Half-Life has multiplayer, // but for some reason it's marked as singleplayer_only. // Old WON version is fine. if( !Q_stricmp( GameInfo->gamefolder, "valve") ) GameInfo->gamemode = GAME_NORMAL; else GameInfo->gamemode = GAME_SINGLEPLAYER_ONLY; Q_strncpy( GameInfo->type, "Single", sizeof( GameInfo->type )); } else if( !Q_stricmp( token, "multiplayer_only" )) { GameInfo->gamemode = GAME_MULTIPLAYER_ONLY; Q_strncpy( GameInfo->type, "Multiplayer", sizeof( GameInfo->type )); } else { // pass type without changes if( !isGameInfo ) GameInfo->gamemode = GAME_NORMAL; Q_strncpy( GameInfo->type, token, sizeof( GameInfo->type )); } } } // valid for both else if( !Q_stricmp( token, "version" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); GameInfo->version = Q_atof( token ); } // valid for both else if( !Q_stricmp( token, "size" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); GameInfo->size = Q_atoi( token ); } else if( !Q_stricmp( token, isGameInfo ? "mp_entity" : "mpentity" )) { pfile = COM_ParseFile( pfile, GameInfo->mp_entity, sizeof( GameInfo->mp_entity )); } else if( !Q_stricmp( token, isGameInfo ? "mp_filter" : "mpfilter" )) { pfile = COM_ParseFile( pfile, GameInfo->mp_filter, sizeof( GameInfo->mp_filter )); } // valid for both else if( !Q_stricmp( token, "secure" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); GameInfo->secure = Q_atoi( token ) ? true : false; } // valid for both else if( !Q_stricmp( token, "nomodels" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); GameInfo->nomodels = Q_atoi( token ) ? true : false; } else if( !Q_stricmp( token, isGameInfo ? "max_edicts" : "edicts" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); GameInfo->max_edicts = bound( MIN_EDICTS, Q_atoi( token ), MAX_EDICTS ); } // valid for both else if( !Q_stricmp( token, "hd_background" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); GameInfo->hd_background = Q_atoi( token ) ? true : false; } else if( !Q_stricmp( token, "animated_title" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); GameInfo->animated_title = Q_atoi( token ) ? true : false; } // only for gameinfo else if( isGameInfo ) { if( !Q_stricmp( token, "basedir" )) { string fs_path; pfile = COM_ParseFile( pfile, fs_path, sizeof( fs_path )); if( Q_stricmp( fs_path, GameInfo->basedir ) || Q_stricmp( fs_path, GameInfo->gamefolder )) Q_strncpy( GameInfo->basedir, fs_path, sizeof( GameInfo->basedir )); } else if( !Q_stricmp( token, "sp_entity" )) { pfile = COM_ParseFile( pfile, GameInfo->sp_entity, sizeof( GameInfo->sp_entity )); } else if( isGameInfo && !Q_stricmp( token, "dllpath" )) { pfile = COM_ParseFile( pfile, GameInfo->dll_path, sizeof( GameInfo->dll_path )); } else if( isGameInfo && !Q_stricmp( token, "date" )) { pfile = COM_ParseFile( pfile, GameInfo->date, sizeof( GameInfo->date )); } else if( !Q_stricmp( token, "max_tempents" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); GameInfo->max_tents = bound( 300, Q_atoi( token ), 2048 ); } else if( !Q_stricmp( token, "max_beams" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); GameInfo->max_beams = bound( 64, Q_atoi( token ), 512 ); } else if( !Q_stricmp( token, "max_particles" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); GameInfo->max_particles = bound( 1024, Q_atoi( token ), 131072 ); } else if( !Q_stricmp( token, "gamemode" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); // TODO: Remove this ugly hack too. // This was made because Half-Life has multiplayer, // but for some reason it's marked as singleplayer_only. // Old WON version is fine. if( !Q_stricmp( token, "singleplayer_only" ) && Q_stricmp( GameInfo->gamefolder, "valve") ) GameInfo->gamemode = GAME_SINGLEPLAYER_ONLY; else if( !Q_stricmp( token, "multiplayer_only" )) GameInfo->gamemode = GAME_MULTIPLAYER_ONLY; } else if( !Q_strnicmp( token, "ambient", 7 )) { int ambientNum = Q_atoi( token + 7 ); if( ambientNum < 0 || ambientNum >= NUM_AMBIENTS ) ambientNum = 0; pfile = COM_ParseFile( pfile, GameInfo->ambientsound[ambientNum], sizeof( GameInfo->ambientsound[ambientNum] )); } else if( !Q_stricmp( token, "noskills" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); GameInfo->noskills = Q_atoi( token ) ? true : false; } else if( !Q_stricmp( token, "render_picbutton_text" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); GameInfo->render_picbutton_text = Q_atoi( token ) ? true : false; } else if( !Q_stricmp( token, "internal_vgui_support" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); GameInfo->internal_vgui_support = Q_atoi( token ) ? true : false; } else if( !Q_stricmp( token, "quicksave_aged_count" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); GameInfo->quicksave_aged_count = bound( 2, Q_atoi( token ), 99 ); } else if( !Q_stricmp( token, "autosave_aged_count" )) { pfile = COM_ParseFile( pfile, token, sizeof( token )); GameInfo->autosave_aged_count = bound( 2, Q_atoi( token ), 99 ); } else if( !Q_stricmp( token, "demomap" )) { pfile = COM_ParseFile( pfile, GameInfo->demomap, sizeof( GameInfo->demomap )); } } } // demomap only valid for gameinfo.txt but HL1 after 25th anniversary update // comes with demo chapter. Set the demomap here. if( COM_StringEmpty( GameInfo->demomap )) { if( !Q_stricmp( GameInfo->title, "Half-Life" )) // original check from GameUI Q_strncpy( GameInfo->demomap, "hldemo1", sizeof( GameInfo->demomap )); } if( !found_linux || !found_osx ) { // just replace extension from dll to so/dylib char gamedll[64]; Q_strncpy( gamedll, GameInfo->game_dll, sizeof( gamedll )); COM_StripExtension( gamedll ); if( !found_linux ) Q_snprintf( GameInfo->game_dll_linux, sizeof( GameInfo->game_dll_linux ), "%s.so", gamedll ); if( !found_osx ) Q_snprintf( GameInfo->game_dll_osx, sizeof( GameInfo->game_dll_osx ), "%s.dylib", gamedll ); } // make sure what gamedir is really exist // a1ba: why we are doing this??? Q_snprintf( token, sizeof( token ), "%s/%s", fs_rootdir, GameInfo->falldir ); if( !FS_SysFolderExists( token )) { if( !COM_StringEmpty( fs_rodir )) { Q_snprintf( token, sizeof( token ), "%s/%s", fs_rodir, GameInfo->falldir ); if( !FS_SysFolderExists( token )) GameInfo->falldir[0] = 0; } else GameInfo->falldir[0] = 0; } } /* ================ FS_ParseLiblistGam ================ */ static qboolean FS_ParseLiblistGam( const char *filename, const char *gamedir, gameinfo_t *GameInfo, time_t mtime ) { char *afile = FS_LoadDirectFile( filename, NULL ); if( !afile ) return false; FS_InitGameInfo( GameInfo, gamedir, false, mtime ); FS_ParseGenericGameInfo( GameInfo, afile, false ); Mem_Free( afile ); return true; } /* ================ FS_ConvertGameInfo ================ */ static qboolean FS_ConvertGameInfo( const char *gamedir, const char *gameinfo_path, const char *liblist_path, gameinfo_t *gi, time_t liblist_mtime ) { memset( gi, 0, sizeof( *gi )); // liblist.gam to gameinfo.txt conversion is deprecated, only support for RwDir! if( FS_ParseLiblistGam( liblist_path, gamedir, gi, liblist_mtime )) { Con_DPrintf( "Convert %s to %s\n", liblist_path, gameinfo_path ); return FS_WriteGameInfo( gameinfo_path, gi ); } return false; } /* ================ FS_ReadGameInfo ================ */ static qboolean FS_ReadGameInfo( const char *filename, const char *gamedir, gameinfo_t *GameInfo, time_t mtime ) { char *afile = FS_LoadDirectFile( filename, NULL ); if( !afile ) return false; FS_InitGameInfo( GameInfo, gamedir, false, mtime ); FS_ParseGenericGameInfo( GameInfo, afile, true ); Mem_Free( afile ); return true; } /* ================ FS_CheckForQuakeGameDir Checks if game directory resembles Quake Engine game directory (some of checks may as well work with Xash gamedirs, it's not a bug) ================ */ static qboolean FS_CheckForQuakeGameDir( const char *gamedir ) { // if directory contain quake.rc or progs.dat it's 100% quake gamedir // quake mods probably always archived, so check pak0.pak too const char *files[] = { "pak0.pak", "PAK0.PAK", "progs.dat", "quake.rc" }; // search it in the filesystem for( int i = 0; i < sizeof( files ) / sizeof( files[0] ); i++ ) { char buf[MAX_SYSPATH]; if( Q_snprintf( buf, sizeof( buf ), "%s/%s", gamedir, files[i] ) > 0 ) { if( !FS_SysFileExists( buf )) continue; if( !Q_stricmp( COM_FileExtension( buf ), "pak" )) { if( FS_CheckForQuakePak( buf, &files[2], sizeof( files ) / sizeof( files[0] ) - 2 )) return true; } else { return true; } } } return false; } /* =============== FS_CheckForXashGameDir Checks if game directory resembles Xash3D game directory =============== */ static qboolean FS_CheckForXashGameDir( const char *gamedir ) { // if directory contain gameinfo.txt or liblist.gam it's 100% gamedir const char *files[] = { "gameinfo.txt", "liblist.gam" }; for( int i = 0; i < sizeof( files ) / sizeof( files[0] ); i++ ) { char buf[MAX_SYSPATH]; if( Q_snprintf( buf, sizeof( buf ), "%s/%s", gamedir, files[i] ) > 0 ) { if( FS_SysFileExists( buf )) return true; } } return false; } /* ================ FS_ParseGameInfo ================ */ qboolean FS_ParseGameInfo( const char *gamedir, gameinfo_t *GameInfo, qboolean rodir ) { char liblist_path[MAX_SYSPATH]; char gameinfo_path[MAX_SYSPATH]; char gamedir_path[MAX_SYSPATH]; time_t liblist_mtime; time_t gameinfo_mtime; if( rodir ) Q_snprintf( gamedir_path, sizeof( gamedir_path ), "%s/%s", fs_rodir, gamedir ); else Q_snprintf( gamedir_path, sizeof( gamedir_path ), "%s", gamedir ); if( !FS_CheckForXashGameDir( gamedir_path )) { // check if we need to generate gameinfo for Quake if( FS_CheckForQuakeGameDir( gamedir_path )) { // just generate stub gameinfo in memory FS_InitGameInfo( GameInfo, gamedir, true, -1 ); GameInfo->rodir = rodir; return true; } // don't add empty or addon directories return false; } Q_snprintf( gameinfo_path, sizeof( gameinfo_path ), "%s/gameinfo.txt", gamedir_path ); Q_snprintf( liblist_path, sizeof( liblist_path ), "%s/liblist.gam", gamedir_path ); liblist_mtime = FS_SysFileTime( liblist_path ); gameinfo_mtime = FS_SysFileTime( gameinfo_path ); // in this function we never write new files for RoDir compatibility // since RoDir is only FWGS feature, do gameinfo.txt conversion only // for RwDir for those who worked with original Xash3D if( !rodir ) { // !!!only if we have both liblist.gam and gameinfo.txt try to convert liblist.gam to gameinfo.txt if it's newer!!! if( liblist_mtime >= 0 && gameinfo_mtime >= 0 && liblist_mtime > gameinfo_mtime ) { if( FS_ConvertGameInfo( gamedir, gameinfo_path, liblist_path, GameInfo, liblist_mtime )) return true; } } // can we parse gameinfo.txt? if( gameinfo_mtime >= 0 ) { if( FS_ReadGameInfo( gameinfo_path, gamedir, GameInfo, gameinfo_mtime )) { GameInfo->rodir = rodir; return true; } } // can we parse liblist.gam? if( liblist_mtime >= 0 ) { if( FS_ParseLiblistGam( liblist_path, gamedir, GameInfo, liblist_mtime )) { GameInfo->rodir = rodir; return true; } } return false; }