m_bInBurstMode
boolean member to the CFAMAS
class (or the concerned weapon) and check whetever it's "false" (full-auto) or "true" (burst).m_bInBurstMode
was never saved in the first place and the default value for booleans is "false".hldll
project), the client does not perform any kind of save/restore.
CBaseEntity
can declare a static TYPEDESCRIPTION
array which is usually called m_SaveData
(the C/C++ translation: static TYPEDESCRIPTION m_SaveData[]
).TYPEDESCRIPTION
which is a structure (struct
):
typedef struct
{
FIELDTYPE fieldType;
char *fieldName;
int fieldOffset;
short fieldSize;
short flags;
} TYPEDESCRIPTION;
FIELDTYPE
is an enumeration of variable types which we will detail below. fieldType
is where you specify what kind the variable to save/restore is.fieldName
is the name of the field.fieldOffset
is the position of the field based on it's class (fieldType
).fieldSize
is the amount of times that the field need to be read.flags
allow to indicate something "special" about the field.#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.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).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.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 across several levels (see Half-Life's "On A Rail" chapter).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. |
engine/eiface.h
.
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 ),
};
If you get compile errors, make sure saverestore.h
is part of the includes (and double check for syntax fixes and such).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.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).CBaseEntity
, CBaseMonster
, CBasePlayerWeapon
and such, it only know one thing: entity dictionaries (edict_t
).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:
entvars_t
.bool Restore( CRestore &restore )
method.ObjectCaps()
has the FCAP_MUST_SPAWN
bit flag set, call the entity's Spawn()
method or else, just call it's Precache()
method instead.void DispatchSave( edict_t *pent, SAVERESTOREDATA *pSaveData )
:
ObjectCaps()
has the FCAP_DONT_SAVE
bit flag set, don't go any further.bool Save( CSave &save )
method.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.bool Save( CSave &save )
and bool Restore( CRestore &restore )
methods.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.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!
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.// 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
bool status = restore.ReadFields( "CDerivedClass", this, m_SaveData, ARRAYSIZE( m_SaveData ) );
// Now we can do whatever we wanted here
return status;
}
Existing examples of custom implementations are the player (CBasePlayer
) and monsters (CBaseMonster
).
LINK_ENTITY_TO_CLASS
). Half-Payne does this for it's gameplay mods class which is saved/restored alongside the "global state" (void SaveGlobalState( SAVERESTOREDATA *pSaveData )
and void RestoreGlobalState( SAVERESTOREDATA *pSaveData )
).SAVERESTOREDATA
structure as well as the CSave
, CRestore
and CSaveRestoreBuffer
classes are good starting points.You must log in to post a comment. You can login or register a new account.