xash3d-fwgs/engine/server/sv_save.c

2357 lines
61 KiB
C
Raw Normal View History

/*
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
2018-06-19 15:22:30 +02:00
#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[] =
{
2018-06-19 15:22:30 +02:00
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;
2018-06-09 00:28:35 +02:00
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( va( "%s*.HL?", DEFAULT_SAVE_DIRECTORY ), 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
2018-10-04 08:08:48 +02:00
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 ), "%s%s%02d.sav", DEFAULT_SAVE_DIRECTORY, pName, count );
Q_snprintf( newShot, sizeof( newShot ), "%s%s%02d.bmp", DEFAULT_SAVE_DIRECTORY, pName, count );
// only delete from game directory, basedir is read-only
FS_Delete( newName );
FS_Delete( newShot );
#ifndef 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 ), "%s%s.sav", DEFAULT_SAVE_DIRECTORY, pName );
Q_snprintf( oldShot, sizeof( oldShot ), "%s%s.bmp", DEFAULT_SAVE_DIRECTORY, pName );
}
else
{
// quick04.sav, etc.
Q_snprintf( oldName, sizeof( oldName ), "%s%s%02d.sav", DEFAULT_SAVE_DIRECTORY, pName, count - 1 );
Q_snprintf( oldShot, sizeof( oldShot ), "%s%s%02d.bmp", DEFAULT_SAVE_DIRECTORY, pName, count - 1 );
}
Q_snprintf( newName, sizeof( newName ), "%s%s%02d.sav", DEFAULT_SAVE_DIRECTORY, pName, count );
Q_snprintf( newShot, sizeof( newShot ), "%s%s%02d.bmp", DEFAULT_SAVE_DIRECTORY, pName, count );
#ifndef 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 ), "%s%s", DEFAULT_SAVE_DIRECTORY, 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;
2018-06-09 00:28:35 +02:00
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;
}
/*
=============
2018-10-04 08:08:48 +02:00
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 ), "%s%s.HL2", DEFAULT_SAVE_DIRECTORY, 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 ), "%s%s.HL1", DEFAULT_SAVE_DIRECTORY, 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 ), "%s%s.HL3", DEFAULT_SAVE_DIRECTORY, 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 ), "%s%s.HL3", DEFAULT_SAVE_DIRECTORY, 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
2018-06-09 00:28:35 +02:00
decalList = (decallist_t *)Z_Calloc( sizeof( decallist_t ) * MAX_RENDER_DECALS * 2 );
// initialize client header
2019-03-22 14:47:48 +01:00
#ifndef XASH_DEDICATED
if( !Host_IsDedicated() )
{
header.decalCount = ref.dllFuncs.R_CreateDecalList( decalList );
}
else
2019-03-22 14:47:48 +01:00
#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 );
2018-04-18 17:32:30 +02:00
#ifndef 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 );
2018-04-18 17:32:30 +02:00
#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++ )
2018-06-19 15:22:30 +02:00
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 ), "%s%s.HL2", DEFAULT_SAVE_DIRECTORY, 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 ), "%s%s.HL2", DEFAULT_SAVE_DIRECTORY, 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 )
{
2018-06-19 15:22:30 +02:00
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++ )
{
2018-06-19 15:22:30 +02:00
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
2018-06-19 15:22:30 +02:00
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( Q_strlen( 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 ), "%s%s.HL1", DEFAULT_SAVE_DIRECTORY, 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 )
{
2018-06-19 15:22:30 +02:00
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_snprintf( hlPath, sizeof( hlPath ), "%s*.HL?", DEFAULT_SAVE_DIRECTORY );
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 ), "%s%s.sav", DEFAULT_SAVE_DIRECTORY, 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
{
2018-10-04 08:08:48 +02:00
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
2018-10-04 08:08:48 +02:00
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;
2018-10-04 08:08:48 +02:00
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%003d", n );
if( !FS_FileExists( va( "%s%s.sav", DEFAULT_SAVE_DIRECTORY, savename ), true ))
break;
}
if( n == 1000 )
{
Con_Printf( S_ERROR "no free slots for savegame\n" );
return;
}
}
else Q_strncpy( savename, pName, sizeof( savename ));
#ifndef XASH_DEDICATED
// unload previous image from memory (it's will be overwritten)
GL_FreeImage( va( "%s%s.bmp", DEFAULT_SAVE_DIRECTORY, savename ));
#endif // XASH_DEDICATED
SaveBuildComment( comment, sizeof( comment ));
result = SaveGameSlot( savename, comment );
2018-04-18 17:32:30 +02:00
#ifndef 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];
2018-12-05 17:57:05 +01:00
int newest = 0, ft;
int i, found = 0;
search_t *t;
if(( t = FS_Search( va( "%s*.sav", DEFAULT_SAVE_DIRECTORY ), 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 SV_GetSaveComment( const char *savename, char *comment )
{
int i, tag, size, nNumberOfFields, nFieldSize, tokenSize, tokenCount;
char *pData, *pSaveData, *pFieldName, **pTokenList;
2018-04-19 22:11:24 +02:00
string mapName, description;
file_t *f;
if(( f = FS_Open( savename, "rb", true )) == NULL )
{
// just not exist - clear comment
Q_strncpy( comment, "", MAX_STRING );
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;
}
2018-04-19 22:11:24 +02:00
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;
}
2018-06-09 00:28:35 +02:00
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 )
{
2018-06-09 00:28:35 +02:00
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" ))
{
2018-04-19 22:11:24 +02:00
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
2018-04-19 22:11:24 +02:00
if( Q_strlen( mapName ) > 0 )
{
time_t fileTime;
const struct tm *file_tm;
string timestring;
2018-04-19 22:11:24 +02:00
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" );
}