Entity Programming - Save/restore Last edited 7 months ago2023-09-04 16:36:14 UTC

Half-Life Programming

Contrary to "multiplayer only mods" like Counter-Strike, Day of Defeat or Ricochet, any mod that has singleplayer capability like Half-Life: Opposing Force, Wanted! or even Half-Life itself must support saving and loading saved games. This process is also called "save/restore".

There will likely be situations where you will need to program your custom entities regardless if it's a weapon, a monster, a trigger or something else. Or you will be adding/modifying data to the exisitng ones. For example: a FAMAS similar to Counter-Strike (or an existing Half-Life firearm) that can fire full-auto or burst.

To program the fire selector, you need to keep track of the "are we in full-auto or burst mode" information. To do that, most programmers will add a m_bInBurstMode boolean member to the CFAMAS class (or the concerned weapon) and check whetever it's "false" (full-auto) or "true" (burst).

But by default, Half-Life won't persist that variable in saved games. So if you put the FAMAS (or again, the concerned weapon) on burst, save the game, quit it, restart it, load the save, then the FAMAS will be in full-auto instead of being in burst mode because m_bInBurstMode was never saved in the first place and the default value for booleans is "false".

Not only saved games are concerned, but it could also impact level transitions. Keep in mind that save/restore is a process that happens between the engine and server (hldll project), the client does not perform any kind of save/restore.

The save/restore table

Each class that is a child of CBaseEntity can declare a static TYPEDESCRIPTION array which is usually called m_SaveData (the C/C++ translation: static TYPEDESCRIPTION m_SaveData[]).

Let's take a look at TYPEDESCRIPTION which is a structure (struct):
typedef struct
{
    FIELDTYPE fieldType;
    char *fieldName;
    int fieldOffset;
    short fieldSize;
    short flags;
} TYPEDESCRIPTION;
To simplify things, Valve provides some handy macros:
#define _FIELD( type, name, fieldtype, count, flags ) { fieldtype, #name, offsetof( type, name ), count, flags }

#define DEFINE_FIELD( type, name, fieldtype ) _FIELD( type, name, fieldtype, 1, 0 )
#define DEFINE_ARRAY( type, name, fieldtype, count ) _FIELD( type, name, fieldtype, count, 0 )

#define DEFINE_ENTITY_FIELD( name, fieldtype ) _FIELD( entvars_t, name, fieldtype, 1, 0 )
#define DEFINE_ENTITY_GLOBAL_FIELD( name, fieldtype ) _FIELD( entvars_t, name, fieldtype, 1, FTYPEDESC_GLOBAL )
#define DEFINE_GLOBAL_FIELD( type, name, fieldtype ) _FIELD( type, name, fieldtype, 1, FTYPEDESC_GLOBAL )
The first macro can be considered the "root" one because every other macro calls it. It automatically calculate fieldSize based on the result of offsetof( type, name ) so that we don't have to do it ourselves. You will likely never use this directly and use the others instead.

The DEFINE_FIELD macro is the one you will see and be using a lot, it already takes care of the size (just one) and set no special flags. The only remaining information to set is the type, name and field type. Another macro you will likely see and use is DEFINE_ARRAY. This is basically DEFINE_FIELD except that the size is not set to one automatically and it's up to you pass the size of the array being treated (usually a constant).

The last three macros are the ones you are going to see rarely and probably never use by yourself but they are worth mentioning.

DEFINE_ENTITY_FIELD is used to save data from the entvars_t structure instead of a class. There is a DEFINE_ENTITY_GLOBAL_FIELD counterpart that add the FTYPEEDESC_GLOBAL flag for everything "global" (as in "entity global" like env_global, not in the C/C++/programming sense) related.

And finally, DEFINE_GLOBAL_FIELD is a "global" version of DEFINE_FIELD. So far, the only entity to use it is func_trackchange since the train (func_train) and paths (path_corner and path_track) need to be tracked accross several levels (see Half-Life's "On A Rail" chapter).

Let's go back to the FIELDTYPE enumeration, here are the values with the appropriate type/kind, the ones with an asterisk ("*") are the ones you will often see and use:
Type Description
FIELD_FLOAT* A floating point value (0.2, 75.5) that is not related to game time (use FIELD_TIME for that purpose)
FIELD_STRING* A string ID (string_t - often the result of ALLOC_STRING)
FIELD_ENTITY An entity offset (EOFFSET)
FIELD_CLASSPTR* An entity class pointer (like CBaseEntity *)
FIELD_EHANDLE* An entity handle (EHANDLE)
FIELD_EVARS An entity variables pointer (EVARS *)
FIELD_EDICT An entity dictionary pointer (edict_t *)
FIELD_VECTOR* A 3D vector (Vector, vec3_t, array of 3 float)
FIELD_POSITION_VECTOR A world coordinate (fixed up across level transitions "automagically")
FIELD_POINTER Arbitrary data pointer (scheduled to be removed by Valve but seems that isn't the case)
FIELD_INTEGER* An integer (5, 3) or enum value.
FIELD_FUNCTION A class function pointer (like Think, Touch, Use...)
FIELD_BOOLEAN* A boolean value, please read warning below because this one is "environment specific"
FIELD_SHORT A 2 byte integer value.
FIELD_CHARACTER A single byte value.
FIELD_TIME* Same as FIELD_FLOAT but for variables related to game time, usually a floating point value that has a relation to the game time (gpGlobals->time)
FIELD_MODELNAME An engine string that is a model's name (requires precaching).
FIELD_SOUNDNAME Same as previous one but for sounds.
Note: all code snippets above are from engine/eiface.h.
Important note about booleans
Depending on the Half-Life SDK you are using, the type and thus the size of boolean variables to use is not the same.

If you are not aware already, GoldSrc (the engine that powers Half-Life and its mods) is a heavily modified version of Quake's engine. The latter was not programmed in C++ but in C so the internal bool type could not be used.

A "trick" that was used to have some kind of "boolean" in C was to typedef int and give it an explicit name. Quake used the name qboolean that Half-Life inherited, but it did the same thing as Windows API with BOOL. That's also how the #define FALSE 0 and #define TRUE 1 macros were born and this is how C programmers made the difference between real integers with int and "booleans".

While this fixed the "code readability" problem, there is another one: in C++, the size of bool is one bit (sizeof( bool ) == 1). However, the size of BOOL is 4 bits (sizeof( BOOL ) == 4) due to the typedef int. Saving/restoring a 1 bit variable in something that expect 4 bits is going to cause corruption, inconsistent saving/restoring and even worse: crashes. The same goes for the reverse (saving 4 bits in 1 bit).

Some Half-Life SDKs like Half-Life: Updated which is the one this book told you to use in the introduction have changed the internal save/restore code so that FIELD_BOOLEAN expects 1 bit and thus the C++ bool must be used.

However, if you are using a standard Half-Life SDK or another SDK which has not performed such change, FIELD_BOOLEAN will expect 4 bits and thus you must use the "C boolean" BOOL instead.

If you are not sure if you should use bool or BOOL, simply check how an existing boolean variable from an existing entity is defined. You can take a look at how m_fLongJump is declared in the CBasePlayer class (dlls/player.h) for example.
Now that you know everything about the save/restore table, it's time to define it. If we take again our FAMAS with burst mode example, this would be:
TYPEDESCRIPTION CFAMAS::m_SaveData[] =
{
    DEFINE_FIELD( CFAMAS, m_bInBurstMode, FIELD_BOOLEAN ),
};
Do I need to re-define the parent data as well? Using the FAMAS as example, do I need to re-define things like magazine's size, attack delay and such?
Short answer: no, unless a parent "break" the hierarchy, but that never happens unless it's an intended behavior (or accident) by the mod's programmer.

Detailed answer in the the next section.
If you get compile errors, make sure saverestore.h is part of the includes (and double check for syntax fixes and such).

And that's it, the save/restore table for the FAMAS is complete. Depending on the burst mechanic, an int member variable usually named m_iBurstShotsLeft is made to keep track of how many shots it must make during a burst (3 --> 2 --> 1 --> 0 or straight to 0 if empty or underwater), that could be added as a FIELD_INTEGER to preserve the "burst" if the game is saved and restored in the middle of it.

Keep in mind that not everything need to be saved/restored. Things like "unlockables progress", "number of secrets uncovered", "weapons attachments like silencer" will be a "yes", but there are other things that will be "no" because it's either minor or it will be re-calculated at some point (AI/monsters navigation is one of them).

Feel free to look at the save/restore tables of existing entities for more examples.

The actual saving and restoring

If you were to compile the code right now and test the burst FAMAS situation mentioned in the introduction, you would notice that it doesn't work and you might have done something wrong.

Relax, you have done nothing wrong. The save/restore table defines what needs to be saved/restored but the actual saving and restoring happens elsewhere.

Let's peek "under the hood" to see how it works. Loading and saving games are actually console commands (load and save respectively), you won't find them in the server (hldll) project because those are defined in the engine which is closed source (unless Xash but that's out of scope for this book).

As already mentioned in the overview page of this chapter, the engine does not know CBaseEntity, CBaseMonster, CBasePlayerWeapon and such, it only know one thing: entity dictionaries (edict_t).

So when loading a saved game and after doing some things internally, there is an internal loop in the engine on all entity dictionaries and it calls the int DispatchRestore( edict_t *pent, SAVERESTOREDATA *pSaveData, int globalEntity ) method which is defined on the server project. For the sake of simplicity, we will not browse the entire code but we will note the important parts of it: The bolded step is very important for us later so keep it in mind for now.

Let's do the same exercise but for saving a game, same logic but with a different method: void DispatchSave( edict_t *pent, SAVERESTOREDATA *pSaveData ): Note: as an "addendum" to the warning in the previous section about boolean values, you might see in older/default Half-Life SDKs the "Save" and "Restore" methods returning int instead of bool. The logic stay the same it's just the return value being different. This book will assume bool for the rest.

With the knowledge we have so far, we can determine that in addition of the save/restore table from the previous section, we need to override/implement those bool Save( CSave &save ) and bool Restore( CRestore &restore ) methods.

You know what time it is? It's declaration time! Here's an example with our FAMAS (if you get compile errors, make sure saverestore.h is included):
class CFAMAS : public CBasePlayerWeapon
{
public:
    // NOTE: if you plan on making childs of CFAMAS and have them save/restore their custom stuff,
    // then remove "override" and add "virtual" at the beginning instead.
    bool CSave( CSave &save ) override;
    bool CRestore( CRestore &restore ) override;
    static TYPEDESCRIPTION m_SaveData[];
};
As for the implementations, Valve also provided a macro to make things easier and you might have seen this one already:
#define IMPLEMENT_SAVERESTORE( derivedClass, baseClass ) \
    bool derivedClass::Save( CSave &save ) \
    { \
        if ( !baseClass::Save( save ) ) \
            return false; \
        return save.WriteFields( #derivedClass, this, m_SaveData, ARRAYSIZE( m_SaveData ) ); \
    } \
    bool derivedClass::Restore( CRestore &restore ) \
    { \
        if ( !baseClass::Restore( restore ) ) \
            return false; \
        return restore.ReadFields( #derivedClass, this, m_SaveData, ARRAYSIZE( m_SaveData ) ); \
    }
Sounds familiar? Yes, it's the extra line that you will usually find just below the save/restore table. You will notice that it expects two parameters, the "derived" (or current) class and the "base" (or parent) class. This also explains you don't need to copy/paste the base/parent's class fields into the derived/childs ones, the hierarchy is saved thanks to the joy of object oriented programmed.

For our FAMAS, we can just add IMPLEMENT_SAVERESTORE( CFAMAS, CBasePlayerWeapon ); below the save/restore table. When compiling the code, the compiler will translate the macro to something like this:
// Code generated by the macro at compile time, do NOT copy/paste it!
bool CFAMAS::Save( CSave &save )
{
    if ( !CBasePlayerWeapon::Save( save ) )
        return false;

    return save.WriteFields( "CFAMAS", this, m_SaveData, ARRAYSIZE( m_SaveData ) );
}

bool CFAMAS::Restore( CRestore &restore )
{
    if ( !CBasePlayerWeapon::Restore( restore ) )
        return false;

    return restore.ReadFields( "CFAMAS", this, m_SaveData, ARRAYSIZE( m_SaveData ) );
}
And voilĂ , the save/restore for our FAMAS is now complete!

Running code before/after saving/restoring

In exceptional circumstances, you might need to run code before and/or after the save/restore process happens. In that case, you cannot use the IMPLEMENT_SAVERESTORE( derivedClass, baseClass ) macro from the previous section and you will have no choice but to provide custom implementations of the bool CSave( &save ) and bool CRestore( &restore ) methods.

A common case is restoring/reseting something when a game has been loaded. That case would be handled like this:
// This is the same code as the default implementation since we don't need to do anything specific when saving
bool CDerivedClass::Save( CSave &save )
{
    // Save parent data first, if it fails, we can't go any further
    if ( !CBaseClass::Save( save ) )
        return false;

    // Just save our data
    return save.WriteFields( "CDerivedClass", this, m_SaveData, ARRAYSIZE( m_SaveData ) );
}

// This is where a custom implementation comes in
bool CDerivedClass::Restore( CRestore &restore )
{
    // Restore parent data first, if it fails, we can't go any further
    if ( !CBaseClass::Restore( restore ) )
        return false;

    // Restore our data, if it fails we can't go any further
    bool status = restore.ReadFields( "CDerivedClass", this, m_SaveData, ARRAYSIZE( m_SaveData ) );
    if ( !status )
        return false;

    // Now we can do whatever we wanted here
    return status;
}
Existing examples of custom implementations are the player (CBasePlayer) and monsters (CBaseMonster).

Some notes

Comments

You must log in to post a comment. You can login or register a new account.