This repository has been archived on 2022-06-27. You can view files and clone it, but cannot push or open issues or pull requests.
Xash3DArchive/engine/server/sv_save.c

1970 lines
51 KiB
C

//=======================================================================
// Copyright XashXT Group 2008 ©
// sv_save.c - save\restore implementation
//=======================================================================
#include "common.h"
#include "server.h"
#include "const.h"
/*
==============================================================================
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 0x0065 // Version 0.65
#define SAVE_AGED_COUNT 1
#define SAVENAME_LENGTH 128 // matches with MAX_OSPATH
typedef struct
{
int nBytesSymbols;
int nSymbols;
int nBytesDataHeaders;
int nBytesData;
} SaveFileSectionsInfo_t;
typedef struct
{
char *pSymbols;
char *pDataHeaders;
char *pData;
} SaveFileSections_t;
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;
int viewentity; // Xash3D added
} SAVE_HEADER;
typedef struct
{
int index;
char style[64];
} SAVE_LIGHTSTYLE;
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 ),
DEFINE_FIELD( SAVE_HEADER, viewentity, FIELD_SHORT ),
};
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, 64 ),
};
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 gDecalList[] =
{
DEFINE_FIELD( decallist_t, position, FIELD_POSITION_VECTOR ),
DEFINE_ARRAY( decallist_t, name, FIELD_CHARACTER, 64 ),
DEFINE_FIELD( decallist_t, entityIndex, FIELD_SHORT ),
DEFINE_FIELD( decallist_t, flags, FIELD_CHARACTER ),
DEFINE_FIELD( decallist_t, impactPlaneNormal, FIELD_VECTOR ),
};
int SumBytes( SaveFileSectionsInfo_t *section )
{
return ( section->nBytesSymbols + section->nBytesDataHeaders + section->nBytesData );
}
/*
----------------------------------------------------------
SaveRestore helpers
assume pSaveData is valid
----------------------------------------------------------
*/
void SaveRestore_Init( SAVERESTOREDATA *pSaveData, void *pNewBase, int nBytes )
{
pSaveData->pCurrentData = pSaveData->pBaseData = (char *)pNewBase;
pSaveData->size = 0;
pSaveData->bufferSize = nBytes;
}
void SaveRestore_MoveCurPos( SAVERESTOREDATA *pSaveData, int nBytes )
{
pSaveData->pCurrentData += nBytes;
pSaveData->size += nBytes;
}
void SaveRestore_Rebase( SAVERESTOREDATA *pSaveData )
{
pSaveData->pBaseData = pSaveData->pCurrentData;
pSaveData->bufferSize -= pSaveData->size;
pSaveData->size = 0;
}
void SaveRestore_Rewind( SAVERESTOREDATA *pSaveData, int nBytes )
{
if( pSaveData->size < nBytes )
nBytes = pSaveData->size;
SaveRestore_MoveCurPos( pSaveData, -nBytes );
}
char *SaveRestore_GetBuffer( SAVERESTOREDATA *pSaveData )
{
return pSaveData->pBaseData;
}
int SaveRestore_BytesAvailable( SAVERESTOREDATA *pSaveData )
{
return (pSaveData->bufferSize - pSaveData->size);
}
int SaveRestore_SizeBuffer( SAVERESTOREDATA *pSaveData )
{
return pSaveData->bufferSize;
}
qboolean SaveRestore_Write( SAVERESTOREDATA *pSaveData, const void *pData, int nBytes )
{
if( nBytes > SaveRestore_BytesAvailable( pSaveData ))
{
pSaveData->size = pSaveData->bufferSize;
return false;
}
Mem_Copy( pSaveData->pCurrentData, pData, nBytes );
SaveRestore_MoveCurPos( pSaveData, nBytes );
return true;
}
qboolean SaveRestore_Read( SAVERESTOREDATA *pSaveData, void *pOutput, int nBytes )
{
if( !SaveRestore_BytesAvailable( pSaveData ))
return false;
if( nBytes > SaveRestore_BytesAvailable( pSaveData ))
{
pSaveData->size = pSaveData->bufferSize;
return false;
}
if( pOutput ) Mem_Copy( pOutput, pSaveData->pCurrentData, nBytes );
SaveRestore_MoveCurPos( pSaveData, nBytes );
return true;
}
int SaveRestore_GetCurPos( SAVERESTOREDATA *pSaveData )
{
return pSaveData->size;
}
char *SaveRestore_AccessCurPos( SAVERESTOREDATA *pSaveData )
{
return pSaveData->pCurrentData;
}
qboolean SaveRestore_Seek( SAVERESTOREDATA *pSaveData, int absPosition )
{
if( absPosition < 0 || absPosition >= pSaveData->bufferSize )
return false;
pSaveData->size = absPosition;
pSaveData->pCurrentData = pSaveData->pBaseData + pSaveData->size;
return true;
}
void SaveRestore_InitEntityTable( SAVERESTOREDATA *pSaveData, ENTITYTABLE *pNewTable, int entityCount )
{
ENTITYTABLE *pTable;
int i;
ASSERT( pSaveData->pTable == NULL );
pSaveData->tableCount = entityCount;
pSaveData->pTable = pNewTable;
// setup entitytable
for( i = 0; i < entityCount; i++ )
{
pTable = &pSaveData->pTable[i];
pTable->pent = EDICT_NUM( i );
}
}
ENTITYTABLE *SaveRestore_DetachEntityTable( SAVERESTOREDATA *pSaveData )
{
ENTITYTABLE *pReturn = pSaveData->pTable;
pSaveData->pTable = NULL;
pSaveData->tableCount = 0;
return pReturn;
}
void SaveRestore_InitSymbolTable( SAVERESTOREDATA *pSaveData, char **pNewTokens, int sizeTable )
{
ASSERT( pSaveData->pTokens == NULL );
pSaveData->tokenCount = sizeTable;
pSaveData->pTokens = pNewTokens;
}
char **SaveRestore_DetachSymbolTable( SAVERESTOREDATA *pSaveData )
{
char **pResult = pSaveData->pTokens;
pSaveData->tokenCount = 0;
pSaveData->pTokens = NULL;
return pResult;
}
qboolean SaveRestore_DefineSymbol( SAVERESTOREDATA *pSaveData, const char *pszToken, int token )
{
if( pSaveData->pTokens[token] == NULL )
{
pSaveData->pTokens[token] = (char *)pszToken;
return true;
}
ASSERT( 0 );
return false;
}
const char *SaveRestore_StringFromSymbol( SAVERESTOREDATA *pSaveData, int token )
{
if( token >= 0 && token < pSaveData->tokenCount )
return pSaveData->pTokens[token];
return "<<illegal>>";
}
void SV_BuildSaveComment( char *text, int maxlength )
{
const char *pName;
edict_t *pWorld = EDICT_NUM( 0 );
float time = sv_time();
if( pWorld && pWorld->v.message )
{
// trying to extract message from world
pName = STRING( pWorld->v.message );
}
else
{
// or use mapname
pName = STRING( svgame.globals->mapname );
}
com.snprintf( text, maxlength, "%-64.64s %02d:%02d", pName, (int)(time / 60.0f ), (int)fmod( time, 60.0f ));
}
int SV_MapCount( const char *pPath )
{
search_t *t;
int count = 0;
t = FS_Search( pPath, true );
if( !t ) return count; // empty
count = t->numfilenames;
Mem_Free( t );
return count;
}
int EntryInTable( SAVERESTOREDATA *pSaveData, const char *pMapName, int index )
{
int i;
index++;
for( i = index; i < pSaveData->connectionCount; i++ )
{
if ( !com.strcmp( pSaveData->levelList[i].mapName, pMapName ) )
return i;
}
return -1;
}
void LandmarkOrigin( SAVERESTOREDATA *pSaveData, vec3_t output, const char *pLandmarkName )
{
int i;
for( i = 0; i < pSaveData->connectionCount; i++ )
{
if( !com.strcmp( pSaveData->levelList[i].landmarkName, pLandmarkName ))
{
VectorCopy( pSaveData->levelList[i].vecLandmarkOrigin, output );
return;
}
}
VectorClear( output );
}
int EntityInSolid( edict_t *ent )
{
edict_t *pParent = ent->v.aiment;
// if you're attached to a client, always go through
if( SV_IsValidEdict( pParent ))
{
if( pParent->v.flags & FL_CLIENT )
return 0;
}
// never suppressing logical entities
if( !ent->v.modelindex )
return 0;
return SV_TestEntityPosition( ent );
}
void ReapplyDecal( SAVERESTOREDATA *pSaveData, decallist_t *entry, qboolean adjacent )
{
int flags = entry->flags;
int decalIndex, entityIndex;
int modelIndex = 0;
if( adjacent ) flags |= FDECAL_DONTSAVE;
// NOTE: at this point all decal indexes is valid
decalIndex = pfnDecalIndex( entry->name );
if( adjacent )
{
// these entities might not exist over transitions,
// so we'll use the saved plane and do a traceline instead
vec3_t testspot, testend;
trace_t tr;
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 );
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;
// FIXME: probably some rotating or moving objects can't receive decal properly
SV_CreateDecal( tr.endpos, decalIndex, entityIndex, modelIndex, flags );
}
}
}
else
{
edict_t *pEdict = pfnPEntityOfEntIndex( entry->entityIndex );
if( pEdict != NULL )
modelIndex = pEdict->v.modelindex;
SV_CreateDecal( entry->position, decalIndex, entry->entityIndex, modelIndex, flags );
}
}
void SV_ClearSaveDir( void )
{
search_t *t;
int i;
// just delete all HL? files
t = FS_SearchExt( "save/*.HL?", true, true ); // lookup only in gamedir
if( !t ) return; // already empty
for( i = 0; i < t->numfilenames; i++ )
{
FS_Delete( t->filenames[i] );
}
Mem_Free( t );
}
int SV_IsValidSave( void )
{
if( !svs.initialized || sv.state != ss_active )
{
Msg( "Not playing a local game.\n" );
return 0;
}
if( CL_Active() == false )
{
Msg( "Can't save if not active.\n" );
return 0;
}
if( sv_maxclients->integer != 1 )
{
Msg( "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 )
{
Msg( "Can't savegame without a player!\n" );
return 0;
}
if( pl->v.deadflag != false )
{
Msg( "Can't savegame with a dead player\n" );
return 0;
}
// Passed all checks, it's ok to save
return 1;
}
Msg( "Can't savegame without a client!\n" );
return 0;
}
void SV_AgeSaveList( const char *pName, int count )
{
string newName, oldName, newImage, oldImage;
// delete last quick/autosave (e.g. quick05.sav)
com.snprintf( newName, sizeof( newName ), "save/%s%02d.sav", pName, count );
com.snprintf( newImage, sizeof( newImage ), "save/%s%02d.%s", pName, count, SI->savshot_ext );
// only delete from game directory, basedir is read-only
FS_Delete( newName );
FS_Delete( newImage );
while( count > 0 )
{
if( count == 1 )
{
// quick.sav
com.snprintf( oldName, sizeof( oldName ), "save/%s.sav", pName );
com.snprintf( oldImage, sizeof( oldImage ), "save/%s.%s", pName, SI->savshot_ext );
}
else
{
// quick04.sav, etc.
com.snprintf( oldName, sizeof( oldName ), "save/%s%02d.sav", pName, count - 1 );
com.snprintf( oldImage, sizeof( oldImage ), "save/%s%02d.%s", pName, count - 1, SI->savshot_ext );
}
com.snprintf( newName, sizeof( newName ), "save/%s%02d.sav", pName, count );
com.snprintf( newImage, sizeof( newImage ), "save/%s%02d.%s", pName, count, SI->savshot_ext );
// Scroll the name list down (rename quick04.sav to quick05.sav)
FS_Rename( oldName, newName );
FS_Rename( oldImage, newImage );
count--;
}
}
void SV_FileCopy( file_t *pOutput, file_t *pInput, int fileSize )
{
char buf[MAX_SYSPATH]; // A small buffer for the copy
int size;
while( fileSize > 0 )
{
if( fileSize > MAX_SYSPATH )
size = MAX_SYSPATH;
else size = fileSize;
FS_Read( pInput, buf, size );
FS_Write( pOutput, buf, size );
fileSize -= size;
}
}
void SV_DirectoryCopy( const char *pPath, file_t *pFile )
{
search_t *t;
int i;
int fileSize;
file_t *pCopy;
char szName[SAVENAME_LENGTH];
t = FS_Search( pPath, true );
if( !t ) return;
for( i = 0; i < t->numfilenames; i++ )
{
fileSize = FS_FileSize( t->filenames[i] );
pCopy = FS_Open( t->filenames[i], "rb" );
// filename can only be as long as a map name + extension
com.strncpy( szName, FS_RemovePath( t->filenames[i] ), SAVENAME_LENGTH );
FS_Write( pFile, szName, SAVENAME_LENGTH );
FS_Write( pFile, &fileSize, sizeof( int ));
SV_FileCopy( pFile, pCopy, fileSize );
FS_Close( pCopy );
}
Mem_Free( t );
}
void SV_DirectoryExtract( file_t *pFile, int fileCount )
{
int i, fileSize;
char szName[SAVENAME_LENGTH], fileName[SAVENAME_LENGTH];
file_t *pCopy;
for( i = 0; i < fileCount; i++ )
{
// filename can only be as long as a map name + extension
FS_Read( pFile, fileName, SAVENAME_LENGTH );
FS_Read( pFile, &fileSize, sizeof( int ));
com.snprintf( szName, sizeof( szName ), "save/%s", fileName );
pCopy = FS_Open( szName, "wb" );
SV_FileCopy( pCopy, pFile, fileSize );
FS_Close( pCopy );
}
}
void SV_SaveFinish( SAVERESTOREDATA *pSaveData )
{
char **pTokens;
ENTITYTABLE *pEntityTable;
pTokens = SaveRestore_DetachSymbolTable( pSaveData );
if( pTokens ) Mem_Free( pTokens );
pEntityTable = SaveRestore_DetachEntityTable( pSaveData );
if( pEntityTable ) Mem_Free( pEntityTable );
if( pSaveData ) Mem_Free( pSaveData );
svgame.globals->pSaveData = NULL;
}
SAVERESTOREDATA *SV_SaveInit( int size )
{
SAVERESTOREDATA *pSaveData;
const int nTokens = 0xfff; // Assume a maximum of 4K-1 symbol table entries(each of some length)
int numents;
if( size <= 0 ) size = 0x80000; // Reserve 512K for now, UNDONE: Shrink this after compressing strings
numents = svgame.numEntities;
pSaveData = Mem_Alloc( host.mempool, sizeof(SAVERESTOREDATA) + ( sizeof(ENTITYTABLE) * numents ) + size );
SaveRestore_Init( pSaveData, (char *)(pSaveData + 1), size ); // skip the save structure
SaveRestore_InitSymbolTable( pSaveData, (char **)Mem_Alloc( host.mempool, nTokens * sizeof( char* )), nTokens );
pSaveData->time = svgame.globals->time; // Use DLL time
VectorClear( pSaveData->vecLandmarkOffset );
pSaveData->fUseLandmark = false;
pSaveData->connectionCount = 0;
// shared with dlls
svgame.globals->pSaveData = pSaveData;
return pSaveData;
}
void SV_SaveGameStateGlobals( SAVERESTOREDATA *pSaveData )
{
sv_client_t *cl;
SAVE_HEADER header;
SAVE_LIGHTSTYLE light;
int i;
// write global data
header.skillLevel = Cvar_VariableValue( "skill" ); // This is created from an int even though it's a float
header.connectionCount = pSaveData->connectionCount;
header.time = svgame.globals->time;
if( sv_skyname->string[0] )
com.strncpy( header.skyName, sv_skyname->string, sizeof( header.skyName ));
else com.strncpy( header.skyName, "", sizeof( header.skyName ));
com.strncpy( header.mapName, sv.name, sizeof( header.mapName ));
header.lightStyleCount = 0;
header.entityCount = svgame.numEntities;
for( i = 0; i < MAX_LIGHTSTYLES; i++ )
{
if( sv.lightstyles[i].pattern[0] )
header.lightStyleCount++;
}
// sky variables
header.skyColor_r = Cvar_VariableValue( "sv_skycolor_r" );
header.skyColor_g = Cvar_VariableValue( "sv_skycolor_g" );
header.skyColor_b = Cvar_VariableValue( "sv_skycolor_b" );
header.skyVec_x = Cvar_VariableValue( "sv_skyvec_x" );
header.skyVec_y = Cvar_VariableValue( "sv_skyvec_y" );
header.skyVec_z = Cvar_VariableValue( "sv_skyvec_z" );
// save viewentity to allow camera works after save\restore
if(( cl = SV_ClientFromEdict( EDICT_NUM( 1 ), true )) != NULL )
{
if( cl->pViewEntity )
header.viewentity = NUM_FOR_EDICT( cl->pViewEntity );
else header.viewentity = 1;
}
else header.viewentity = 1;
pSaveData->time = 0; // prohibits rebase of header.time (why not just save time as a field_float and ditch this hack?)
svgame.dllFuncs.pfnSaveWriteFields( pSaveData, "Save Header", &header, gSaveHeader, ARRAYSIZE( gSaveHeader ));
pSaveData->time = header.time;
// write entity table
for( i = 0; i < pSaveData->tableCount; i++ )
svgame.dllFuncs.pfnSaveWriteFields( pSaveData, "ETABLE", pSaveData->pTable + i, gEntityTable, ARRAYSIZE( gEntityTable ));
// write 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] )
{
light.index = i;
com.strncpy( light.style, sv.lightstyles[i].pattern, sizeof( light.style ));
svgame.dllFuncs.pfnSaveWriteFields( pSaveData, "LIGHTSTYLE", &light, gLightStyle, ARRAYSIZE( gLightStyle ));
}
}
}
SAVERESTOREDATA *SV_LoadSaveData( const char *level )
{
string name;
file_t *pFile;
SaveFileSectionsInfo_t sectionsInfo;
SAVERESTOREDATA *pSaveData;
char *pszTokenList;
int i, id, size, version;
com.snprintf( name, sizeof( name ), "save/%s.HL1", level );
MsgDev( D_INFO, "Loading game from %s...\n", name );
pFile = FS_OpenEx( name, "rb", true );
if( !pFile )
{
MsgDev( D_INFO, "ERROR: couldn't open.\n" );
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, &sectionsInfo, sizeof( sectionsInfo ));
pSaveData = Mem_Alloc( host.mempool, sizeof(SAVERESTOREDATA) + SumBytes( &sectionsInfo ));
com.strncpy( pSaveData->szCurrentMapName, level, sizeof( pSaveData->szCurrentMapName ));
FS_Read( pFile, (char *)(pSaveData + 1), SumBytes( &sectionsInfo ));
FS_Close( pFile );
// Parse the symbol table
pszTokenList = (char *)(pSaveData + 1); // Skip past the CSaveRestoreData structure
if( sectionsInfo.nBytesSymbols > 0 )
{
SaveRestore_InitSymbolTable( pSaveData, (char **)Mem_Alloc( host.mempool, sectionsInfo.nSymbols * sizeof( char* )), sectionsInfo.nSymbols );
// make sure the token strings pointed to by the pToken hashtable.
for( i = 0; i < sectionsInfo.nSymbols; i++ )
{
if( *pszTokenList )
{
ASSERT( SaveRestore_DefineSymbol( pSaveData, pszTokenList, i ));
}
while( *pszTokenList++ ); // find next token (after next null)
}
}
else
{
SaveRestore_InitSymbolTable( pSaveData, NULL, 0 );
}
ASSERT( pszTokenList - (char *)(pSaveData + 1) == sectionsInfo.nBytesSymbols );
// set up the restore basis
size = SumBytes( &sectionsInfo ) - sectionsInfo.nBytesSymbols;
// the point pszTokenList was incremented to the end of the tokens
SaveRestore_Init( pSaveData, (char *)(pszTokenList), size );
pSaveData->connectionCount = 0;
pSaveData->fUseLandmark = true;
pSaveData->time = 0.0f;
VectorClear( pSaveData->vecLandmarkOffset );
// shared with dlls
svgame.globals->pSaveData = pSaveData;
return pSaveData;
}
void SV_ReadEntityTable( SAVERESTOREDATA *pSaveData )
{
ENTITYTABLE *pEntityTable;
int i;
pEntityTable = (ENTITYTABLE *)Mem_Alloc( host.mempool, sizeof( ENTITYTABLE ) * pSaveData->tableCount );
SaveRestore_InitEntityTable( pSaveData, pEntityTable, pSaveData->tableCount );
for( i = 0; i < pSaveData->tableCount; i++ )
svgame.dllFuncs.pfnSaveReadFields( pSaveData, "ETABLE", pSaveData->pTable + i, gEntityTable, ARRAYSIZE( gEntityTable ));
}
void SV_ParseSaveTables( SAVERESTOREDATA *pSaveData, SAVE_HEADER *pHeader, int updateGlobals )
{
int i;
SAVE_LIGHTSTYLE light;
// process SAVE_HEADER
svgame.dllFuncs.pfnSaveReadFields( pSaveData, "Save Header", pHeader, gSaveHeader, ARRAYSIZE( gSaveHeader ));
pSaveData->connectionCount = pHeader->connectionCount;
pSaveData->time = pHeader->time;
pSaveData->fUseLandmark = true;
VectorClear( pSaveData->vecLandmarkOffset );
pSaveData->tableCount = pHeader->entityCount;
SV_ReadEntityTable( pSaveData );
// read adjacency list
for( i = 0; i < pSaveData->connectionCount; i++ )
{
LEVELLIST *pList = &pSaveData->levelList[i];
svgame.dllFuncs.pfnSaveReadFields( pSaveData, "ADJACENCY", pList, gAdjacency, ARRAYSIZE( gAdjacency ));
}
if( updateGlobals ) // g-cont. maybe this rename to 'clearLightstyles' ?
{
Mem_Set( 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 );
}
}
/*
=============
SV_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)
=============
*/
void SV_EntityPatchWrite( SAVERESTOREDATA *pSaveData, const char *level )
{
string name;
file_t *pFile;
int i, size;
com.snprintf( name, sizeof( name ), "save/%s.HL3", level );
pFile = FS_Open( name, "wb" );
if( !pFile ) return;
for( i = size = 0; i < pSaveData->tableCount; i++ )
{
if( pSaveData->pTable[i].flags & FENTTABLE_REMOVED )
size++;
}
// patch count
FS_Write( pFile, &size, sizeof( int ));
for( i = 0; i < pSaveData->tableCount; i++ )
{
if( pSaveData->pTable[i].flags & FENTTABLE_REMOVED )
FS_Write( pFile, &i, sizeof( int ));
}
FS_Close( pFile );
}
/*
=============
SV_EntityPatchRead
read the list of entities that are no longer in the save file for this level
(they've been moved to another level)
=============
*/
void SV_EntityPatchRead( SAVERESTOREDATA *pSaveData, const char *level )
{
string name;
file_t *pFile;
int i, size, entityId;
com.snprintf( name, sizeof( name ), "save/%s.HL3", level );
pFile = FS_OpenEx( name, "rb", true );
if( !pFile ) 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 );
}
/*
=============
SV_SaveClientState
write out the list of premanent decals for this level
=============
*/
void SV_SaveClientState( SAVERESTOREDATA *pSaveData, const char *level )
{
string name;
file_t *pFile;
decallist_t *decalList;
int i, decalCount;
int id, version;
com.snprintf( name, sizeof( name ), "save/%s.HL2", level );
pFile = FS_Open( name, "wb" );
if( !pFile ) return;
id = SAVEFILE_HEADER;
version = SAVEGAME_VERSION;
// write the header
FS_Write( pFile, &id, sizeof( int ));
FS_Write( pFile, &version, sizeof( int ));
decalList = (decallist_t *)Z_Malloc(sizeof( decallist_t ) * MAX_RENDER_DECALS );
decalCount = Host_CreateDecalList( decalList, svgame.globals->changelevel );
FS_Write( pFile, &decalCount, sizeof( int ));
// we can't use SaveRestore system here...
for( i = 0; i < decalCount; i++ )
{
vec3_t localPos;
decallist_t *entry;
byte nameSize;
entry = &decalList[i];
if( pSaveData->fUseLandmark )
VectorSubtract( entry->position, pSaveData->vecLandmarkOffset, localPos );
else VectorCopy( entry->position, localPos );
nameSize = com.strlen( entry->name ) + 1;
FS_Write( pFile, localPos, sizeof( localPos ));
FS_Write( pFile, &nameSize, sizeof( nameSize ));
FS_Write( pFile, entry->name, nameSize );
FS_Write( pFile, &entry->entityIndex, sizeof( entry->entityIndex ));
FS_Write( pFile, &entry->flags, sizeof( entry->flags ));
FS_Write( pFile, entry->impactPlaneNormal, sizeof( entry->impactPlaneNormal ));
}
Z_Free( decalList );
FS_Close( pFile );
}
/*
=============
SV_LoadClientState
read the list of decals and reapply them again
=============
*/
void SV_LoadClientState( SAVERESTOREDATA *pSaveData, const char *level, qboolean adjacent )
{
string name;
file_t *pFile;
int i, tag;
decallist_t *decalList;
int decalCount;
com.snprintf( name, sizeof( name ), "save/%s.HL2", level );
pFile = FS_OpenEx( name, "rb", true );
if( !pFile ) return;
FS_Read( pFile, &tag, sizeof( int ));
if( tag != SAVEFILE_HEADER )
{
FS_Close( pFile );
return;
}
FS_Read( pFile, &tag, sizeof( int ));
if( tag != SAVEGAME_VERSION )
{
FS_Close( pFile );
return;
}
if( adjacent ) MsgDev( D_INFO, "Loading decals from %s\n", level );
// read the decalCount
FS_Read( pFile, &decalCount, sizeof( int ));
decalList = (decallist_t *)Z_Malloc( sizeof( decallist_t ) * decalCount );
// we can't use SaveRestore system here...
for( i = 0; i < decalCount; i++ )
{
vec3_t localPos;
decallist_t *entry;
byte nameSize;
entry = &decalList[i];
FS_Read( pFile, localPos, sizeof( localPos ));
if( pSaveData->fUseLandmark )
VectorAdd( localPos, pSaveData->vecLandmarkOffset, entry->position );
else VectorCopy( localPos, entry->position );
FS_Read( pFile, &nameSize, sizeof( nameSize ));
FS_Read( pFile, entry->name, nameSize );
FS_Read( pFile, &entry->entityIndex, sizeof( entry->entityIndex ));
FS_Read( pFile, &entry->flags, sizeof( entry->flags ));
FS_Read( pFile, entry->impactPlaneNormal, sizeof( entry->impactPlaneNormal ));
ReapplyDecal( pSaveData, entry, adjacent );
}
Z_Free( decalList );
FS_Close( pFile );
}
/*
=============
SV_SaveGameState
save current game state
=============
*/
SAVERESTOREDATA *SV_SaveGameState( void )
{
SaveFileSectionsInfo_t sectionsInfo;
SaveFileSections_t sections;
SAVERESTOREDATA *pSaveData;
ENTITYTABLE *pTable;
file_t *pFile;
int i, numents;
int id, version;
pSaveData = SV_SaveInit( 0 );
// Save the data
sections.pData = SaveRestore_AccessCurPos( pSaveData );
numents = svgame.numEntities;
SaveRestore_InitEntityTable( pSaveData, Mem_Alloc( host.mempool, sizeof(ENTITYTABLE) * numents ), numents );
// Build the adjacent map list (after entity table build by game in presave)
svgame.dllFuncs.pfnParmsChangeLevel();
// write entity descriptions
for( i = 0; i < svgame.numEntities; i++ )
{
edict_t *pent = EDICT_NUM( i );
pTable = &pSaveData->pTable[pSaveData->currentIndex];
svgame.dllFuncs.pfnSave( pent, pSaveData );
if( pent->v.flags & FL_CLIENT ) // mark client
pTable->flags |= FENTTABLE_PLAYER;
if( pTable->classname && pTable->size )
pTable->id = pent->serialnumber;
pSaveData->currentIndex++; // move pointer
}
sectionsInfo.nBytesData = SaveRestore_AccessCurPos( pSaveData ) - sections.pData;
// Save necessary tables/dictionaries/directories
sections.pDataHeaders = SaveRestore_AccessCurPos( pSaveData );
SV_SaveGameStateGlobals( pSaveData );
sectionsInfo.nBytesDataHeaders = SaveRestore_AccessCurPos( pSaveData ) - sections.pDataHeaders;
// Write the save file symbol table
sections.pSymbols = SaveRestore_AccessCurPos( pSaveData );
for( i = 0; i < pSaveData->tokenCount; i++ )
{
const char *pszToken = (SaveRestore_StringFromSymbol( pSaveData, i ));
if( !pszToken ) pszToken = "";
if( !SaveRestore_Write( pSaveData, pszToken, com.strlen( pszToken ) + 1 ))
break;
}
sectionsInfo.nBytesSymbols = SaveRestore_AccessCurPos( pSaveData ) - sections.pSymbols;
sectionsInfo.nSymbols = pSaveData->tokenCount;
id = SAVEFILE_HEADER;
version = SAVEGAME_VERSION;
// output to disk
pFile = FS_Open( va( "save/%s.HL1", sv.name ), "wb" );
if( !pFile ) return NULL;
// write the header
FS_Write( pFile, &id, sizeof( int ));
FS_Write( pFile, &version, sizeof( int ));
// 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, &sectionsInfo, sizeof( sectionsInfo ));
FS_Write( pFile, sections.pSymbols, sectionsInfo.nBytesSymbols );
FS_Write( pFile, sections.pDataHeaders, sectionsInfo.nBytesDataHeaders );
FS_Write( pFile, sections.pData, sectionsInfo.nBytesData );
FS_Close( pFile );
SV_EntityPatchWrite( pSaveData, sv.name );
SV_SaveClientState( pSaveData, sv.name );
return pSaveData;
}
int SV_LoadGameState( char const *level, qboolean createPlayers )
{
SAVE_HEADER header;
SAVERESTOREDATA *pSaveData;
ENTITYTABLE *pEntInfo;
edict_t *pent;
int i;
pSaveData = SV_LoadSaveData( level );
if( !pSaveData ) return 0; // couldn't load the file
SV_ParseSaveTables( pSaveData, &header, 1 );
SV_EntityPatchRead( pSaveData, level );
Cvar_SetFloat( "skill", header.skillLevel );
com.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_SetFloat( "sv_skycolor_r", header.skyColor_r );
Cvar_SetFloat( "sv_skycolor_g", header.skyColor_g );
Cvar_SetFloat( "sv_skycolor_b", header.skyColor_b );
Cvar_SetFloat( "sv_skyvec_x", header.skyVec_x );
Cvar_SetFloat( "sv_skyvec_y", header.skyVec_y );
Cvar_SetFloat( "sv_skyvec_z", header.skyVec_z );
sv.viewentity = ( header.viewentity == 1 ) ? 0 : (word)header.viewentity;
// re-base the savedata since we re-ordered the entity/table / restore fields
SaveRestore_Rebase( pSaveData );
// create entity list
for( i = 0; i < pSaveData->tableCount; i++ )
{
pEntInfo = &pSaveData->pTable[i];
if( pEntInfo->classname != 0 && pEntInfo->size && !( pEntInfo->flags & FENTTABLE_REMOVED ))
{
if( pEntInfo->id == 0 ) // worldspawn
{
ASSERT( i == 0 );
pent = EDICT_NUM( 0 );
SV_InitEdict( pent );
pent = SV_AllocPrivateData( pent, pEntInfo->classname );
SaveRestore_Seek( pSaveData, pEntInfo->location );
if( svgame.dllFuncs.pfnRestore( pent, pSaveData, false ) < 0 )
{
pEntInfo->pent = NULL;
pent->v.flags |= FL_KILLME;
}
}
else if(( pEntInfo->id > 0 ) && ( pEntInfo->id < svgame.globals->maxClients + 1 ))
{
edict_t *ed;
if(!( pEntInfo->flags & FENTTABLE_PLAYER ))
{
MsgDev( D_WARN, "ENTITY IS NOT A PLAYER: %d\n", i );
ASSERT( 0 );
}
ed = EDICT_NUM( pEntInfo->id );
if( ed && createPlayers )
{
ASSERT( ed->free == false );
// create the player
pent = SV_AllocPrivateData( ed, pEntInfo->classname );
}
else pent = NULL;
}
else
{
pent = SV_AllocPrivateData( NULL, pEntInfo->classname );
}
pEntInfo->pent = pent;
}
else
{
pEntInfo->pent = NULL; // invalid
}
}
// now spawn entities
for( i = 0; i < pSaveData->tableCount; i++ )
{
pEntInfo = &pSaveData->pTable[i];
if( pEntInfo->id != 0 )
{
pent = pEntInfo->pent;
SaveRestore_Seek( pSaveData, pEntInfo->location );
if( pent )
{
if( svgame.dllFuncs.pfnRestore( pent, pSaveData, false ) < 0 )
{
pEntInfo->pent = NULL;
pent->v.flags |= FL_KILLME;
}
}
}
}
SV_LoadClientState( pSaveData, level, false );
SV_SaveFinish( pSaveData );
// restore server time
sv.time = header.time;
return 1;
}
//-----------------------------------------------------------------------------
int SV_CreateEntityTransitionList( SAVERESTOREDATA *pSaveData, int levelMask )
{
edict_t *pent;
ENTITYTABLE *pEntInfo;
int i, movedCount, active;
movedCount = 0;
// create entity list
for( i = 0; i < pSaveData->tableCount; i++ )
{
pent = NULL;
pEntInfo = &pSaveData->pTable[i];
if( pEntInfo->size && pEntInfo->id != 0 )
{
if( pEntInfo->classname != 0 )
{
active = (pEntInfo->flags & levelMask) ? 1 : 0;
// spawn players
if(( pEntInfo->id > 0) && ( pEntInfo->id < svgame.globals->maxClients + 1 ))
{
edict_t *ed = EDICT_NUM( pEntInfo->id );
if( active && ed && !ed->free )
{
if(!( pEntInfo->flags & FENTTABLE_PLAYER ))
{
MsgDev( D_WARN, "ENTITY IS NOT A PLAYER: %d\n", i );
ASSERT( 0 );
}
pent = SV_AllocPrivateData( ed, pEntInfo->classname );
}
}
else if( active )
{
// create named entity
pent = SV_AllocPrivateData( NULL, pEntInfo->classname );
}
}
else
{
MsgDev( D_WARN, "Entity with data saved, but with no classname\n" );
}
}
pEntInfo->pent = pent;
}
// re-base the savedata since we re-ordered the entity/table / restore fields
SaveRestore_Rebase( pSaveData );
// now spawn entities
for( i = 0; i < pSaveData->tableCount; i++ )
{
pEntInfo = &pSaveData->pTable[i];
pent = pEntInfo->pent;
pSaveData->currentIndex = i;
SaveRestore_Seek( pSaveData, pEntInfo->location );
if( SV_IsValidEdict( pent ) && ( pEntInfo->flags & levelMask )) // screen out the player if he's not to be spawned
{
if( pEntInfo->flags & FENTTABLE_GLOBAL )
{
MsgDev( D_INFO, "Merging changes for global: %s\n", STRING( pEntInfo->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, true ) > 0 )
{
movedCount++;
}
}
else
{
MsgDev( D_INFO, "Transferring %s (%d)\n", STRING( pEntInfo->classname ), pent->serialnumber );
if( svgame.dllFuncs.pfnRestore( pent, pSaveData, false ) < 0 )
{
pent->v.flags |= FL_KILLME;
}
else
{
if(!( pEntInfo->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
MsgDev( D_INFO, "Suppressing %s\n", STRING( pEntInfo->classname ));
pent->v.flags |= FL_KILLME;
}
else
{
movedCount++;
pEntInfo->flags = FENTTABLE_REMOVED;
}
}
}
// remove any entities that were removed using UTIL_Remove()
// as a result of the above calls to UTIL_RemoveImmediate()
SV_FreeOldEntities ();
}
}
return movedCount;
}
void SV_LoadAdjacentEnts( const char *pOldLevel, const char *pLandmarkName )
{
SAVE_HEADER header;
SAVERESTOREDATA currentLevelData, *pSaveData;
int i, test, flags, index, movedCount = 0;
vec3_t landmarkOrigin;
qboolean foundprevious = false;
Mem_Set( &currentLevelData, 0, sizeof( SAVERESTOREDATA ));
svgame.globals->pSaveData = &currentLevelData;
// 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( !com.stricmp( currentLevelData.levelList[i].mapName, pOldLevel ))
{
foundprevious = true;
}
for( test = 0; test < i; test++ )
{
// only do maps once
if( !com.strcmp( currentLevelData.levelList[i].mapName, currentLevelData.levelList[test].mapName ))
break;
}
// map was already in the list
if( test < i ) continue;
MsgDev( D_NOTE, "Merging entities from %s ( at %s )\n", currentLevelData.levelList[i].mapName, currentLevelData.levelList[i].landmarkName );
pSaveData = SV_LoadSaveData( currentLevelData.levelList[i].mapName );
if( pSaveData )
{
SV_ParseSaveTables( pSaveData, &header, 0 );
SV_EntityPatchRead( pSaveData, currentLevelData.levelList[i].mapName );
pSaveData->time = sv_time(); // - header.time;
pSaveData->fUseLandmark = true;
// calculate landmark offset
LandmarkOrigin( &currentLevelData, landmarkOrigin, pLandmarkName );
LandmarkOrigin( pSaveData, pSaveData->vecLandmarkOffset, pLandmarkName );
VectorSubtract( landmarkOrigin, pSaveData->vecLandmarkOffset, pSaveData->vecLandmarkOffset );
flags = 0;
if( !com.strcmp( currentLevelData.levelList[i].mapName, pOldLevel ))
flags |= FENTTABLE_PLAYER;
index = -1;
while( 1 )
{
index = EntryInTable( pSaveData, sv.name, index );
if( index < 0 ) break;
flags |= 1<<index;
}
if( flags ) movedCount = SV_CreateEntityTransitionList( pSaveData, flags );
// if ents were moved, rewrite entity table to save file
if( movedCount ) SV_EntityPatchWrite( pSaveData, currentLevelData.levelList[i].mapName );
// move the decals from another level
SV_LoadClientState( pSaveData, currentLevelData.levelList[i].mapName, true );
SV_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_ChangeLevel
=============
*/
void SV_ChangeLevel( qboolean loadfromsavedgame, const char *mapname, const char *start )
{
string level;
string oldlevel;
string _startspot;
char *startspot;
SAVERESTOREDATA *pSaveData = NULL;
if( sv.state != ss_active )
{
Msg( "SV_ChangeLevel: server not running\n");
return;
}
if( !start )
{
startspot = NULL;
}
else
{
com.strncpy( _startspot, start, MAX_STRING );
startspot = _startspot;
}
com.strncpy( level, mapname, MAX_STRING );
com.strncpy( oldlevel, sv.name, MAX_STRING );
if( loadfromsavedgame )
{
// smooth transition in-progress
svgame.globals->changelevel = true;
// save the current level's state
pSaveData = SV_SaveGameState();
sv.loadgame = true;
}
SV_InactivateClients ();
SV_DeactivateServer ();
if( !SV_SpawnServer( level, startspot ))
return;
if( loadfromsavedgame )
{
// Finish saving gamestate
SV_SaveFinish( pSaveData );
svgame.globals->changelevel = true;
SV_LevelInit( level, oldlevel, startspot, true );
sv.paused = true; // pause until all clients connect
sv.loadgame = true;
}
else
{
SV_LevelInit( level, NULL, NULL, false );
}
SV_ActivateServer ();
}
int SV_SaveGameSlot( const char *pSaveName, const char *pSaveComment )
{
string hlPath, name;
char *pTokenData;
SAVERESTOREDATA *pSaveData;
GAME_HEADER gameHeader;
int i, tag, tokenSize;
file_t *pFile;
pSaveData = SV_SaveGameState();
if( !pSaveData ) return 0;
SV_SaveFinish( pSaveData );
pSaveData = SV_SaveInit( 0 );
com.strncpy( hlPath, "save/*.HL?", sizeof( hlPath ));
gameHeader.mapCount = SV_MapCount( hlPath );
com.strncpy( gameHeader.mapName, sv.name, sizeof( gameHeader.mapName ));
com.strncpy( gameHeader.comment, pSaveComment, sizeof( gameHeader.comment ));
svgame.dllFuncs.pfnSaveWriteFields( pSaveData, "GameHeader", &gameHeader, gGameHeader, ARRAYSIZE( gGameHeader ));
svgame.dllFuncs.pfnSaveGlobalState( pSaveData );
// write entity string token table
pTokenData = SaveRestore_AccessCurPos( pSaveData );
for( i = 0; i < pSaveData->tokenCount; i++ )
{
const char *pszToken = (SaveRestore_StringFromSymbol( pSaveData, i ));
if( !pszToken ) pszToken = "";
if( !SaveRestore_Write( pSaveData, pszToken, com.strlen( pszToken ) + 1 ))
{
MsgDev( D_ERROR, "Token Table Save/Restore overflow!\n" );
break;
}
}
tokenSize = SaveRestore_AccessCurPos( pSaveData ) - pTokenData;
SaveRestore_Rewind( pSaveData, tokenSize );
com.snprintf( name, sizeof( name ), "save/%s.sav", pSaveName );
MsgDev( D_INFO, "Saving game to %s...\n", name );
Cbuf_AddText( va( "saveshot \"%s\"\n", pSaveName ));
// output to disk
if( com.stricmp( pSaveName, "quick" ) || com.stricmp( pSaveName, "autosave" ))
SV_AgeSaveList( pSaveName, SAVE_AGED_COUNT );
pFile = FS_Open( name, "wb" );
tag = SAVEGAME_HEADER;
FS_Write( pFile, &tag, sizeof( int ));
tag = SAVEGAME_VERSION;
FS_Write( pFile, &tag, sizeof( int ));
tag = SaveRestore_GetCurPos( pSaveData );
FS_Write( pFile, &tag, sizeof( int )); // does not include token table
// write out the tokens first so we can load them before we load the entities
tag = pSaveData->tokenCount;
FS_Write( pFile, &tag, sizeof( int ));
FS_Write( pFile, &tokenSize, sizeof( int ));
FS_Write( pFile, pTokenData, tokenSize );
// save gamestate
FS_Write( pFile, SaveRestore_GetBuffer( pSaveData ), SaveRestore_GetCurPos( pSaveData ));
SV_DirectoryCopy( hlPath, pFile );
FS_Close( pFile );
SV_SaveFinish( pSaveData );
return 1;
}
int SV_SaveReadHeader( file_t *pFile, GAME_HEADER *pHeader, int readGlobalState )
{
int i, tag, size, tokenCount, tokenSize;
char *pszTokenList;
SAVERESTOREDATA *pSaveData;
FS_Read( pFile, &tag, sizeof( int ));
if( tag != SAVEGAME_HEADER )
{
FS_Close( pFile );
return 0;
}
FS_Read( pFile, &tag, sizeof( int ));
if( tag != 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 = Mem_Alloc( host.mempool, sizeof( SAVERESTOREDATA ) + tokenSize + size );
pSaveData->connectionCount = 0;
pszTokenList = (char *)(pSaveData + 1);
if( tokenSize > 0 )
{
FS_Read( pFile, pszTokenList, tokenSize );
SaveRestore_InitSymbolTable( pSaveData, (char **)Mem_Alloc( host.mempool, tokenCount * sizeof( char* )), tokenCount );
// make sure the token strings pointed to by the pToken hashtable.
for( i = 0; i < tokenCount; i++ )
{
if( *pszTokenList )
{
ASSERT( SaveRestore_DefineSymbol( pSaveData, pszTokenList, i ));
}
while( *pszTokenList++ ); // find next token (after next null)
}
}
else
{
SaveRestore_InitSymbolTable( pSaveData, NULL, 0 );
}
pSaveData->fUseLandmark = false;
pSaveData->time = 0.0f;
// pszTokenList now points after token data
SaveRestore_Init( pSaveData, (char *)(pszTokenList), size );
FS_Read( pFile, SaveRestore_GetBuffer( pSaveData ), size );
if( readGlobalState )
svgame.dllFuncs.pfnResetGlobalState();
svgame.dllFuncs.pfnSaveReadFields( pSaveData, "GameHeader", pHeader, gGameHeader, ARRAYSIZE( gGameHeader ));
if( readGlobalState )
svgame.dllFuncs.pfnRestoreGlobalState( pSaveData );
SV_SaveFinish( pSaveData );
return 1;
}
qboolean SV_LoadGame( const char *pName )
{
file_t *pFile;
qboolean validload = false;
GAME_HEADER gameHeader;
string name;
if( host.type == HOST_DEDICATED )
return false;
if( !pName || !pName[0] )
return false;
com.snprintf( name, sizeof( name ), "save/%s.sav", pName );
MsgDev( D_INFO, "Loading game from %s...\n", name );
SV_ClearSaveDir();
if( !svs.initialized ) SV_InitGame ();
pFile = FS_OpenEx( name, "rb", true );
if( pFile )
{
if( SV_SaveReadHeader( pFile, &gameHeader, 1 ))
{
SV_DirectoryExtract( pFile, gameHeader.mapCount );
validload = true;
}
FS_Close( pFile );
}
else MsgDev( D_ERROR, "File not found or failed to open.\n" );
if( !validload )
{
com.snprintf( host.finalmsg, MAX_STRING, "Couldn't load %s.sav\n", pName );
SV_Shutdown( false );
return false;
}
Cvar_FullSet( "coop", "0", CVAR_LATCH );
Cvar_FullSet( "teamplay", "0", CVAR_LATCH );
Cvar_FullSet( "deathmatch", "0", CVAR_LATCH );
return Host_NewGame( gameHeader.mapName, true );
}
/*
==================
SV_SaveGetName
==================
*/
void SV_SaveGetName( int lastnum, char *filename )
{
int a, b, c;
if( !filename ) return;
if( lastnum < 0 || lastnum > 999 )
{
// bound
com.strcpy( filename, "error" );
return;
}
a = lastnum / 100;
lastnum -= a * 100;
b = lastnum / 10;
c = lastnum % 10;
com.sprintf( filename, "save%i%i%i", a, b, c );
}
void SV_SaveGame( const char *pName )
{
char comment[80];
string savename;
int n;
if( !pName || !*pName )
return;
// can we save at this point?
if( !SV_IsValidSave( ))
return;
if( !com.stricmp( pName, "new" ))
{
// scan for a free filename
for( n = 0; n < 999; n++ )
{
SV_SaveGetName( n, savename );
if( !FS_FileExists( va( "save/%s.sav", savename )))
break;
}
if( n == 1000 )
{
Msg( "^3ERROR: no free slots for savegame\n" );
return;
}
}
else com.strncpy( savename, pName, sizeof( savename ));
// make sure what oldsave is removed
if( FS_FileExists( va( "save/%s.sav", savename )))
FS_Delete( va( "save/%s.sav", savename ));
if( FS_FileExists( va( "save/%s.%s", savename, SI->savshot_ext )))
FS_Delete( va( "save/%s.%s", savename, SI->savshot_ext ));
// HACKHACK: unload previous shader from memory
if( re ) re->FreeShader( va( "save/%s.%s", savename, SI->savshot_ext ));
SV_BuildSaveComment( comment, sizeof( comment ));
SV_SaveGameSlot( savename, comment );
// HACKHACK: send usermessage from engine
if( com.stricmp( pName, "autosave" ) && svgame.gmsgHudText != -1 )
{
const char *pMsg = "GAMESAVED"; // defined in titles.txt
sv_client_t *cl;;
if(( cl = SV_ClientFromEdict( EDICT_NUM( 1 ), true )) != NULL )
{
BF_WriteByte( &cl->netchan.message, svgame.gmsgHudText );
BF_WriteByte( &cl->netchan.message, com.strlen( pMsg ) + 1 );
BF_WriteString( &cl->netchan.message, pMsg );
}
}
}
/*
==================
SV_GetLatestSave
used for reload game after player death
==================
*/
const char *SV_GetLatestSave( void )
{
search_t *f = FS_SearchExt( "save/*.sav", true, true ); // lookup only in gamedir
int i, found = 0;
long newest = 0, ft;
string savename;
if( !f ) return NULL;
for( i = 0; i < f->numfilenames; i++ )
{
ft = FS_FileTime( f->filenames[i] );
// found a match?
if( ft > 0 )
{
// should we use the matche?
if( !found || Host_CompareFileTime( newest, ft ) < 0 )
{
newest = ft;
com.strncpy( savename, f->filenames[i], MAX_STRING );
found = 1;
}
}
}
Mem_Free( f ); // release search
if( found )
return va( "%s", savename ); // move to static memory
return NULL;
}
qboolean SV_GetComment( const char *savename, char *comment )
{
int i, tag, size, nNumberOfFields, nFieldSize, tokenSize, tokenCount;
char *pData, *pSaveData, *pFieldName, **pTokenList;
string name, description;
file_t *f;
f = FS_Open( savename, "rb" );
if( !f )
{
// just not exist - clear comment
com.strncpy( comment, "", MAX_STRING );
return 0;
}
FS_Read( f, &tag, sizeof( int ));
if( tag != SAVEGAME_HEADER )
{
// invalid header
com.strncpy( comment, "<corrupted>", MAX_STRING );
FS_Close( f );
return 0;
}
FS_Read( f, &tag, sizeof( int ));
if( tag == 0x0071 )
{
com.strncpy( comment, "Gold Source <unsupported>", MAX_STRING );
FS_Close( f );
return 0;
}
if( tag < SAVEGAME_VERSION )
{
com.strncpy( comment, "<old version>", MAX_STRING );
FS_Close( f );
return 0;
}
if( tag > SAVEGAME_VERSION )
{
// old xash version ?
com.strncpy( comment, "<unknown version>", MAX_STRING );
FS_Close( f );
return 0;
}
name[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 > ( 1024 * 1024 * 32 ))
{
com.strncpy( comment, "<corrupted>", MAX_STRING );
FS_Close( f );
return 0;
}
if( tokenSize < 0 || tokenSize > ( 1024 * 1024 * 32 ))
{
com.strncpy( comment, "<corrupted>", MAX_STRING );
FS_Close( f );
return 0;
}
pSaveData = (char *)Mem_Alloc( 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_Alloc( 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( com.stricmp( pFieldName, "GameHeader" ))
{
com.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( !com.stricmp( pFieldName, "comment" ))
{
com.strncpy( description, pData, nFieldSize );
}
else if( !com.stricmp( pFieldName, "mapName" ))
{
com.strncpy( name, 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 );
if( com.strlen( name ) > 0 && com.strlen( description ) > 0 )
{
time_t fileTime;
const struct tm *file_tm;
string timestring;
fileTime = FS_FileTime( savename );
file_tm = localtime( &fileTime );
// split comment to sections
if( com.strstr( savename, "quick" ))
com.strncat( comment, "[quick]", CS_SIZE );
else if( com.strstr( savename, "autosave" ))
com.strncat( comment, "[autosave]", CS_SIZE );
com.strncat( comment, description, CS_SIZE );
strftime( timestring, sizeof ( timestring ), "%b%d %Y", file_tm );
com.strncpy( comment + CS_SIZE, timestring, CS_TIME );
strftime( timestring, sizeof( timestring ), "%H:%M", file_tm );
com.strncpy( comment + CS_SIZE + CS_TIME, timestring, CS_TIME );
com.strncpy( comment + CS_SIZE + (CS_TIME * 2), description + CS_SIZE, CS_SIZE );
return 1;
}
com.strncpy( comment, "<unknown version>", MAX_STRING );
return 0;
}