/* sv_save.c - save\restore implementation Copyright (C) 2008 Uncle Mike 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 "common.h" #include "server.h" #include "library.h" #include "const.h" #include "render_api.h" // decallist_t #include "sound.h" // S_GetDynamicSounds #include "ref_common.h" // decals /* ============================================================================== SAVE FILE half-life implementation of saverestore system ============================================================================== */ #define SAVEFILE_HEADER (('V'<<24)+('L'<<16)+('A'<<8)+'V') // little-endian "VALV" #define SAVEGAME_HEADER (('V'<<24)+('A'<<16)+('S'<<8)+'J') // little-endian "JSAV" #define SAVEGAME_VERSION 0x0071 // Version 0.71 GoldSrc compatible #define CLIENT_SAVEGAME_VERSION 0x0067 // Version 0.67 #define SAVE_HEAPSIZE 0x400000 // reserve 4Mb for now #define SAVE_HASHSTRINGS 0xFFF // 4095 unique strings #define SAVE_AGED_COUNT 2 // savedata headers typedef struct { char mapName[32]; char comment[80]; int mapCount; } GAME_HEADER; typedef struct { int skillLevel; int entityCount; int connectionCount; int lightStyleCount; float time; char mapName[32]; char skyName[32]; int skyColor_r; int skyColor_g; int skyColor_b; float skyVec_x; float skyVec_y; float skyVec_z; } SAVE_HEADER; typedef struct { int decalCount; // render decals count int entityCount; // static entity count int soundCount; // sounds count int tempEntsCount; // not used char introTrack[64]; char mainTrack[64]; int trackPosition; short viewentity; // Xash3D added float wateralpha; float wateramp; // world waves } SAVE_CLIENT; typedef struct { int index; char style[256]; float time; } SAVE_LIGHTSTYLE; void (__cdecl *pfnSaveGameComment)( char *buffer, int max_length ) = NULL; static TYPEDESCRIPTION gGameHeader[] = { DEFINE_ARRAY( GAME_HEADER, mapName, FIELD_CHARACTER, 32 ), DEFINE_ARRAY( GAME_HEADER, comment, FIELD_CHARACTER, 80 ), DEFINE_FIELD( GAME_HEADER, mapCount, FIELD_INTEGER ), }; static TYPEDESCRIPTION gSaveHeader[] = { DEFINE_FIELD( SAVE_HEADER, skillLevel, FIELD_INTEGER ), DEFINE_FIELD( SAVE_HEADER, entityCount, FIELD_INTEGER ), DEFINE_FIELD( SAVE_HEADER, connectionCount, FIELD_INTEGER ), DEFINE_FIELD( SAVE_HEADER, lightStyleCount, FIELD_INTEGER ), DEFINE_FIELD( SAVE_HEADER, time, FIELD_TIME ), DEFINE_ARRAY( SAVE_HEADER, mapName, FIELD_CHARACTER, 32 ), DEFINE_ARRAY( SAVE_HEADER, skyName, FIELD_CHARACTER, 32 ), DEFINE_FIELD( SAVE_HEADER, skyColor_r, FIELD_INTEGER ), DEFINE_FIELD( SAVE_HEADER, skyColor_g, FIELD_INTEGER ), DEFINE_FIELD( SAVE_HEADER, skyColor_b, FIELD_INTEGER ), DEFINE_FIELD( SAVE_HEADER, skyVec_x, FIELD_FLOAT ), DEFINE_FIELD( SAVE_HEADER, skyVec_y, FIELD_FLOAT ), DEFINE_FIELD( SAVE_HEADER, skyVec_z, FIELD_FLOAT ), }; static TYPEDESCRIPTION gAdjacency[] = { DEFINE_ARRAY( LEVELLIST, mapName, FIELD_CHARACTER, 32 ), DEFINE_ARRAY( LEVELLIST, landmarkName, FIELD_CHARACTER, 32 ), DEFINE_FIELD( LEVELLIST, pentLandmark, FIELD_EDICT ), DEFINE_FIELD( LEVELLIST, vecLandmarkOrigin, FIELD_VECTOR ), }; static TYPEDESCRIPTION gLightStyle[] = { DEFINE_FIELD( SAVE_LIGHTSTYLE, index, FIELD_INTEGER ), DEFINE_ARRAY( SAVE_LIGHTSTYLE, style, FIELD_CHARACTER, 256 ), DEFINE_FIELD( SAVE_LIGHTSTYLE, time, FIELD_FLOAT ), }; static TYPEDESCRIPTION gEntityTable[] = { DEFINE_FIELD( ENTITYTABLE, id, FIELD_INTEGER ), DEFINE_FIELD( ENTITYTABLE, location, FIELD_INTEGER ), DEFINE_FIELD( ENTITYTABLE, size, FIELD_INTEGER ), DEFINE_FIELD( ENTITYTABLE, flags, FIELD_INTEGER ), DEFINE_FIELD( ENTITYTABLE, classname, FIELD_STRING ), }; static TYPEDESCRIPTION gSaveClient[] = { DEFINE_FIELD( SAVE_CLIENT, decalCount, FIELD_INTEGER ), DEFINE_FIELD( SAVE_CLIENT, entityCount, FIELD_INTEGER ), DEFINE_FIELD( SAVE_CLIENT, soundCount, FIELD_INTEGER ), DEFINE_FIELD( SAVE_CLIENT, tempEntsCount, FIELD_INTEGER ), DEFINE_ARRAY( SAVE_CLIENT, introTrack, FIELD_CHARACTER, 64 ), DEFINE_ARRAY( SAVE_CLIENT, mainTrack, FIELD_CHARACTER, 64 ), DEFINE_FIELD( SAVE_CLIENT, trackPosition, FIELD_INTEGER ), DEFINE_FIELD( SAVE_CLIENT, viewentity, FIELD_SHORT ), DEFINE_FIELD( SAVE_CLIENT, wateralpha, FIELD_FLOAT ), DEFINE_FIELD( SAVE_CLIENT, wateramp, FIELD_FLOAT ), }; static TYPEDESCRIPTION gDecalEntry[] = { DEFINE_FIELD( decallist_t, position, FIELD_VECTOR ), DEFINE_ARRAY( decallist_t, name, FIELD_CHARACTER, 64 ), DEFINE_FIELD( decallist_t, entityIndex, FIELD_SHORT ), DEFINE_FIELD( decallist_t, depth, FIELD_CHARACTER ), DEFINE_FIELD( decallist_t, flags, FIELD_CHARACTER ), DEFINE_FIELD( decallist_t, scale, FIELD_FLOAT ), DEFINE_FIELD( decallist_t, impactPlaneNormal, FIELD_VECTOR ), DEFINE_ARRAY( decallist_t, studio_state, FIELD_CHARACTER, sizeof( modelstate_t )), }; static TYPEDESCRIPTION gStaticEntry[] = { DEFINE_FIELD( entity_state_t, messagenum, FIELD_MODELNAME ), // HACKHACK: store model into messagenum DEFINE_FIELD( entity_state_t, origin, FIELD_VECTOR ), DEFINE_FIELD( entity_state_t, angles, FIELD_VECTOR ), DEFINE_FIELD( entity_state_t, sequence, FIELD_INTEGER ), DEFINE_FIELD( entity_state_t, frame, FIELD_FLOAT ), DEFINE_FIELD( entity_state_t, colormap, FIELD_INTEGER ), DEFINE_FIELD( entity_state_t, skin, FIELD_SHORT ), DEFINE_FIELD( entity_state_t, body, FIELD_INTEGER ), DEFINE_FIELD( entity_state_t, scale, FIELD_FLOAT ), DEFINE_FIELD( entity_state_t, effects, FIELD_INTEGER ), DEFINE_FIELD( entity_state_t, framerate, FIELD_FLOAT ), DEFINE_FIELD( entity_state_t, mins, FIELD_VECTOR ), DEFINE_FIELD( entity_state_t, maxs, FIELD_VECTOR ), DEFINE_FIELD( entity_state_t, rendermode, FIELD_INTEGER ), DEFINE_FIELD( entity_state_t, renderamt, FIELD_FLOAT ), DEFINE_ARRAY( entity_state_t, rendercolor, FIELD_CHARACTER, sizeof( color24 )), DEFINE_FIELD( entity_state_t, renderfx, FIELD_INTEGER ), DEFINE_FIELD( entity_state_t, controller, FIELD_INTEGER ), DEFINE_FIELD( entity_state_t, blending, FIELD_INTEGER ), DEFINE_FIELD( entity_state_t, solid, FIELD_SHORT ), DEFINE_FIELD( entity_state_t, animtime, FIELD_TIME ), DEFINE_FIELD( entity_state_t, movetype, FIELD_INTEGER ), DEFINE_FIELD( entity_state_t, vuser1, FIELD_VECTOR ), DEFINE_FIELD( entity_state_t, vuser2, FIELD_VECTOR ), DEFINE_FIELD( entity_state_t, vuser3, FIELD_VECTOR ), DEFINE_FIELD( entity_state_t, vuser4, FIELD_VECTOR ), DEFINE_FIELD( entity_state_t, iuser1, FIELD_INTEGER ), DEFINE_FIELD( entity_state_t, iuser2, FIELD_INTEGER ), DEFINE_FIELD( entity_state_t, iuser3, FIELD_INTEGER ), DEFINE_FIELD( entity_state_t, iuser4, FIELD_INTEGER ), DEFINE_FIELD( entity_state_t, fuser1, FIELD_FLOAT ), DEFINE_FIELD( entity_state_t, fuser2, FIELD_FLOAT ), DEFINE_FIELD( entity_state_t, fuser3, FIELD_FLOAT ), DEFINE_FIELD( entity_state_t, fuser4, FIELD_FLOAT ), }; static TYPEDESCRIPTION gSoundEntry[] = { DEFINE_ARRAY( soundlist_t, name, FIELD_CHARACTER, 64 ), DEFINE_FIELD( soundlist_t, entnum, FIELD_SHORT ), DEFINE_FIELD( soundlist_t, origin, FIELD_VECTOR ), DEFINE_FIELD( soundlist_t, volume, FIELD_FLOAT ), DEFINE_FIELD( soundlist_t, attenuation, FIELD_FLOAT ), DEFINE_FIELD( soundlist_t, looping, FIELD_BOOLEAN ), DEFINE_FIELD( soundlist_t, channel, FIELD_CHARACTER ), DEFINE_FIELD( soundlist_t, pitch, FIELD_CHARACTER ), DEFINE_FIELD( soundlist_t, wordIndex, FIELD_CHARACTER ), DEFINE_ARRAY( soundlist_t, samplePos, FIELD_CHARACTER, sizeof( double )), DEFINE_ARRAY( soundlist_t, forcedEnd, FIELD_CHARACTER, sizeof( double )), }; static TYPEDESCRIPTION gTempEntvars[] = { DEFINE_ENTITY_FIELD( classname, FIELD_STRING ), DEFINE_ENTITY_GLOBAL_FIELD( globalname, FIELD_STRING ), }; struct { const char *mapname; const char *titlename; } gTitleComments[] = { // default Half-Life map titles // ordering is important // strings hw.so| grep T0A0TITLE -B 50 -A 150 { "T0A0", "#T0A0TITLE" }, { "C0A0", "#C0A0TITLE" }, { "C1A0", "#C0A1TITLE" }, { "C1A1", "#C1A1TITLE" }, { "C1A2", "#C1A2TITLE" }, { "C1A3", "#C1A3TITLE" }, { "C1A4", "#C1A4TITLE" }, { "C2A1", "#C2A1TITLE" }, { "C2A2", "#C2A2TITLE" }, { "C2A3", "#C2A3TITLE" }, { "C2A4D", "#C2A4TITLE2" }, { "C2A4E", "#C2A4TITLE2" }, { "C2A4F", "#C2A4TITLE2" }, { "C2A4G", "#C2A4TITLE2" }, { "C2A4", "#C2A4TITLE1" }, { "C2A5", "#C2A5TITLE" }, { "C3A1", "#C3A1TITLE" }, { "C3A2", "#C3A2TITLE" }, { "C4A1A", "#C4A1ATITLE" }, { "C4A1B", "#C4A1ATITLE" }, { "C4A1C", "#C4A1ATITLE" }, { "C4A1D", "#C4A1ATITLE" }, { "C4A1E", "#C4A1ATITLE" }, { "C4A1", "#C4A1TITLE" }, { "C4A2", "#C4A2TITLE" }, { "C4A3", "#C4A3TITLE" }, { "C5A1", "#C5TITLE" }, { "OFBOOT", "#OF_BOOT0TITLE" }, { "OF0A", "#OF1A1TITLE" }, { "OF1A1", "#OF1A3TITLE" }, { "OF1A2", "#OF1A3TITLE" }, { "OF1A3", "#OF1A3TITLE" }, { "OF1A4", "#OF1A3TITLE" }, { "OF1A", "#OF1A5TITLE" }, { "OF2A1", "#OF2A1TITLE" }, { "OF2A2", "#OF2A1TITLE" }, { "OF2A3", "#OF2A1TITLE" }, { "OF2A", "#OF2A4TITLE" }, { "OF3A1", "#OF3A1TITLE" }, { "OF3A2", "#OF3A1TITLE" }, { "OF3A", "#OF3A3TITLE" }, { "OF4A1", "#OF4A1TITLE" }, { "OF4A2", "#OF4A1TITLE" }, { "OF4A3", "#OF4A1TITLE" }, { "OF4A", "#OF4A4TITLE" }, { "OF5A", "#OF5A1TITLE" }, { "OF6A1", "#OF6A1TITLE" }, { "OF6A2", "#OF6A1TITLE" }, { "OF6A3", "#OF6A1TITLE" }, { "OF6A4b", "#OF6A4TITLE" }, { "OF6A4", "#OF6A4TITLE" }, { "OF6A5", "#OF6A4TITLE" }, { "OF6A", "#OF6A4TITLE" }, { "OF7A", "#OF7A0TITLE" }, { "ba_tram", "#BA_TRAMTITLE" }, { "ba_security", "#BA_SECURITYTITLE" }, { "ba_main", "#BA_SECURITYTITLE" }, { "ba_elevator", "#BA_SECURITYTITLE" }, { "ba_canal", "#BA_CANALSTITLE" }, { "ba_yard", "#BA_YARDTITLE" }, { "ba_xen", "#BA_XENTITLE" }, { "ba_hazard", "#BA_HAZARD" }, { "ba_power", "#BA_POWERTITLE" }, { "ba_teleport1", "#BA_POWERTITLE" }, { "ba_teleport", "#BA_TELEPORTTITLE" }, { "ba_outro", "#BA_OUTRO" }, }; /* ============= SaveBuildComment build commentary for each savegame typically it writes world message and level time ============= */ static void SaveBuildComment( char *text, int maxlength ) { string comment; const char *pName = NULL; text[0] = '\0'; // clear if( pfnSaveGameComment != NULL ) { // get save comment from gamedll pfnSaveGameComment( comment, MAX_STRING ); pName = comment; } else { size_t i; const char *mapname = STRING( svgame.globals->mapname ); for( i = 0; i < ARRAYSIZE( gTitleComments ); i++ ) { // compare if strings are equal at beginning size_t len = strlen( gTitleComments[i].mapname ); if( !Q_strnicmp( mapname, gTitleComments[i].mapname, len )) { pName = gTitleComments[i].titlename; break; } } if( !pName ) { if( svgame.edicts->v.message != 0 ) { // trying to extract message from the world pName = STRING( svgame.edicts->v.message ); } else { // or use mapname pName = STRING( svgame.globals->mapname ); } } } Q_snprintf( text, maxlength, "%-64.64s %02d:%02d", pName, (int)(sv.time / 60.0 ), (int)fmod( sv.time, 60.0 )); } /* ============= DirectoryCount counting all the files with HL1-HL3 extension in save folder ============= */ static int DirectoryCount( const char *pPath ) { int count; search_t *t; t = FS_Search( pPath, true, true ); // lookup only in gamedir if( !t ) return 0; // empty count = t->numfilenames; Mem_Free( t ); return count; } /* ============= InitEntityTable reserve space for ETABLE's ============= */ static void InitEntityTable( SAVERESTOREDATA *pSaveData, int entityCount ) { ENTITYTABLE *pTable; int i; pSaveData->pTable = Mem_Calloc( host.mempool, sizeof( ENTITYTABLE ) * entityCount ); pSaveData->tableCount = entityCount; // setup entitytable for( i = 0; i < entityCount; i++ ) { pTable = &pSaveData->pTable[i]; pTable->pent = EDICT_NUM( i ); pTable->id = i; } } /* ============= EntryInTable check level in transition list ============= */ static int EntryInTable( SAVERESTOREDATA *pSaveData, const char *pMapName, int index ) { int i; for( i = index + 1; i < pSaveData->connectionCount; i++ ) { if ( !Q_stricmp( pSaveData->levelList[i].mapName, pMapName )) return i; } return -1; } /* ============= EdictFromTable get edict from table ============= */ static edict_t *EdictFromTable( SAVERESTOREDATA *pSaveData, int entityIndex ) { if( pSaveData && pSaveData->pTable ) { entityIndex = bound( 0, entityIndex, pSaveData->tableCount - 1 ); return pSaveData->pTable[entityIndex].pent; } return NULL; } /* ============= LandmarkOrigin find global offset for a given landmark ============= */ static void LandmarkOrigin( SAVERESTOREDATA *pSaveData, vec3_t output, const char *pLandmarkName ) { int i; for( i = 0; i < pSaveData->connectionCount; i++ ) { if( !Q_strcmp( pSaveData->levelList[i].landmarkName, pLandmarkName )) { VectorCopy( pSaveData->levelList[i].vecLandmarkOrigin, output ); return; } } VectorClear( output ); } /* ============= EntityInSolid some moved edicts on a next level cause stuck outside of world. Find them and remove ============= */ static int EntityInSolid( edict_t *pent ) { edict_t *aiment = pent->v.aiment; vec3_t point; // if you're attached to a client, always go through if( pent->v.movetype == MOVETYPE_FOLLOW && SV_IsValidEdict( aiment ) && FBitSet( aiment->v.flags, FL_CLIENT )) return 0; VectorAverage( pent->v.absmin, pent->v.absmax, point ); svs.groupmask = pent->v.groupinfo; return (SV_PointContents( point ) == CONTENTS_SOLID); } /* ============= ClearSaveDir remove all the temp files HL1-HL3 (it will be extracted again from another .sav file) ============= */ static void ClearSaveDir( void ) { search_t *t; int i; // just delete all HL? files t = FS_Search( DEFAULT_SAVE_DIRECTORY "*.HL?", true, true ); if( !t ) return; // already empty for( i = 0; i < t->numfilenames; i++ ) FS_Delete( t->filenames[i] ); Mem_Free( t ); } /* ============= IsValidSave savegame is allowed? ============= */ static int IsValidSave( void ) { if( !svs.initialized || sv.state != ss_active ) { Con_Printf( "Not playing a local game.\n" ); return 0; } // ignore autosave during background if( sv.background || UI_CreditsActive( )) return 0; if( svgame.physFuncs.SV_AllowSaveGame != NULL ) { if( !svgame.physFuncs.SV_AllowSaveGame( )) { Con_Printf( "Savegame is not allowed.\n" ); return 0; } } if( !CL_Active( )) { Con_Printf( "Can't save if not active.\n" ); return 0; } if( CL_IsIntermission( )) { Con_Printf( "Can't save during intermission.\n" ); return 0; } if( svs.maxclients != 1 ) { Con_Printf( "Can't save multiplayer games.\n" ); return 0; } if( svs.clients && svs.clients[0].state == cs_spawned ) { edict_t *pl = svs.clients[0].edict; if( !pl ) { Con_Printf( "Can't savegame without a player!\n" ); return 0; } if( pl->v.deadflag || pl->v.health <= 0.0f ) { Con_Printf( "Can't savegame with a dead player\n" ); return 0; } // Passed all checks, it's ok to save return 1; } Con_Printf( "Can't savegame without a client!\n" ); return 0; } /* ============= AgeSaveList scroll the name list down ============= */ static void AgeSaveList( const char *pName, int count ) { char newName[MAX_OSPATH], oldName[MAX_OSPATH]; char newShot[MAX_OSPATH], oldShot[MAX_OSPATH]; // delete last quick/autosave (e.g. quick05.sav) Q_snprintf( newName, sizeof( newName ), DEFAULT_SAVE_DIRECTORY "%s%02d.sav", pName, count ); Q_snprintf( newShot, sizeof( newShot ), DEFAULT_SAVE_DIRECTORY "%s%02d.bmp", pName, count ); // only delete from game directory, basedir is read-only FS_Delete( newName ); FS_Delete( newShot ); #if !XASH_DEDICATED // unloading the shot footprint GL_FreeImage( newShot ); #endif // XASH_DEDICATED while( count > 0 ) { if( count == 1 ) { // quick.sav Q_snprintf( oldName, sizeof( oldName ), DEFAULT_SAVE_DIRECTORY "%s.sav", pName ); Q_snprintf( oldShot, sizeof( oldShot ), DEFAULT_SAVE_DIRECTORY "%s.bmp", pName ); } else { // quick04.sav, etc. Q_snprintf( oldName, sizeof( oldName ), DEFAULT_SAVE_DIRECTORY "%s%02d.sav", pName, count - 1 ); Q_snprintf( oldShot, sizeof( oldShot ), DEFAULT_SAVE_DIRECTORY "%s%02d.bmp", pName, count - 1 ); } Q_snprintf( newName, sizeof( newName ), DEFAULT_SAVE_DIRECTORY "%s%02d.sav", pName, count ); Q_snprintf( newShot, sizeof( newShot ), DEFAULT_SAVE_DIRECTORY "%s%02d.bmp", pName, count ); #if !XASH_DEDICATED // unloading the oldshot footprint too GL_FreeImage( oldShot ); #endif // XASH_DEDICATED // scroll the name list down (e.g. rename quick04.sav to quick05.sav) FS_Rename( oldName, newName ); FS_Rename( oldShot, newShot ); count--; } } /* ============= DirectoryCopy put the HL1-HL3 files into .sav file ============= */ static void DirectoryCopy( const char *pPath, file_t *pFile ) { char szName[MAX_OSPATH]; int i, fileSize; file_t *pCopy; search_t *t; t = FS_Search( pPath, true, true ); if( !t ) return; // nothing to copy ? for( i = 0; i < t->numfilenames; i++ ) { pCopy = FS_Open( t->filenames[i], "rb", true ); fileSize = FS_FileLength( pCopy ); memset( szName, 0, sizeof( szName )); // clearing the string to prevent garbage in output file Q_strncpy( szName, COM_FileWithoutPath( t->filenames[i] ), MAX_OSPATH ); FS_Write( pFile, szName, MAX_OSPATH ); FS_Write( pFile, &fileSize, sizeof( int )); FS_FileCopy( pFile, pCopy, fileSize ); FS_Close( pCopy ); } Mem_Free( t ); } /* ============= DirectoryExtract extract the HL1-HL3 files from the .sav file ============= */ static void DirectoryExtract( file_t *pFile, int fileCount ) { char szName[MAX_OSPATH]; char fileName[MAX_OSPATH]; int i, fileSize; file_t *pCopy; for( i = 0; i < fileCount; i++ ) { // filename can only be as long as a map name + extension FS_Read( pFile, szName, MAX_OSPATH ); FS_Read( pFile, &fileSize, sizeof( int )); Q_snprintf( fileName, sizeof( fileName ), DEFAULT_SAVE_DIRECTORY "%s", szName ); COM_FixSlashes( fileName ); pCopy = FS_Open( fileName, "wb", true ); FS_FileCopy( pCopy, pFile, fileSize ); FS_Close( pCopy ); } } /* ============= SaveInit initialize global save-restore buffer ============= */ static SAVERESTOREDATA *SaveInit( int size, int tokenCount ) { SAVERESTOREDATA *pSaveData; pSaveData = Mem_Calloc( host.mempool, sizeof( SAVERESTOREDATA ) + size ); pSaveData->pTokens = (char **)Mem_Calloc( host.mempool, tokenCount * sizeof( char* )); pSaveData->tokenCount = tokenCount; pSaveData->pBaseData = (char *)(pSaveData + 1); // skip the save structure); pSaveData->pCurrentData = pSaveData->pBaseData; // reset the pointer pSaveData->bufferSize = size; pSaveData->time = svgame.globals->time; // Use DLL time // shared with dlls svgame.globals->pSaveData = pSaveData; return pSaveData; } /* ============= SaveClear clearing buffer for reuse ============= */ static void SaveClear( SAVERESTOREDATA *pSaveData ) { memset( pSaveData->pTokens, 0, pSaveData->tokenCount * sizeof( char* )); pSaveData->pBaseData = (char *)(pSaveData + 1); // skip the save structure); pSaveData->pCurrentData = pSaveData->pBaseData; // reset the pointer pSaveData->time = svgame.globals->time; // Use DLL time pSaveData->tokenSize = 0; // reset the hashtable pSaveData->size = 0; // reset the pointer // shared with dlls svgame.globals->pSaveData = pSaveData; } /* ============= SaveFinish release global save-restore buffer ============= */ static void SaveFinish( SAVERESTOREDATA *pSaveData ) { if( !pSaveData ) return; if( pSaveData->pTokens ) { Mem_Free( pSaveData->pTokens ); pSaveData->pTokens = NULL; pSaveData->tokenCount = 0; } if( pSaveData->pTable ) { Mem_Free( pSaveData->pTable ); pSaveData->pTable = NULL; pSaveData->tableCount = 0; } svgame.globals->pSaveData = NULL; Mem_Free( pSaveData ); } /* ============= DumpHashStrings debug thing ============= */ static void DumpHashStrings( SAVERESTOREDATA *pSaveData, const char *pMessage ) { int i, count = 0; if( pSaveData && pSaveData->pTokens ) { Con_Printf( "%s\n", pMessage ); for( i = 0; i < pSaveData->tokenCount; i++ ) { if( !pSaveData->pTokens[i] ) continue; Con_Printf( "#%i %s\n", count, pSaveData->pTokens[i] ); count++; } Con_Printf( "total %i actual %i\n", pSaveData->tokenCount, count ); } } /* ============= StoreHashTable write the stringtable into file ============= */ static char *StoreHashTable( SAVERESTOREDATA *pSaveData ) { char *pTokenData = pSaveData->pCurrentData; int i; // Write entity string token table if( pSaveData->pTokens ) { for( i = 0; i < pSaveData->tokenCount; i++ ) { const char *pszToken = pSaveData->pTokens[i] ? pSaveData->pTokens[i] : ""; // just copy the token byte-by-byte while( *pszToken ) *pSaveData->pCurrentData++ = *pszToken++; *pSaveData->pCurrentData++ = 0; // Write the term } } pSaveData->tokenSize = pSaveData->pCurrentData - pTokenData; return pTokenData; } /* ============= BuildHashTable build the stringtable from buffer ============= */ static void BuildHashTable( SAVERESTOREDATA *pSaveData, file_t *pFile ) { char *pszTokenList = pSaveData->pBaseData; int i; // Parse the symbol table if( pSaveData->tokenSize > 0 ) { FS_Read( pFile, pszTokenList, pSaveData->tokenSize ); // make sure the token strings pointed to by the pToken hashtable. for( i = 0; i < pSaveData->tokenCount; i++ ) { pSaveData->pTokens[i] = *pszTokenList ? pszTokenList : NULL; while( *pszTokenList++ ); // Find next token (after next null) } } // rebase the data pointer pSaveData->pBaseData = pszTokenList; // pszTokenList now points after token data pSaveData->pCurrentData = pSaveData->pBaseData; } /* ============= GetClientDataSize g-cont: this routine is redundant i'm write it just for more readable code ============= */ static int GetClientDataSize( const char *level ) { int tokenCount, tokenSize; int size, id, version; char name[MAX_QPATH]; file_t *pFile; Q_snprintf( name, sizeof( name ), DEFAULT_SAVE_DIRECTORY "%s.HL2", level ); if(( pFile = FS_Open( name, "rb", true )) == NULL ) return 0; FS_Read( pFile, &id, sizeof( id )); if( id != SAVEGAME_HEADER ) { FS_Close( pFile ); return 0; } FS_Read( pFile, &version, sizeof( version )); if( version != CLIENT_SAVEGAME_VERSION ) { FS_Close( pFile ); return 0; } FS_Read( pFile, &size, sizeof( int )); FS_Read( pFile, &tokenCount, sizeof( int )); FS_Read( pFile, &tokenSize, sizeof( int )); FS_Close( pFile ); return ( size + tokenSize ); } /* ============= LoadSaveData fill the save resore buffer parse hash strings ============= */ static SAVERESTOREDATA *LoadSaveData( const char *level ) { int tokenSize, tableCount; int size, tokenCount; char name[MAX_OSPATH]; int id, version; int clientSize; SAVERESTOREDATA *pSaveData; int totalSize; file_t *pFile; Q_snprintf( name, sizeof( name ), DEFAULT_SAVE_DIRECTORY "%s.HL1", level ); Con_Printf( "Loading game from %s...\n", name ); if(( pFile = FS_Open( name, "rb", true )) == NULL ) { Con_Printf( S_ERROR "Couldn't open save data file %s.\n", name ); return NULL; } // Read the header FS_Read( pFile, &id, sizeof( int )); FS_Read( pFile, &version, sizeof( int )); // is this a valid save? if( id != SAVEFILE_HEADER || version != SAVEGAME_VERSION ) { FS_Close( pFile ); return NULL; } // Read the sections info and the data FS_Read( pFile, &size, sizeof( int )); // total size of all data to initialize read buffer FS_Read( pFile, &tableCount, sizeof( int )); // entities count to right initialize entity table FS_Read( pFile, &tokenCount, sizeof( int )); // num hash tokens to prepare token table FS_Read( pFile, &tokenSize, sizeof( int )); // total size of hash tokens // determine highest size of seve-restore buffer // because it's used twice: for HL1 and HL2 restore clientSize = GetClientDataSize( level ); totalSize = Q_max( clientSize, ( size + tokenSize )); // init the read buffer pSaveData = SaveInit( totalSize, tokenCount ); Q_strncpy( pSaveData->szCurrentMapName, level, sizeof( pSaveData->szCurrentMapName )); pSaveData->tableCount = tableCount; // count ETABLE entries pSaveData->tokenCount = tokenCount; pSaveData->tokenSize = tokenSize; // Parse the symbol table BuildHashTable( pSaveData, pFile ); // Set up the restore basis pSaveData->fUseLandmark = true; pSaveData->time = 0.0f; // now reading all the rest of data FS_Read( pFile, pSaveData->pBaseData, size ); FS_Close( pFile ); // data is sucessfully moved into SaveRestore buffer (ETABLE will be init later) return pSaveData; } /* ============= ParseSaveTables reading global data, setup ETABLE's ============= */ static void ParseSaveTables( SAVERESTOREDATA *pSaveData, SAVE_HEADER *pHeader, int updateGlobals ) { SAVE_LIGHTSTYLE light; int i; // Re-base the savedata since we re-ordered the entity/table / restore fields InitEntityTable( pSaveData, pSaveData->tableCount ); for( i = 0; i < pSaveData->tableCount; i++ ) svgame.dllFuncs.pfnSaveReadFields( pSaveData, "ETABLE", &pSaveData->pTable[i], gEntityTable, ARRAYSIZE( gEntityTable )); pSaveData->pBaseData = pSaveData->pCurrentData; pSaveData->size = 0; // process SAVE_HEADER svgame.dllFuncs.pfnSaveReadFields( pSaveData, "Save Header", pHeader, gSaveHeader, ARRAYSIZE( gSaveHeader )); pSaveData->connectionCount = pHeader->connectionCount; VectorClear( pSaveData->vecLandmarkOffset ); pSaveData->time = pHeader->time; pSaveData->fUseLandmark = true; // read adjacency list for( i = 0; i < pSaveData->connectionCount; i++ ) svgame.dllFuncs.pfnSaveReadFields( pSaveData, "ADJACENCY", &pSaveData->levelList[i], gAdjacency, ARRAYSIZE( gAdjacency )); if( updateGlobals ) memset( sv.lightstyles, 0, sizeof( sv.lightstyles )); for( i = 0; i < pHeader->lightStyleCount; i++ ) { svgame.dllFuncs.pfnSaveReadFields( pSaveData, "LIGHTSTYLE", &light, gLightStyle, ARRAYSIZE( gLightStyle )); if( updateGlobals ) SV_SetLightStyle( light.index, light.style, light.time ); } } /* ============= EntityPatchWrite write out the list of entities that are no longer in the save file for this level (they've been moved to another level) ============= */ static void EntityPatchWrite( SAVERESTOREDATA *pSaveData, const char *level ) { char name[MAX_QPATH]; int i, size = 0; file_t *pFile; Q_snprintf( name, sizeof( name ), DEFAULT_SAVE_DIRECTORY "%s.HL3", level ); if(( pFile = FS_Open( name, "wb", true )) == NULL ) return; for( i = 0; i < pSaveData->tableCount; i++ ) { if( FBitSet( pSaveData->pTable[i].flags, FENTTABLE_REMOVED )) size++; } // patch count FS_Write( pFile, &size, sizeof( int )); for( i = 0; i < pSaveData->tableCount; i++ ) { if( FBitSet( pSaveData->pTable[i].flags, FENTTABLE_REMOVED )) FS_Write( pFile, &i, sizeof( int )); } FS_Close( pFile ); } /* ============= EntityPatchRead read the list of entities that are no longer in the save file for this level (they've been moved to another level) ============= */ static void EntityPatchRead( SAVERESTOREDATA *pSaveData, const char *level ) { char name[MAX_QPATH]; int i, size, entityId; file_t *pFile; Q_snprintf( name, sizeof( name ), DEFAULT_SAVE_DIRECTORY "%s.HL3", level ); if(( pFile = FS_Open( name, "rb", true )) == NULL ) return; // patch count FS_Read( pFile, &size, sizeof( int )); for( i = 0; i < size; i++ ) { FS_Read( pFile, &entityId, sizeof( int )); pSaveData->pTable[entityId].flags = FENTTABLE_REMOVED; } FS_Close( pFile ); } /* ============= RestoreDecal restore decal\move across transition ============= */ static void RestoreDecal( SAVERESTOREDATA *pSaveData, decallist_t *entry, qboolean adjacent ) { int decalIndex, entityIndex = 0; int flags = entry->flags; int modelIndex = 0; edict_t *pEdict; // never move permanent decals if( adjacent && FBitSet( flags, FDECAL_PERMANENT )) return; // restore entity and model index pEdict = EdictFromTable( pSaveData, entry->entityIndex ); if( SV_RestoreCustomDecal( entry, pEdict, adjacent )) return; // decal was sucessfully restored at the game-side // studio decals are handled at game-side if( FBitSet( flags, FDECAL_STUDIO )) return; if( SV_IsValidEdict( pEdict )) modelIndex = pEdict->v.modelindex; if( SV_IsValidEdict( pEdict )) entityIndex = NUM_FOR_EDICT( pEdict ); decalIndex = pfnDecalIndex( entry->name ); // this can happens if brush entity from previous level was turned into world geometry if( adjacent && entry->entityIndex != 0 && !SV_IsValidEdict( pEdict )) { vec3_t testspot, testend; trace_t tr; Con_Printf( S_ERROR "RestoreDecal: couldn't restore entity index %i\n", entry->entityIndex ); VectorCopy( entry->position, testspot ); VectorMA( testspot, 5.0f, entry->impactPlaneNormal, testspot ); VectorCopy( entry->position, testend ); VectorMA( testend, -5.0f, entry->impactPlaneNormal, testend ); tr = SV_Move( testspot, vec3_origin, vec3_origin, testend, MOVE_NOMONSTERS, NULL, false ); // NOTE: this code may does wrong result on moving brushes e.g. func_tracktrain if( tr.fraction != 1.0f && !tr.allsolid ) { // check impact plane normal float dot = DotProduct( entry->impactPlaneNormal, tr.plane.normal ); if( dot >= 0.95f ) { entityIndex = pfnIndexOfEdict( tr.ent ); if( entityIndex > 0 ) modelIndex = tr.ent->v.modelindex; SV_CreateDecal( &sv.signon, tr.endpos, decalIndex, entityIndex, modelIndex, flags, entry->scale ); } } } else { // global entity is exist on new level so we can apply decal in local space SV_CreateDecal( &sv.signon, entry->position, decalIndex, entityIndex, modelIndex, flags, entry->scale ); } } /* ============= RestoreSound continue playing sound from saved position ============= */ static void RestoreSound( SAVERESTOREDATA *pSaveData, soundlist_t *snd ) { edict_t *ent = EdictFromTable( pSaveData, snd->entnum ); int flags = SND_RESTORE_POSITION; // this can happens if serialized map contain 4096 static decals... if( MSG_GetNumBytesLeft( &sv.signon ) < 36 ) return; if( !snd->looping ) SetBits( flags, SND_STOP_LOOPING ); if( SV_BuildSoundMsg( &sv.signon, ent, snd->channel, snd->name, snd->volume * 255, snd->attenuation, flags, snd->pitch, snd->origin )) { // write extradata for svc_restoresound MSG_WriteByte( &sv.signon, snd->wordIndex ); MSG_WriteBytes( &sv.signon, &snd->samplePos, sizeof( snd->samplePos )); MSG_WriteBytes( &sv.signon, &snd->forcedEnd, sizeof( snd->forcedEnd )); } } /* ============= SaveClientState write out the list of premanent decals for this level ============= */ static void SaveClientState( SAVERESTOREDATA *pSaveData, const char *level, int changelevel ) { soundlist_t soundInfo[MAX_CHANNELS]; sv_client_t *cl = svs.clients; char name[MAX_QPATH]; int i, id, version; char *pTokenData; decallist_t *decalList; SAVE_CLIENT header; file_t *pFile; // clearing the saving buffer to reuse SaveClear( pSaveData ); memset( &header, 0, sizeof( header )); // g-cont. add space for studiodecals if present decalList = (decallist_t *)Z_Calloc( sizeof( decallist_t ) * MAX_RENDER_DECALS * 2 ); // initialize client header #if !XASH_DEDICATED if( !Host_IsDedicated() ) { header.decalCount = ref.dllFuncs.R_CreateDecalList( decalList ); } else #endif // XASH_DEDICATED { // we probably running a dedicated server header.decalCount = 0; } header.entityCount = sv.num_static_entities; if( !changelevel ) { // sounds won't going across transition header.soundCount = S_GetCurrentDynamicSounds( soundInfo, MAX_CHANNELS ); #if !XASH_DEDICATED // music not reqiured to save position: it's just continue playing on a next level S_StreamGetCurrentState( header.introTrack, header.mainTrack, &header.trackPosition ); #endif } // save viewentity to allow camera works after save\restore if( SV_IsValidEdict( cl->pViewEntity ) && cl->pViewEntity != cl->edict ) header.viewentity = NUM_FOR_EDICT( cl->pViewEntity ); header.wateralpha = sv_wateralpha.value; header.wateramp = sv_wateramp.value; // Store the client header svgame.dllFuncs.pfnSaveWriteFields( pSaveData, "ClientHeader", &header, gSaveClient, ARRAYSIZE( gSaveClient )); // store decals for( i = 0; i < header.decalCount; i++ ) { // NOTE: apply landmark offset only for brush entities without origin brushes if( pSaveData->fUseLandmark && FBitSet( decalList[i].flags, FDECAL_USE_LANDMARK )) VectorSubtract( decalList[i].position, pSaveData->vecLandmarkOffset, decalList[i].position ); svgame.dllFuncs.pfnSaveWriteFields( pSaveData, "DECALLIST", &decalList[i], gDecalEntry, ARRAYSIZE( gDecalEntry )); } Z_Free( decalList ); // write client entities for( i = 0; i < header.entityCount; i++ ) svgame.dllFuncs.pfnSaveWriteFields( pSaveData, "STATICENTITY", &svs.static_entities[i], gStaticEntry, ARRAYSIZE( gStaticEntry )); // write sounds for( i = 0; i < header.soundCount; i++ ) svgame.dllFuncs.pfnSaveWriteFields( pSaveData, "SOUNDLIST", &soundInfo[i], gSoundEntry, ARRAYSIZE( gSoundEntry )); // Write entity string token table pTokenData = StoreHashTable( pSaveData ); Q_snprintf( name, sizeof( name ), DEFAULT_SAVE_DIRECTORY "%s.HL2", level ); // output to disk if(( pFile = FS_Open( name, "wb", true )) == NULL ) return; // something bad is happens version = CLIENT_SAVEGAME_VERSION; id = SAVEGAME_HEADER; FS_Write( pFile, &id, sizeof( id )); FS_Write( pFile, &version, sizeof( version )); FS_Write( pFile, &pSaveData->size, sizeof( int )); // does not include token table // write out the tokens first so we can load them before we load the entities FS_Write( pFile, &pSaveData->tokenCount, sizeof( int )); FS_Write( pFile, &pSaveData->tokenSize, sizeof( int )); FS_Write( pFile, pTokenData, pSaveData->tokenSize ); FS_Write( pFile, pSaveData->pBaseData, pSaveData->size ); // header and globals FS_Close( pFile ); } /* ============= LoadClientState read the list of decals and reapply them again ============= */ static void LoadClientState( SAVERESTOREDATA *pSaveData, const char *level, qboolean changelevel, qboolean adjacent ) { int tokenCount, tokenSize; int i, size, id, version; sv_client_t *cl = svs.clients; char name[MAX_QPATH]; soundlist_t soundEntry; decallist_t decalEntry; SAVE_CLIENT header; file_t *pFile; Q_snprintf( name, sizeof( name ), DEFAULT_SAVE_DIRECTORY "%s.HL2", level ); if(( pFile = FS_Open( name, "rb", true )) == NULL ) return; // something bad is happens FS_Read( pFile, &id, sizeof( id )); if( id != SAVEGAME_HEADER ) { FS_Close( pFile ); return; } FS_Read( pFile, &version, sizeof( version )); if( version != CLIENT_SAVEGAME_VERSION ) { FS_Close( pFile ); return; } FS_Read( pFile, &size, sizeof( int )); FS_Read( pFile, &tokenCount, sizeof( int )); FS_Read( pFile, &tokenSize, sizeof( int )); // sanity check ASSERT( pSaveData->bufferSize >= ( size + tokenSize )); // clearing the restore buffer to reuse SaveClear( pSaveData ); pSaveData->tokenCount = tokenCount; pSaveData->tokenSize = tokenSize; // Parse the symbol table BuildHashTable( pSaveData, pFile ); FS_Read( pFile, pSaveData->pBaseData, size ); FS_Close( pFile ); // Read the client header svgame.dllFuncs.pfnSaveReadFields( pSaveData, "ClientHeader", &header, gSaveClient, ARRAYSIZE( gSaveClient )); // restore decals for( i = 0; i < header.decalCount; i++ ) { svgame.dllFuncs.pfnSaveReadFields( pSaveData, "DECALLIST", &decalEntry, gDecalEntry, ARRAYSIZE( gDecalEntry )); // NOTE: apply landmark offset only for brush entities without origin brushes if( pSaveData->fUseLandmark && FBitSet( decalEntry.flags, FDECAL_USE_LANDMARK )) VectorAdd( decalEntry.position, pSaveData->vecLandmarkOffset, decalEntry.position ); RestoreDecal( pSaveData, &decalEntry, adjacent ); } // clear old entities if( !adjacent ) { memset( svs.static_entities, 0, sizeof( entity_state_t ) * MAX_STATIC_ENTITIES ); sv.num_static_entities = 0; } // restore client entities for( i = 0; i < header.entityCount; i++ ) { id = sv.num_static_entities; svgame.dllFuncs.pfnSaveReadFields( pSaveData, "STATICENTITY", &svs.static_entities[id], gStaticEntry, ARRAYSIZE( gStaticEntry )); if( adjacent ) continue; // static entities won't loading from adjacent levels if( SV_CreateStaticEntity( &sv.signon, id )) sv.num_static_entities++; } // restore sounds for( i = 0; i < header.soundCount; i++ ) { svgame.dllFuncs.pfnSaveReadFields( pSaveData, "SOUNDLIST", &soundEntry, gSoundEntry, ARRAYSIZE( gSoundEntry )); if( adjacent ) continue; // sounds don't going across the levels RestoreSound( pSaveData, &soundEntry ); } if( !adjacent ) { // restore camera view here edict_t *pent = pSaveData->pTable[bound( 0, (word)header.viewentity, pSaveData->tableCount )].pent; if( COM_CheckStringEmpty( header.introTrack ) ) { // NOTE: music is automatically goes across transition, never restore it on changelevel MSG_BeginServerCmd( &sv.signon, svc_stufftext ); MSG_WriteString( &sv.signon, va( "music \"%s\" \"%s\" %i\n", header.introTrack, header.mainTrack, header.trackPosition )); } // don't go camera across the levels if( header.viewentity > svs.maxclients && !changelevel ) cl->pViewEntity = pent; // restore some client cvars Cvar_SetValue( "sv_wateralpha", header.wateralpha ); Cvar_SetValue( "sv_wateramp", header.wateramp ); } } /* ============= CreateEntitiesInRestoreList alloc private data for restored entities ============= */ static void CreateEntitiesInRestoreList( SAVERESTOREDATA *pSaveData, int levelMask, qboolean create_world ) { int i, active; ENTITYTABLE *pTable; edict_t *pent; // create entity list if( svgame.physFuncs.pfnCreateEntitiesInRestoreList != NULL ) { svgame.physFuncs.pfnCreateEntitiesInRestoreList( pSaveData, levelMask, create_world ); } else { for( i = 0; i < pSaveData->tableCount; i++ ) { pTable = &pSaveData->pTable[i]; pent = NULL; if( pTable->classname && pTable->size && ( !FBitSet( pTable->flags, FENTTABLE_REMOVED ) || !create_world )) { if( !create_world ) active = FBitSet( pTable->flags, levelMask ) ? 1 : 0; else active = 1; if( pTable->id == 0 && create_world ) // worldspawn { pent = EDICT_NUM( 0 ); SV_InitEdict( pent ); pent = SV_CreateNamedEntity( pent, pTable->classname ); } else if(( pTable->id > 0 ) && ( pTable->id < svs.maxclients + 1 )) { edict_t *ed = EDICT_NUM( pTable->id ); if( !FBitSet( pTable->flags, FENTTABLE_PLAYER )) Con_Printf( S_ERROR "ENTITY IS NOT A PLAYER: %d\n", i ); // create the player if( active && SV_IsValidEdict( ed )) pent = SV_CreateNamedEntity( ed, pTable->classname ); } else if( active ) { pent = SV_CreateNamedEntity( NULL, pTable->classname ); } } pTable->pent = pent; } } } /* ============= SaveGameState save current game state ============= */ static SAVERESTOREDATA *SaveGameState( int changelevel ) { char name[MAX_QPATH]; int i, id, version; char *pTableData; char *pTokenData; SAVERESTOREDATA *pSaveData; int tableSize; int dataSize; ENTITYTABLE *pTable; SAVE_HEADER header; SAVE_LIGHTSTYLE light; file_t *pFile; if( !svgame.dllFuncs.pfnParmsChangeLevel ) return NULL; pSaveData = SaveInit( SAVE_HEAPSIZE, SAVE_HASHSTRINGS ); Q_snprintf( name, sizeof( name ), DEFAULT_SAVE_DIRECTORY "%s.HL1", sv.name ); COM_FixSlashes( name ); // initialize entity table to count moved entities InitEntityTable( pSaveData, svgame.numEntities ); // Build the adjacent map list svgame.dllFuncs.pfnParmsChangeLevel(); // Write the global data header.skillLevel = (int)skill.value; // this is created from an int even though it's a float header.entityCount = pSaveData->tableCount; header.connectionCount = pSaveData->connectionCount; header.time = svgame.globals->time; // use DLL time Q_strncpy( header.mapName, sv.name, sizeof( header.mapName )); Q_strncpy( header.skyName, sv_skyname.string, sizeof( header.skyName )); header.skyColor_r = sv_skycolor_r.value; header.skyColor_g = sv_skycolor_g.value; header.skyColor_b = sv_skycolor_b.value; header.skyVec_x = sv_skyvec_x.value; header.skyVec_y = sv_skyvec_y.value; header.skyVec_z = sv_skyvec_z.value; header.lightStyleCount = 0; // counting the lightstyles for( i = 0; i < MAX_LIGHTSTYLES; i++ ) { if( sv.lightstyles[i].pattern[0] ) header.lightStyleCount++; } // Write the main header pSaveData->time = 0.0f; // prohibits rebase of header.time (keep compatibility with old saves) svgame.dllFuncs.pfnSaveWriteFields( pSaveData, "Save Header", &header, gSaveHeader, ARRAYSIZE( gSaveHeader )); pSaveData->time = header.time; // Write the adjacency list for( i = 0; i < pSaveData->connectionCount; i++ ) svgame.dllFuncs.pfnSaveWriteFields( pSaveData, "ADJACENCY", &pSaveData->levelList[i], gAdjacency, ARRAYSIZE( gAdjacency )); // Write the lightstyles for( i = 0; i < MAX_LIGHTSTYLES; i++ ) { if( !sv.lightstyles[i].pattern[0] ) continue; Q_strncpy( light.style, sv.lightstyles[i].pattern, sizeof( light.style )); light.time = sv.lightstyles[i].time; light.index = i; svgame.dllFuncs.pfnSaveWriteFields( pSaveData, "LIGHTSTYLE", &light, gLightStyle, ARRAYSIZE( gLightStyle )); } // build the table of entities // this is used to turn pointers into savable indices // build up ID numbers for each entity, for use in pointer conversions // if an entity requires a certain edict number upon restore, save that as well for( i = 0; i < svgame.numEntities; i++ ) { pTable = &pSaveData->pTable[i]; pTable->location = pSaveData->size; pSaveData->currentIndex = i; pTable->size = 0; if( !SV_IsValidEdict( pTable->pent )) continue; svgame.dllFuncs.pfnSave( pTable->pent, pSaveData ); if( FBitSet( pTable->pent->v.flags, FL_CLIENT )) SetBits( pTable->flags, FENTTABLE_PLAYER ); } // total data what includes: // 1. save header // 2. adjacency list // 3. lightstyles // 4. all the entity data dataSize = pSaveData->size; // Write entity table pTableData = pSaveData->pCurrentData; for( i = 0; i < pSaveData->tableCount; i++ ) svgame.dllFuncs.pfnSaveWriteFields( pSaveData, "ETABLE", &pSaveData->pTable[i], gEntityTable, ARRAYSIZE( gEntityTable )); tableSize = pSaveData->size - dataSize; // Write entity string token table pTokenData = StoreHashTable( pSaveData ); // output to disk if(( pFile = FS_Open( name, "wb", true )) == NULL ) { // something bad is happens SaveFinish( pSaveData ); return NULL; } // Write the header -- THIS SHOULD NEVER CHANGE STRUCTURE, USE SAVE_HEADER FOR NEW HEADER INFORMATION // THIS IS ONLY HERE TO IDENTIFY THE FILE AND GET IT'S SIZE. version = SAVEGAME_VERSION; id = SAVEFILE_HEADER; // write the header FS_Write( pFile, &id, sizeof( id )); FS_Write( pFile, &version, sizeof( version )); // Write out the tokens and table FIRST so they are loaded in the right order, then write out the rest of the data in the file. FS_Write( pFile, &pSaveData->size, sizeof( int )); // total size of all data to initialize read buffer FS_Write( pFile, &pSaveData->tableCount, sizeof( int )); // entities count to right initialize entity table FS_Write( pFile, &pSaveData->tokenCount, sizeof( int )); // num hash tokens to prepare token table FS_Write( pFile, &pSaveData->tokenSize, sizeof( int )); // total size of hash tokens FS_Write( pFile, pTokenData, pSaveData->tokenSize ); // write tokens into the file FS_Write( pFile, pTableData, tableSize ); // dump ETABLE structures FS_Write( pFile, pSaveData->pBaseData, dataSize ); // and finally store all the other data FS_Close( pFile ); EntityPatchWrite( pSaveData, sv.name ); SaveClientState( pSaveData, sv.name, changelevel ); return pSaveData; } /* ============= LoadGameState load current game state ============= */ static int LoadGameState( char const *level, qboolean changelevel ) { SAVERESTOREDATA *pSaveData; ENTITYTABLE *pTable; SAVE_HEADER header; edict_t *pent; int i; pSaveData = LoadSaveData( level ); if( !pSaveData ) return 0; // couldn't load the file ParseSaveTables( pSaveData, &header, true ); EntityPatchRead( pSaveData, level ); // pause until all clients connect sv.loadgame = sv.paused = true; Cvar_SetValue( "skill", header.skillLevel ); Q_strncpy( sv.name, header.mapName, sizeof( sv.name )); svgame.globals->mapname = MAKE_STRING( sv.name ); Cvar_Set( "sv_skyname", header.skyName ); // restore sky parms Cvar_SetValue( "sv_skycolor_r", header.skyColor_r ); Cvar_SetValue( "sv_skycolor_g", header.skyColor_g ); Cvar_SetValue( "sv_skycolor_b", header.skyColor_b ); Cvar_SetValue( "sv_skyvec_x", header.skyVec_x ); Cvar_SetValue( "sv_skyvec_y", header.skyVec_y ); Cvar_SetValue( "sv_skyvec_z", header.skyVec_z ); // create entity list CreateEntitiesInRestoreList( pSaveData, 0, true ); // now spawn entities for( i = 0; i < pSaveData->tableCount; i++ ) { pTable = &pSaveData->pTable[i]; pSaveData->pCurrentData = pSaveData->pBaseData + pTable->location; pSaveData->size = pTable->location; pSaveData->currentIndex = i; pent = pTable->pent; if( pent != NULL ) { if( svgame.dllFuncs.pfnRestore( pent, pSaveData, 0 ) < 0 ) { SetBits( pent->v.flags, FL_KILLME ); pTable->pent = NULL; } else { // force the entity to be relinked // SV_LinkEdict( pent, false ); } } } LoadClientState( pSaveData, level, changelevel, false ); SaveFinish( pSaveData ); // restore server time sv.time = header.time; return 1; } /* ============= SaveGameSlot do a save game ============= */ static int SaveGameSlot( const char *pSaveName, const char *pSaveComment ) { char hlPath[MAX_QPATH]; char name[MAX_QPATH]; int id, version; char *pTokenData; SAVERESTOREDATA *pSaveData; GAME_HEADER gameHeader; file_t *pFile; pSaveData = SaveGameState( false ); if( !pSaveData ) return 0; SaveFinish( pSaveData ); pSaveData = SaveInit( SAVE_HEAPSIZE, SAVE_HASHSTRINGS ); // re-init the buffer Q_strncpy( hlPath, DEFAULT_SAVE_DIRECTORY "*.HL?", sizeof( hlPath ) ); Q_strncpy( gameHeader.mapName, sv.name, sizeof( gameHeader.mapName )); // get the name of level where a player Q_strncpy( gameHeader.comment, pSaveComment, sizeof( gameHeader.comment )); gameHeader.mapCount = DirectoryCount( hlPath ); // counting all the adjacency maps // Store the game header svgame.dllFuncs.pfnSaveWriteFields( pSaveData, "GameHeader", &gameHeader, gGameHeader, ARRAYSIZE( gGameHeader )); // Write the game globals svgame.dllFuncs.pfnSaveGlobalState( pSaveData ); // Write entity string token table pTokenData = StoreHashTable( pSaveData ); Q_snprintf( name, sizeof( name ), DEFAULT_SAVE_DIRECTORY "%s.sav", pSaveName ); COM_FixSlashes( name ); // output to disk if( !Q_stricmp( pSaveName, "quick" ) || !Q_stricmp( pSaveName, "autosave" )) AgeSaveList( pSaveName, SAVE_AGED_COUNT ); // output to disk if(( pFile = FS_Open( name, "wb", true )) == NULL ) { // something bad is happens SaveFinish( pSaveData ); return 0; } // pending the preview image for savegame Cbuf_AddText( va( "saveshot \"%s\"\n", pSaveName )); Con_Printf( "Saving game to %s...\n", name ); version = SAVEGAME_VERSION; id = SAVEGAME_HEADER; FS_Write( pFile, &id, sizeof( id )); FS_Write( pFile, &version, sizeof( version )); FS_Write( pFile, &pSaveData->size, sizeof( int )); // does not include token table // write out the tokens first so we can load them before we load the entities FS_Write( pFile, &pSaveData->tokenCount, sizeof( int )); FS_Write( pFile, &pSaveData->tokenSize, sizeof( int )); FS_Write( pFile, pTokenData, pSaveData->tokenSize ); FS_Write( pFile, pSaveData->pBaseData, pSaveData->size ); // header and globals DirectoryCopy( hlPath, pFile ); SaveFinish( pSaveData ); FS_Close( pFile ); return 1; } /* ============= SaveReadHeader read header of .sav file ============= */ static int SaveReadHeader( file_t *pFile, GAME_HEADER *pHeader ) { int tokenCount, tokenSize; int size, id, version; SAVERESTOREDATA *pSaveData; FS_Read( pFile, &id, sizeof( id )); if( id != SAVEGAME_HEADER ) { FS_Close( pFile ); return 0; } FS_Read( pFile, &version, sizeof( version )); if( version != SAVEGAME_VERSION ) { FS_Close( pFile ); return 0; } FS_Read( pFile, &size, sizeof( int )); FS_Read( pFile, &tokenCount, sizeof( int )); FS_Read( pFile, &tokenSize, sizeof( int )); pSaveData = SaveInit( size + tokenSize, tokenCount ); pSaveData->tokenCount = tokenCount; pSaveData->tokenSize = tokenSize; // Parse the symbol table BuildHashTable( pSaveData, pFile ); // Set up the restore basis pSaveData->fUseLandmark = false; pSaveData->time = 0.0f; FS_Read( pFile, pSaveData->pBaseData, size ); svgame.dllFuncs.pfnSaveReadFields( pSaveData, "GameHeader", pHeader, gGameHeader, ARRAYSIZE( gGameHeader )); svgame.dllFuncs.pfnRestoreGlobalState( pSaveData ); SaveFinish( pSaveData ); return 1; } /* ============= CreateEntityTransitionList moving edicts to another level ============= */ static int CreateEntityTransitionList( SAVERESTOREDATA *pSaveData, int levelMask ) { int i, movedCount; ENTITYTABLE *pTable; edict_t *pent; movedCount = 0; // create entity list CreateEntitiesInRestoreList( pSaveData, levelMask, false ); // now spawn entities for( i = 0; i < pSaveData->tableCount; i++ ) { pTable = &pSaveData->pTable[i]; pSaveData->pCurrentData = pSaveData->pBaseData + pTable->location; pSaveData->size = pTable->location; pSaveData->currentIndex = i; pent = pTable->pent; if( SV_IsValidEdict( pent ) && FBitSet( pTable->flags, levelMask )) // screen out the player if he's not to be spawned { if( FBitSet( pTable->flags, FENTTABLE_GLOBAL )) { entvars_t tmpVars; edict_t *pNewEnt; // NOTE: we need to update table pointer so decals on the global entities with brush models can be // correctly moved. found the classname and the globalname for our globalentity svgame.dllFuncs.pfnSaveReadFields( pSaveData, "ENTVARS", &tmpVars, gTempEntvars, ARRAYSIZE( gTempEntvars )); // reset the save pointers, so dll can read this too pSaveData->pCurrentData = pSaveData->pBaseData + pTable->location; pSaveData->size = pTable->location; // IMPORTANT: we should find the already spawned or local restored global entity pNewEnt = SV_FindGlobalEntity( tmpVars.classname, tmpVars.globalname ); Con_DPrintf( "Merging changes for global: %s\n", STRING( pTable->classname )); // ------------------------------------------------------------------------- // Pass the "global" flag to the DLL to indicate this entity should only override // a matching entity, not be spawned if( svgame.dllFuncs.pfnRestore( pent, pSaveData, 1 ) > 0 ) { movedCount++; } else { if( SV_IsValidEdict( pNewEnt )) // update the table so decals can find parent entity pTable->pent = pNewEnt; SetBits( pent->v.flags, FL_KILLME ); } } else { Con_Reportf( "Transferring %s (%d)\n", STRING( pTable->classname ), NUM_FOR_EDICT( pent )); if( svgame.dllFuncs.pfnRestore( pent, pSaveData, 0 ) < 0 ) { SetBits( pent->v.flags, FL_KILLME ); } else { if( !FBitSet( pTable->flags, FENTTABLE_PLAYER ) && EntityInSolid( pent )) { // this can happen during normal processing - PVS is just a guess, // some map areas won't exist in the new map Con_Reportf( "Suppressing %s\n", STRING( pTable->classname )); SetBits( pent->v.flags, FL_KILLME ); } else { pTable->flags = FENTTABLE_REMOVED; movedCount++; } } } // remove any entities that were removed using UTIL_Remove() // as a result of the above calls to UTIL_RemoveImmediate() SV_FreeOldEntities (); } } return movedCount; } /* ============= LoadAdjacentEnts loading edicts from adjacency levels ============= */ static void LoadAdjacentEnts( const char *pOldLevel, const char *pLandmarkName ) { SAVE_HEADER header; SAVERESTOREDATA currentLevelData, *pSaveData; int i, test, flags, index, movedCount = 0; qboolean foundprevious = false; vec3_t landmarkOrigin; memset( ¤tLevelData, 0, sizeof( SAVERESTOREDATA )); svgame.globals->pSaveData = ¤tLevelData; sv.loadgame = sv.paused = true; // build the adjacent map list svgame.dllFuncs.pfnParmsChangeLevel(); for( i = 0; i < currentLevelData.connectionCount; i++ ) { // make sure the previous level is in the connection list so we can // bring over the player. if( !Q_stricmp( currentLevelData.levelList[i].mapName, pOldLevel )) foundprevious = true; for( test = 0; test < i; test++ ) { // only do maps once if( !Q_stricmp( currentLevelData.levelList[i].mapName, currentLevelData.levelList[test].mapName )) break; } // map was already in the list if( test < i ) continue; pSaveData = LoadSaveData( currentLevelData.levelList[i].mapName ); if( pSaveData ) { ParseSaveTables( pSaveData, &header, false ); EntityPatchRead( pSaveData, currentLevelData.levelList[i].mapName ); pSaveData->time = sv.time; // - header.time; pSaveData->fUseLandmark = true; flags = movedCount = 0; index = -1; // calculate landmark offset LandmarkOrigin( ¤tLevelData, landmarkOrigin, pLandmarkName ); LandmarkOrigin( pSaveData, pSaveData->vecLandmarkOffset, pLandmarkName ); VectorSubtract( landmarkOrigin, pSaveData->vecLandmarkOffset, pSaveData->vecLandmarkOffset ); if( !Q_stricmp( currentLevelData.levelList[i].mapName, pOldLevel )) SetBits( flags, FENTTABLE_PLAYER ); while( 1 ) { index = EntryInTable( pSaveData, sv.name, index ); if( index < 0 ) break; SetBits( flags, BIT( index )); } if( flags ) movedCount = CreateEntityTransitionList( pSaveData, flags ); // if ents were moved, rewrite entity table to save file if( movedCount ) EntityPatchWrite( pSaveData, currentLevelData.levelList[i].mapName ); // move the decals from another level LoadClientState( pSaveData, currentLevelData.levelList[i].mapName, true, true ); SaveFinish( pSaveData ); } } svgame.globals->pSaveData = NULL; if( !foundprevious ) Host_Error( "Level transition ERROR\nCan't find connection to %s from %s\n", pOldLevel, sv.name ); } /* ============= SV_LoadGameState loading entities from the savegame ============= */ int SV_LoadGameState( char const *level ) { return LoadGameState( level, false ); } /* ============= SV_ClearGameState clear current game state ============= */ void SV_ClearGameState( void ) { ClearSaveDir(); if( svgame.dllFuncs.pfnResetGlobalState != NULL ) svgame.dllFuncs.pfnResetGlobalState(); } /* ============= SV_ChangeLevel ============= */ void SV_ChangeLevel( qboolean loadfromsavedgame, const char *mapname, const char *start, qboolean background ) { char level[MAX_QPATH]; char oldlevel[MAX_QPATH]; char _startspot[MAX_QPATH]; char *startspot = NULL; SAVERESTOREDATA *pSaveData = NULL; if( sv.state != ss_active ) { Con_Printf( S_ERROR "server not running\n"); return; } if( start ) { Q_strncpy( _startspot, start, MAX_STRING ); startspot = _startspot; } Q_strncpy( level, mapname, MAX_STRING ); Q_strncpy( oldlevel, sv.name, MAX_STRING ); if( loadfromsavedgame ) { // smooth transition in-progress svgame.globals->changelevel = true; // save the current level's state pSaveData = SaveGameState( true ); } SV_InactivateClients (); SV_FinalMessage( "", true ); SV_DeactivateServer (); if( !SV_SpawnServer( level, startspot, background )) return; // ??? if( loadfromsavedgame ) { // finish saving gamestate SaveFinish( pSaveData ); if( !LoadGameState( level, true )) SV_SpawnEntities( level ); LoadAdjacentEnts( oldlevel, startspot ); if( sv_newunit.value ) ClearSaveDir(); SV_ActivateServer( false ); } else { // classic quake changelevel svgame.dllFuncs.pfnResetGlobalState(); SV_SpawnEntities( level ); SV_ActivateServer( true ); } } /* ============= SV_LoadGame ============= */ qboolean SV_LoadGame( const char *pPath ) { qboolean validload = false; GAME_HEADER gameHeader; file_t *pFile; uint flags; if( Host_IsDedicated() ) return false; if( UI_CreditsActive( )) return false; if( !COM_CheckString( pPath )) return false; // silently ignore if missed if( !FS_FileExists( pPath, true )) return false; // initialize game if needs if( !SV_InitGame( )) return false; pFile = FS_Open( pPath, "rb", true ); if( pFile ) { SV_ClearGameState(); if( SaveReadHeader( pFile, &gameHeader )) { DirectoryExtract( pFile, gameHeader.mapCount ); validload = true; } FS_Close( pFile ); if( validload ) { // now check for map problems flags = SV_MapIsValid( gameHeader.mapName, GI->sp_entity, NULL ); if( FBitSet( flags, MAP_INVALID_VERSION )) { Con_Printf( S_ERROR "map %s is invalid or not supported\n", gameHeader.mapName ); validload = false; } if( !FBitSet( flags, MAP_IS_EXIST )) { Con_Printf( S_ERROR "map %s doesn't exist\n", gameHeader.mapName ); validload = false; } } } if( !validload ) { Con_Printf( S_ERROR "Couldn't load %s\n", pPath ); return false; } Con_Printf( "Loading game from %s...\n", pPath ); Cvar_FullSet( "maxplayers", "1", FCVAR_LATCH ); Cvar_SetValue( "deathmatch", 0 ); Cvar_SetValue( "coop", 0 ); COM_LoadGame( gameHeader.mapName ); return true; } /* ================== SV_SaveGame ================== */ void SV_SaveGame( const char *pName ) { char comment[80]; int result; string savename; if( !COM_CheckString( pName )) return; // can we save at this point? if( !IsValidSave( )) return; if( !Q_stricmp( pName, "new" )) { int n; // scan for a free filename for( n = 0; n < 1000; n++ ) { Q_snprintf( savename, sizeof( savename ), "save%03d", n ); if( !FS_FileExists( va( DEFAULT_SAVE_DIRECTORY "%s.sav", savename ), true )) break; } if( n == 1000 ) { Con_Printf( S_ERROR "no free slots for savegame\n" ); return; } } else Q_strncpy( savename, pName, sizeof( savename )); #if !XASH_DEDICATED // unload previous image from memory (it's will be overwritten) GL_FreeImage( va( DEFAULT_SAVE_DIRECTORY "%s.bmp", savename ) ); #endif // XASH_DEDICATED SaveBuildComment( comment, sizeof( comment )); result = SaveGameSlot( savename, comment ); #if !XASH_DEDICATED if( result && !FBitSet( host.features, ENGINE_QUAKE_COMPATIBLE )) CL_HudMessage( "GAMESAVED" ); // defined in titles.txt #endif // XASH_DEDICATED } /* ================== SV_GetLatestSave used for reload game after player death ================== */ const char *SV_GetLatestSave( void ) { static char savename[MAX_QPATH]; int newest = 0, ft; int i, found = 0; search_t *t; if(( t = FS_Search( DEFAULT_SAVE_DIRECTORY "*.sav" , true, true )) == NULL ) return NULL; for( i = 0; i < t->numfilenames; i++ ) { ft = FS_FileTime( t->filenames[i], true ); // found a match? if( ft > 0 ) { // should we use the matched? if( !found || Host_CompareFileTime( newest, ft ) < 0 ) { Q_strncpy( savename, t->filenames[i], sizeof( savename )); newest = ft; found = 1; } } } Mem_Free( t ); // release search if( found ) return savename; return NULL; } /* ================== SV_GetSaveComment check savegame for valid ================== */ int GAME_EXPORT SV_GetSaveComment( const char *savename, char *comment ) { int i, tag, size, nNumberOfFields, nFieldSize, tokenSize, tokenCount; char *pData, *pSaveData, *pFieldName, **pTokenList; string mapName, description; file_t *f; if(( f = FS_Open( savename, "rb", true )) == NULL ) { // just not exist - clear comment comment[0] = '\0'; return 0; } FS_Read( f, &tag, sizeof( int )); if( tag != SAVEGAME_HEADER ) { // invalid header Q_strncpy( comment, "", MAX_STRING ); FS_Close( f ); return 0; } FS_Read( f, &tag, sizeof( int )); if( tag == 0x0065 ) { Q_strncpy( comment, "", MAX_STRING ); FS_Close( f ); return 0; } if( tag < SAVEGAME_VERSION ) { Q_strncpy( comment, "", MAX_STRING ); FS_Close( f ); return 0; } if( tag > SAVEGAME_VERSION ) { // old xash version ? Q_strncpy( comment, "", MAX_STRING ); FS_Close( f ); return 0; } mapName[0] = '\0'; comment[0] = '\0'; FS_Read( f, &size, sizeof( int )); FS_Read( f, &tokenCount, sizeof( int )); // These two ints are the token list FS_Read( f, &tokenSize, sizeof( int )); size += tokenSize; // sanity check. if( tokenCount < 0 || tokenCount > SAVE_HASHSTRINGS ) { Q_strncpy( comment, "", MAX_STRING ); FS_Close( f ); return 0; } if( tokenSize < 0 || tokenSize > SAVE_HEAPSIZE ) { Q_strncpy( comment, "", MAX_STRING ); FS_Close( f ); return 0; } pSaveData = (char *)Mem_Malloc( host.mempool, size ); FS_Read( f, pSaveData, size ); pData = pSaveData; // allocate a table for the strings, and parse the table if( tokenSize > 0 ) { pTokenList = Mem_Calloc( host.mempool, tokenCount * sizeof( char* )); // make sure the token strings pointed to by the pToken hashtable. for( i = 0; i < tokenCount; i++ ) { pTokenList[i] = *pData ? pData : NULL; // point to each string in the pToken table while( *pData++ ); // find next token (after next null) } } else pTokenList = NULL; // short, short (size, index of field name) nFieldSize = *(short *)pData; pData += sizeof( short ); pFieldName = pTokenList[*(short *)pData]; if( Q_stricmp( pFieldName, "GameHeader" )) { Q_strncpy( comment, "", MAX_STRING ); if( pTokenList ) Mem_Free( pTokenList ); if( pSaveData ) Mem_Free( pSaveData ); FS_Close( f ); return 0; } // int (fieldcount) pData += sizeof( short ); nNumberOfFields = (int)*pData; pData += nFieldSize; // each field is a short (size), short (index of name), binary string of "size" bytes (data) for( i = 0; i < nNumberOfFields; i++ ) { size_t size; // Data order is: // Size // szName // Actual Data nFieldSize = *(short *)pData; pData += sizeof( short ); pFieldName = pTokenList[*(short *)pData]; pData += sizeof( short ); size = Q_min( nFieldSize, MAX_STRING ); if( !Q_stricmp( pFieldName, "comment" )) { Q_strncpy( description, pData, size ); } else if( !Q_stricmp( pFieldName, "mapName" )) { Q_strncpy( mapName, pData, size ); } // move to start of next field. pData += nFieldSize; } // delete the string table we allocated if( pTokenList ) Mem_Free( pTokenList ); if( pSaveData ) Mem_Free( pSaveData ); FS_Close( f ); // at least mapname should be filled if( COM_CheckStringEmpty( mapName ) ) { time_t fileTime; const struct tm *file_tm; string timestring; uint flags; // now check for map problems flags = SV_MapIsValid( mapName, GI->sp_entity, NULL ); if( FBitSet( flags, MAP_INVALID_VERSION )) { Q_strncpy( comment, va( "", mapName ), MAX_STRING ); return 0; } if( !FBitSet( flags, MAP_IS_EXIST )) { Q_strncpy( comment, va( "", mapName ), MAX_STRING ); return 0; } fileTime = FS_FileTime( savename, true ); file_tm = localtime( &fileTime ); // split comment to sections if( Q_strstr( savename, "quick" )) Q_strncat( comment, "[quick]", CS_SIZE ); else if( Q_strstr( savename, "autosave" )) Q_strncat( comment, "[autosave]", CS_SIZE ); Q_strncat( comment, description, CS_SIZE ); strftime( timestring, sizeof ( timestring ), "%b%d %Y", file_tm ); Q_strncpy( comment + CS_SIZE, timestring, CS_TIME ); strftime( timestring, sizeof( timestring ), "%H:%M", file_tm ); Q_strncpy( comment + CS_SIZE + CS_TIME, timestring, CS_TIME ); Q_strncpy( comment + CS_SIZE + (CS_TIME * 2), description + CS_SIZE, CS_SIZE ); return 1; } Q_strncpy( comment, "", MAX_STRING ); return 0; } void SV_InitSaveRestore( void ) { pfnSaveGameComment = COM_GetProcAddress( svgame.hInstance, "SV_SaveGameComment" ); }