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