Saving and Restoring

From The DarkMod Wiki
Jump to navigationJump to search

Originally written by Ishtvan on http://forums.thedarkmod.com/topic/1586

WARNING: This is a first draft, complete with spelling and grammatical errors. Also, I just got this functionality working myself. So there may well be some unknown issues with this code that have yet to pop up.

Saving and Restoring Non-Spawned Classes

NOTE: This tutorial only applies to non-spawned objects. You only have to do all of this if you are NOT deriving your class from idEntity, i.e., you do not want your class to be spawned in the map. If you're deriving from idEntity, your object will automatically be saved and restored (although you still might have to save specialized data by writing Save and Restore methods)

Let's suppose you create a new class, and have a global object for it that you want to save and restore the state of, but you do NOT want to actually spawn this object as an entity in the map (this would require the mappers to place a lot of weird invisible entities on every map, just to store specialized data).

If you take a look at src/game/game_local.cpp, you will see 2 relevant methods: idGameLocal::SaveGame and idGameLocal::InitFromSave . Within these, a bunch of calls are made to the savegame and restore game to save and restore objects. So how do you put your object in there? I will try to explain that in the following tutorial.

Step 1. Add Your Class to the Id Class Heirarchy

If your new class is not already derived from an Id class, you must add it to the Id class heirarchy by inheriting from idClass and call the proper macros to let the Id class heirarchy know about your class.

NOTE: Be aware that adding your class to the Id class heirarchy may cause some methods to be called automatically in certain situations, if they have a certain name (such as Init, Shutdown). Therefore if you don't want your method called automatically, you should name it something else. See /src/game/gamesys/class.h for a complete list of methods on idClass that may be called automatically.

Step 1A

Put the macro CLASS_PROTOTYPE( <your class name> ) within your class declaration in the header file.

Example:

In myclass.h

class myClass : public idClass {

CLASS_PROTOTYPE( myClass );

public:
// [...]

Step 1B

Put the macro: CLASS_DECLARATION in your implementation file. In this case we will NOT add any scripting events to the class, because, from the start, we do not intend to ever spawn this class into an entity, so it does not make sense to define scripts on it, as scripts are primarily run from entities.

The syntax is CLASS_DECLARATION( <base class>, <your derived class> )

Example:

In myclass.cpp:

CLASS_DECLARATION( idClass, myClass )
END_CLASS

Step 2. Write Your Save Method

Write the method Save( idSaveFile *save ) const for your class. This will be called automatically by Id during the save process. Save and Restore methods typically utilize idSaveFile methods to read and write relevant data. Look at src/game/gamesys/savegame.h for an explanation of idSaveFile methods.

Example:

void myClass::Save( idSaveGame *saveGame ) const
{
    // write an integer member variable to the save file
    saveGame->WriteInt( m_intVar );

    return;
}

Step 3. Write Your Restore Method

A restore method reads relevant data from the savefile.

Key Point: (Thanks to Sparhawk for figuring this out) Since your object is not going to be spawned, it will not be copied to the entities list after being restored. When restored, your object is not automatically copied ANYWHERE, and only exists in the scope of idGameLocal::InitFromSave. When InitFromSave is finished loading everything, and the game exits from this method (which it does before starting the map), your object will go out of scope and WILL BE DESTROYED, unless you copy it somewhere else yourself.

In order to avoid destruction when idGameLocal::InitFromSave exits, your restore method must copy the instance of your class somewhere, in addition to loading the data from the savefile. It could copy it to a global variable, static member variable in the class, whatever.

Example:

Assuming that elsewhere you have declared a global, and that you also have a working copy constructor for your class, etc.:

Somewhere else with global scope:

myClass g_globalMyClass;

In myClass.cpp

void myClass::Restore( idRestoreGame *SaveGame )
{
    // read in your data, in this case an integer member var
    SaveGame->ReadInt( m_intVar );

    // copy this instance somewhere else
    // In this case we assume that a global of type myClass has been declared
    // elsewhere.  It could also be a static member variable of type myClass.
     
    g_globalMyClass = *this;

    return;
}

Step 4. Add your object to the SaveGame method in idGameLocal

idSaveGame::AddObject adds any object that's been derived from idClass to a savefile, and registers the object within the savefile so that it can be restored easily later.

We want to add our object in when everything else is saved, using idSaveGame::AddObject. As far as I can tell, you can put it in anywhere in this method, as long as it's before the call to savegame.WriteObjectList(), since your object must be added before the object list is written to the savefile.

To be safe, I put it in right before savegame.WriteObjectList().

Example:

Assuming you have declared myClass myObject elsewhere, and are using it to store data.

In idGameLocal::SaveGame

// [...]

    // Add your object
    savegame.AddObject( &myObject );

    // write out complete object list
    savegame.WriteObjectList();

// [...]

The save method for the instance of your class in g_globalMyClass will now be called automatically now when the game is saved, and the savefile will know about your object when it comes time to restore.

NOTE: AddObject takes a reference to your object. If you want to use forward-declaration of your myClass object in game_local.cpp, just call AddObject with the reference to your object.

Step 5. You're Done!

The good news is, in game_local::InitFromSave, your object will now be restored automatically, when idRestoreGame::RestoreObjects is called, which goes thru all the objects that were added to the savefile and calls the Restore method on them.

Remember to have your restore method copy your object somewhere else, since your object is not a spawnable entity and it will otherwise go out of scope and be destroyed when idGameLocal::InitFromSave exits.

Addendum: Saving Entities

Automatic Stuff

Just to emphasize again, the above tutorial is for saving something that's not necessarily an entity. If you derive from idEntity, your object will automatically be saved in idGameLocal::SaveGame. In InitFromSave, it will be automatically restored, and (I think) copied to the entities[] array.

(although I thought gameLocal.entities[] was an array of entity pointers, not actual entities, so it may be copied somewhere else... in any cases, it's copied to the same array where all the spawned entities are stored)

All you have to do is put your entity on the map, and all of this happens automatically.

Must write Save/Restore methods in your class if you want to Save/Restore data that's not in the derived class

However, if you do not specify a Save method, it will call the one on the base class/superclass of your entity. So if you have a new data member in your class that was not in the base class, and you want it saved and restored, you MUST write your own myClass::Save and myClass::Restore methods, as described above (but don't worry about the copying over for the Restore method).

I think that idSaveGame::SaveObject (the method that's called to save your object) will keep calling Save all the way up the inheritence chain, so I don't believe you need to inherit the Save method of the base class in your new class' Save method. I'm not positive about this though.

If you want an example of this, the code would be almost the same as the Save and Restore methods in the first post, assuming your new data is some member variable m_intVar. In this case, since we derived from idEntity, you only need to call saveGame->ReadInt in myClass::Restore. You do NOT need to copy your object somewhere as was done above.

Note: Spawnargs are already saved by default when an entity is saved, so if the only new data stored on your new entity is stored in spawnargs, you don't need to write Save/Restore methods.

Addendum 2

Sweet, TinMan over at D3World found another way to restore the object by adding Savefile->ReadObject in the "Init from Savegame" section of game_local.cpp. ReadObject takes a pointer, so I thought it would just turn into a bad pointer when InitFromSave exited, but apparently it doesn't. So you can use ReadObject to copy your object, instead of copying it in yourObject::Restore.

Of course this means you have to maintain your objects saving/restoring in both idGameLocal::SaveGame and idGameLocal::InitFromSave .. with my method you only need to add the object in SaveGame and then it's automatically restored in InitFromSave. Anyway, here is his method copied from D3World:

Follow the tutorial for the most part. Set up your class as a global pointer to an instance of your class, create new instance in idGameLocal::Init. Don't copy out to global in your class ::Restore Clear your object in idGameLocal::MapShutdown

In idGameLocal::SaveGame near the end

// [...]
 // TinMan: Save object index to this spot in file
 savegame.WriteObject( myObject );

 // write out pending events
 idEvent::Save( &savegame );
    
 savegame.Close();
}

In idGameLocal::InitFromSaveGame near the end

// [...]
 // TinMan: Overwrite your global pointer with the restored instance.
 savegame.ReadObject( reinterpret_cast<idClass *&>( overGame ) );

 // Read out pending events
 idEvent::Restore( &savegame );

 savegame.RestoreObjects();

// [...]

Important Note: For any idSaveGame::Write function you must have a corresponding idSaveGame::Read function in the same order since the savefile is being operated on in a stream.

Further Notes:

  • idSaveGame::Close
  • Calls ::Save on all objects in it's list (you know the one you added to via AddObject).
  • idRestoreGame::RestoreObjects
  • Yep you guessed it, calls ::Restore on all objects.

Further Notes potentially making your Life easier

written by greebo

When traversing the SDK to make the Saving/Loading routines working again with our Mod code, I noticed a few things. I better write them here to prevent other people from running into the same situation.

Data Types that can be saved/restored

There is a limited number of data types that can be serialised and written into the savefile. It's always best to look into the idSaveGame declaration to see which types can be written to the savefile. Amongst them are the atomic types like int, unsigned int, string, bool as well as some often used structures like idVec3, trace_t, joint_t and so on. Note that raw pointers and C++ references are NOT amonst them (you can't expect the addresses to be the same when the game is restored from file again). If you're using custom structures, it's probably best to define Save/Restore methods on them which in turn call the idSaveGame::writeInt(),writeBool(),writeString(),.. methods.

Call Order

Another important thing to remember is the call order in the Save/Restore methods. As an example, suppose you want to save an int, a string and a bool to your savegame:

void myClass::Save(idSaveGame* savefile)
{
  savefile->WriteInt(myInt);
  savefile->WriteString(myStr);
  savefile->WriteBool(myBool);
}

The important thing is to exactly match the call order of your Restore function:

void myClass::Restore(idRestoreGame* savefile)
{
  savefile->ReadInt(myInt);
  savefile->ReadString(myStr);
  savefile->ReadBool(myBool);
}

The savefile can be considered as a stream of data. All the classes are saved/restored in the exact same order, otherwise the data gets restored to the wrong class members.

Should I save the base class?

If your class is deriving from idEntity you don't need to call the base class in your Save/Restore routines, this is done by some automatic home-grown routine by id software (the same applies to Spawn() btw.). Technically, it doesn't hurt to include a call to the base class, but then the data will get written multiple times to the savegame file. So all you need to do is implement the Save/Restore methods on your class and it will get called.

Hint: If you're unsure, try to set a breakpoint in your Save/Restore routine to check whether and how often your routine is invoked.

If your class is a custom one (like the classes used in the AI framework, like ai::Task or ai::State), you must call the base class, like this:

void MySubclass::Save(idSavegame* savefile) const
{
   // call the base class first
   MyBaseClass::Save(savefile);
   
   // now save your local members of the subclass
}

Saving Raw Pointers

First rule is: whereever possible, avoid to use raw pointers (except for the example Ishtvan wrote about, using the WriteObject()/ReadObject() method). Pointers can't be saved/restored just like that. Supposed you have some kind of graph which is interconnected by raw pointers and you're in the unlucky situation to save this structure, you can follow some of the general guidelines of graph serialisation: http://www.parashift.com/c++-faq-lite/serialization.html

The idEntityPtr<> template

There is a templated container call idEntityPtr: this object wraps around an idEntity* pointer and can be saved/restored just fine. It provides an assignment operator taking idEntity* pointers, which looks up the entity index in the gameLocal.entities[] array. Only this index is saved into the structure (and the savefile). In case you need to access the entity, you call the idEntityPtr::getEntity() method, see the example below.

class idAI
{
   // This wraps around a idEntity* pointer
   idEntityPtr<idEntity> myEnemy;

public:

   void setEnemy(idEntity* newEnemy)
   {
     // Use the assignment operator to store the <newEnemy> pointer   
     myEnemy = newEnemy;
     // Only the entity index is stored into the idEntityPtr, it is resolved
     // using the gameLocal.entities[] array on demand.
   }

   idEntity* getEnemy()
   {
     // Now call getEntity() to convert the entity index into a valid pointer
     return myEnemy.getEntity();
   }
};

The container is a template so you can use this container to store pointers to subclasses of idEntity too:

idEntityPtr<idAI> myAI;

As mentioned above, the idEntityPtr provides a Save/Restore method, so it can be conveniently used. Behind the scenes, only the entity index gets written to the savefile, as you might expect.

idEntity* vs. idEntityPtr<idEntity>

When choosing between raw entity pointers idEntity* and the idEntityPtr<idEntity>, consider the following:

  • idEntity*
    • raw pointer, is fast when accessing the entity
    • can be saved via ReadObject/WriteObject
    • can be broken when the entity is removed from the game (you won't know until your code crashes the game when trying to access it)
  • idEntityPtr<idEntity>
    • stores the spawnid internally
    • provides its own Save/Restore method
    • requires a GetEntity() resolution call before it can be used to access the underlying entity.
    • each GetEntity() call implicitly checks whether the entity is actually existing, returns NULL if this fails.

So basically, the idEntityPtr<idEntity> construct lets you know when the entity has been removed and your code can deal with it, which comes with the cost of the GetEntity() call each time you need to access the contained pointer.

Dereferencing idEntityPtrs during Restore

As mentioned above, the idEntityPtr structure doesn't store an actual idEntity* raw pointer, but uses an integer to do so (which is looked up in the entity index in gameLocal.entities). This integer is resolved as soon as the client calls getEntity(). It is important that the gameLocal.entities array is initialised by the time the getEntity() method is called, which can't be guaranteed when the restore routine is still running. The order the entities are restored from the savegame is arbitrary and you therefore can't rely on entity X to be restored and available before entity Y. If you really need to access other entities to fully restore your class, you must work around that by using the Event system. Just post an event with 0 milliseconds delay time which restores the information. By the time the event is fired, all the entities are restored and it's safe to dereference the idEntityPtr. Examples can be found by searching for PostEventMS in the project:

 PostEventMS(&EV_SetOwnerFromSpawnArgs, 0); // Call the event with 0 msecs delay

Saving References

As C++ references can't be changed after class construction, so this is an absolute no-no for use in the SDK. Unless you start to use some tricks with reference encapsulation classes, you're lost here. Change your reference to a compatible structure like idEntityPtr or change your class.

Saving idLists

I thought it would be handy to have a prototype save/restore routine for idList in this article, so here it goes. Suppose we have a list of strings like this:

idList<idStr> myStringList;

The saving routine is pretty straightforward:

// Important: save the number of items into the list before the actual list members
savefile->WriteInt(myStringList.Num());

// Now write all the list members to the file
for (int i = 0; i < myStringList.Num(); i++)
{
  savefile->WriteString(myStringList[i]);
}

The restore method reads the integer and resizes the list accordingly. Then the members are restored one by one:

// Read the number, we need a local object to pass a reference
int num; 
savefile->ReadInt(num);
 
// Resize the list
myStringList.SetNum(num);

for (int i = 0; i < num; i++)
{
  savefile->ReadString(myStringList[i]);
}

The important thing is to resize the list before starting the for loop, the myStringList[i] array access must not crash.