xash3d-fwgs/engine/server/sv_save.c
Gleb Mazovetskiy 5e0a0765ce Trim all trailing whitespace
The `.editorconfig` file in this repo is configured to trim all trailing
whitespace regardless of whether the line is modified.

Trims all trailing whitespace in the repository to make the codebase easier
to work with in editors that respect `.editorconfig`.

`git blame` becomes less useful on these lines but it already isn't very useful.

Commands:

```
find . -type f -name '*.h' -exec sed --in-place 's/[[:space:]]\+$//' {} \+
find . -type f -name '*.c' -exec sed --in-place 's/[[:space:]]\+$//' {} \+
```
2021-01-04 20:55:10 +03:00

2357 lines
61 KiB
C

/*
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 ),
};
/*
=============
SaveBuildComment
build commentary for each savegame
typically it writes world message and level time
=============
*/
static void SaveBuildComment( char *text, int maxlength )
{
const char *pName;
text[0] = '\0'; // clear
if( pfnSaveGameComment != NULL )
{
// get save comment from gamedll
pfnSaveGameComment( text, maxlength );
}
else
{
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++ )
{
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( &currentLevelData, 0, sizeof( SAVERESTOREDATA ));
svgame.globals->pSaveData = &currentLevelData;
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( &currentLevelData, 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;
int 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, "<corrupted>", MAX_STRING );
FS_Close( f );
return 0;
}
FS_Read( f, &tag, sizeof( int ));
if( tag == 0x0065 )
{
Q_strncpy( comment, "old version Xash3D <unsupported>", MAX_STRING );
FS_Close( f );
return 0;
}
if( tag < SAVEGAME_VERSION )
{
Q_strncpy( comment, "<old version>", MAX_STRING );
FS_Close( f );
return 0;
}
if( tag > SAVEGAME_VERSION )
{
// old xash version ?
Q_strncpy( comment, "<invalid version>", 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, "<corrupted hashtable>", MAX_STRING );
FS_Close( f );
return 0;
}
if( tokenSize < 0 || tokenSize > SAVE_HEAPSIZE )
{
Q_strncpy( comment, "<corrupted hashtable>", 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, "<missing GameHeader>", 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++ )
{
// Data order is:
// Size
// szName
// Actual Data
nFieldSize = *(short *)pData;
pData += sizeof( short );
pFieldName = pTokenList[*(short *)pData];
pData += sizeof( short );
if( !Q_stricmp( pFieldName, "comment" ))
{
Q_strncpy( description, pData, nFieldSize );
}
else if( !Q_stricmp( pFieldName, "mapName" ))
{
Q_strncpy( mapName, pData, nFieldSize );
}
// 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;
int flags;
// now check for map problems
flags = SV_MapIsValid( mapName, GI->sp_entity, NULL );
if( FBitSet( flags, MAP_INVALID_VERSION ))
{
Q_strncpy( comment, va( "<map %s has invalid format>", mapName ), MAX_STRING );
return 0;
}
if( !FBitSet( flags, MAP_IS_EXIST ))
{
Q_strncpy( comment, va( "<map %s is missed>", 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, "<unknown version>", MAX_STRING );
return 0;
}
void SV_InitSaveRestore( void )
{
pfnSaveGameComment = COM_GetProcAddress( svgame.hInstance, "SV_SaveGameComment" );
}