Saving and Loading a Game

An absolutely critical part of a single player game is the ability to save the game at any point and be able to reload it at will. Quake 3 was not designed with this in mind and it requires some ingenious coding to implement it. Fortunately, RTCW and ST:EF both have single player games and the technique to save and load games was developed for them.

Source File

The source file containing the save/load code is called g_savestate.c and is available on the Downloads page as part of the Single Player Source Package. Place this file in the code/game folder and add it to the game project.

New Functions Needed

Some new functions that are used by the save/load routines have to be added to the game source. This is done as follows :

In q_shared.h about line 874 add :

int Q_isalpha( int c );
#ifdef SINGLEPLAYER
int Q_isnumeric( int c );
int Q_isalphanumeric( int c );
int Q_isforfilename( int c );
#endif

In q_shared.c about line 680 add :

#ifdef SINGLEPLAYER
int Q_isnumeric( int c )
{
if (c >= '0' && c <= '9')
return ( 1 );
return ( 0 );
}
int Q_isalphanumeric( int c )
{
if( Q_isalpha(c) ||
Q_isnumeric(c) )
return( 1 );
return ( 0 );
}
int Q_isforfilename( int c )
{
if( (Q_isalphanumeric(c) || c == '_' ) && c!= ' ' ) // space not allowed in filename
return( 1 );
return ( 0 );
}
#endif

In g_local.h about line 630 add :

#ifdef SINGLEPLAYER
//
// g_savestate.c
//
void G_LoadGame(char *username);
qboolean G_SaveGame(char *username);
#endif

Save Method

The method used to save games is simple in concept but more complicated in implementation. To create a saved game you must save the data for all the entities, all the data for each connected client, plus a few other things such as level time. The complications arise because entities and clients contain pointers to other data and these cannot just be directly saved and reloaded. Instead these pointers must be converted to something else when saved and converted back when reloading.

The pointers that must be converted include pointers to string data, pointers to entities, pointers to items, pointers to clients and pointers to functions. The pointer to a function is the only one that requires changes outside the save/load code.

A look at the entity structure gentity_s in g_local.h will reveal the following pointers to functions :

	void		(*think)(gentity_t *self);
void (*reached)(gentity_t *self); // movers call this when hitting endpoint
void (*blocked)(gentity_t *self, gentity_t *other);
void (*touch)(gentity_t *self, gentity_t *other, trace_t *trace);
void (*use)(gentity_t *self, gentity_t *other, gentity_t *activator);
void (*pain)(gentity_t *self, gentity_t *attacker, int damage);
void (*die)(gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod);

What we will be doing here is to convert the pointer to the function to the name of the function when we save the data and convert it back to the pointer when we load it. To accomplish this we first need a list of all functions that may be used by one of the above pointers so we can reference them in our conversion code. And we must make sure that none of them are static functions, so we can reference them from our save/load code. The following source code changes are needed to convert two functions from static to global :

In g_misc.c about line 288 add :

#ifndef SINGLEPLAYER // static fixup
static void InitShooter_Finish( gentity_t *ent ) {
#else
void InitShooter_Finish( gentity_t *ent ) {
#endif 

In g_target.c about line 423 add :

#ifndef SINGLEPLAYER // static fixup
static void target_location_linkup(gentity_t *ent)
#else
void target_location_linkup(gentity_t *ent)
#endif

Looking at the source in g_savestate.c we see a list of external declarations of functions used by each of the entity function pointers. Also the structure funcList contains the function name and a pointer to the function, which will be used in the pointer conversion. Any new functions which are added to your mod source and are referenced by the entity function pointers must be added to both these lists. Otherwise the save/load routines will not work properly.

The structure gentityFields contains the entity pointers which must be converted for saving and loading. The structure gclientFields contains the client pointers that must be converted. In some instances there are also pointers in entities and clients that we don't want to alter when loading a saved game. These are called ignore fields and the load routines make sure these pointers are not changed during loading. The structures gentityIgnoreFields and gclientIgnoreFields contain these pointers for entities and clients, respectively.

Save Game Format

The format of the saved game is as follows :

Data
Size of Entry
Save Version
sizeof(int)
Map Name
MAX_QPATH characters
Level Time
sizeof(int)
Skill Level
sizeof(int)
Size of Entity Structure
sizeof(int)
Entity Number
sizeof(int)
Entity Data
see below for details
...
...
-1
sizeof(int)
Size of Client Structure
sizeof(int)
Client Number
sizeof(int)
Client Data
see below for details
...
...
-1
sizeof(int)

Each entity in the level is saved as the Entity Number and the Entity Data, with a -1 following the last entity's data. The format of the entity data is as follows :

Entity Data
Size of Entry
'GENT'
4
Block Size
sizeof(int)
Data Block
Value of Block Size
'STRG'
4
String Data
length of string
'STRG'
4
String Data
length of string
...
...

Each client in the level is saved as the Client Number and the Client Data, with a -1 following the last client's data. The format of the client data is as follows :

Client Data
Size of Entry
'CLEN'
4
Block Size
sizeof(int)
Data Block
Value of Block Size
'STRG'
4
String Data
length of string
'STRG'
4
String Data
length of string
...
...

Saving a Game

The G_SaveGame function in g_savestate.c is used to save the current game using the provided name. To allow saving games from the console as well as from the UI we will make this routine accessable as a Client Console command called savegame. It's format will be :

savegame <file name>

where <file name> is the name of the saved game file without any extension. The source code changes are as follows :

In g_cmds.c just before the ClientCommand function add :

#ifdef SINGLEPLAYER
void Cmd_Save_f( gentity_t *ent ) {
 char buffer[MAX_TOKEN_CHARS];
 if ( trap_Argc() < 2 ) {
   if( !G_SaveGame("autosave") ) 
   {
     G_Printf("Could not save.\n");
   }
 }
 else
 {
   trap_Argv( 1, buffer, sizeof( buffer ) );
   if( !G_SaveGame(buffer) ) 
   {
     G_Printf("Could not save.\n");
   }
 }
}
#endif

In g_cmds.c in the ClientCommand function about line 1718 add :

	else if (Q_stricmp (cmd, "stats") == 0)
Cmd_Stats_f( ent );
#ifdef SINGLEPLAYER
else if (Q_stricmp (cmd, "savegame") == 0)
Cmd_Save_f( ent );
#endif

else
trap_SendServerCommand( clientNum, va("print \"unknown cmd %s\n\"", cmd ) );

Loading a Game

The G_LoadGame function in g_savestate.c is used to load a saved game whose name is specified by the ui_LoadGame cvar. The Save_Loading cvar must be set to 1 to indicate a load is desired. These cvars are set in the game loading portion of the UI as described in the Interfacing to the UI section. The saved game is loaded after the level is initialized and the player has connected to the server. This is implemented as follows :

In g_main.c in function G_RunFrame about line 1716 add :

void G_RunFrame( int levelTime ) {
int i;
gentity_t *ent;
int msec;
#ifdef SINGLEPLAYER
gclient_t *cl;
int save_loading;
#endif

In g_main.c in function G_RunFrame about line 1737 add :

	G_UpdateCvars();
#ifdef SINGLEPLAYER
save_loading = trap_Cvar_VariableIntegerValue( "Save_Loading" );
if(save_loading == 1) // Load a level from save game
{
char load_game[100];
trap_Cvar_VariableStringBuffer( "ui_LoadGame", load_game, sizeof(load_game) );
if (load_game[0])
{
for (i=0 ; i<MAX_CLIENTS ; i++)
{
cl = &level.clients[i];
if (cl->pers.connected == CON_CONNECTED)
{
G_LoadGame(load_game);
trap_Cvar_Set("Save_Loading","0");
break;
}
}
}
else
{
trap_Cvar_Set("Save_Loading","0");
}
}
#endif

//
// go through all allocated objects
//
start = trap_Milliseconds();
ent = &g_entities[0];

 

Return to Home Page