Well, this is the first in a series of how to guides that I plan to write regarding various issues that come up in OP2 DLL coding, that perhaps not many people are aware of.
If you have any comments, questions regarding this how to, or any other how to's you would like to request, let me know, either by PM, irc, or by just posting in this thread.
If you have questions / want clarification on anything in this how-to, post in the thread and I'll clarify it for you.
After I've given people a few weeks to ask questions and myself to answer them, I'll post this how-to up on the Wiki for reference.
---
How To: make colony games that load and save properlyA few people have come to me asking why custom colony / campaign games (especially those with AI or custom trigger routines) work fine when the game is played for the first time, but crash when the game is loaded from a saved game file.
Because of this, I felt it would be a good idea to write a how-to describing why this happens, and how to fix this problem.
When you click Save Game in OP2, it writes a bunch of different information into the save game file. Things such as the current state of the map, and a list of all the units are saved, as well as a bunch of other variables within the game engine.
Likewise, when the game is reloaded from the saved game file, all these variables are restored into the game engine. This causes a few changes in how the mission DLL runs, which I will get to in a minute.
When a game is started (not loaded from a saved game file), your mission DLL is loaded and the game calls your InitProc to give you a chance to create different mission related variables in the game. (Namely, units, triggers, victory conditions, the whole lot).
As an example:
int InitProc()
{
Unit scout;
TethysGame::CreateUnit(scout, mapScout, LOCATION(30+31, 30-1), 0, mapNone, 0);
// ...
Trigger timeTrig = CreateTimeTrigger(1, 1, 1000, "NoResponseToTrigger");
// ...
}
Sometimes some of these variables are used outside of InitProc, thus you declare them globally, which means outside of any functions, like so:
Trigger timeTrig;
FightGroup fighters;
MAP_RECT fighterRect(0+31, 0-1, 20+31, 20-1);
// ...
int InitProc()
{
// Here begins your InitProc code
// ...
}
An example of how a global variable like this might be used inside of a trigger routine:
SCRIPT_API void Attack1()
{
if (timeTrig.HasFired()) // if it's time, make the AI tougher
{
fighters.SetTargCount(mapLynx, mapRailGun, 5);
}
}
Assume that timeTrig and fighters are both global variables, and both were created using CreateTimeTrigger and CreateFightGroup inside of InitProc.
No problem, right?
The ProblemNow back to how OP2 loads a saved game into memory. When OP2 loads the saved game, it just loads the game state back into memory.
Because all the game data has already been created, it simply loads your DLL and starts running from where it left off. This means one important thing for you:
OP2 does not call InitProc when the game is loaded from a saved game file.That's right, InitProc is only called when the game is initially starting up, not when the game is being loaded from a saved game file.
This creates a problem for any mission which creates global variables in InitProc, and then uses them from trigger routines later.
When the trigger routine uses the global variable, it is essentially the same as using a trigger or a FightGroup which was never created using CreateXXXTrigger or CreateFightGroup -- almost always causing a game crash.
Thus, you must tell OP2 to save your global variables for you when the user saves the game, and then load them back into memory when the game is loaded. OP2 cannot do this automatically for you -- you must tell it which variables to save.
The SolutionThe solution is GetSaveRegions, that little piece of code that exists in all mission DLLs but that few people are aware of what it does.
GetSaveRegions is responsible for telling OP2 what global variables need to be saved when the game is saved, and restored when the game is loaded.
The default GetSaveRegions looks like this:
void __cdecl GetSaveRegions(struct BufferDesc &bufDesc)
{
bufDesc.bufferStart = 0;
bufDesc.length = 0;
}
The BufferDesc that is passed to GetSaveRegions is something your DLL fills in. It tells OP2 where your global variables are in your DLL, and how much space they take up (this way OP2 knows exactly what it needs to save).
The only trick left is to figure out what these are (the values of bufferStart and length).
Using a structOne trick is to create a C++ structure (or struct for short) which will be used to hold all of your global variables that need to be saved. Figuring out the bufferStart and length for this struct is straightforward.
Using such a struct is quite easy. It's probably easiest to explain using example code.
This first example is the 'regular' way of declaring global variables. Nothing fancy here:
Trigger timeTrig;
Trigger resTrig;
FightGroup fighters;
MiningGroup miners;
// ...
Now, placing these into a struct to use with GetSaveRegions requires few changes:
struct
{
Trigger timeTrig;
Trigger resTrig;
FightGroup fighters;
MiningGroup miners;
} saveData; // You can rename this from saveData to anything you want (be it a valid C++ variable name)
This creates a struct variable named saveData containing all of your global variables. The only caveat is, to access your global variables from within trigger routines or elsewhere, you will need to prefix the name with
saveData. (or whatever you named it). If you fail to do that, things won't work right.
So,
fighters.SetTargCount(mapLynx, mapRailGun, 5);
becomes
saveData.fighters.SetTargCount(mapLynx, mapRailGun, 5);
Not too bad.
Now the only thing left is to change the information inside of GetSaveRegions so it references this struct, like so:
void __cdecl GetSaveRegions(struct BufferDesc &bufDesc)
{
bufDesc.bufferStart = &saveData;
bufDesc.length = sizeof(saveData); // Again, if you renamed your struct, change saveData here
}
Note the & in front of saveData. This is important. (if you leave it out, a compile error will result). '&' is an operator in C++ that returns the memory location of the variable that comes after it (this is needed so OP2 knows where in memory the struct is located).
sizeof(saveData) just returns the size, in bytes, of the structure (this way OP2 knows how much memory is taken up by the global variables, so they can all be saved).
Now, compile your code, and test saving your game. When the game is reloaded, the data inside the struct will be restored, and it should work without a hitch.
Some other ConsiderationsYou might be wondering if you really have to save EVERY SINGLE global variable used in your mission. The answer is, not necessarily.
If you use things such as MAP_RECTs or LOCATIONs that are declared globally (with parameters in parentheses), such as:
LOCATION smelterLoc(45+31, 20-1);
MAP_RECT attackGroupLoc(20+31, 20-1, 50+31, 50-1);
you can skip having to put them into the struct if and
only if you do not modify their contents in any other routine.
So for example:
SCRIPT_API void MoveUnits()
{
attackGroupLoc.x1 = 50+31; // You need to put this into saveData since it has been modified
// ...
}
The reason: LOCATIONs, MAP_RECTs, and similar types of things which are initialized globally with parameters (like up above) are not handled by InitProc, but automatically by the C runtime library (hidden pieces of code which are bundled with your mission DLL when it is compiled). These are initialized automatically when the DLL is loaded into memory (which happens regardless of whether the game calls InitProc or not).
If you modify the contents of one of these variables, you will need to save them by moving them to saveData, since the changes will need to be accounted for (saved and restored).
However, to avoid problems, it is probably easiest to just add each and every global variable you use to saveData, rather than trying to decide which ones need to get saved and which ones don't.
Another consideration is that IUnit and other Op2extra variables may need to be saved as regular Unit variables (it may introduce problems when trying to save the extended IUnit). However, I haven't verified this with Eddy-B (the writer of the Op2extra library).
The same may apply to other 'extensions' of standard OP2 objects.
Multiplayer, tutorial, etcNone of this applies or is needed for multiplayer, tutorial, or autodemo missions (since it is impossible to save the game in these modes). You only need to take the above into consideration if you are developing a colony or campaign mission.
ConclusionI hope that this how to guide is useful for those of you interested in developing single player games.
If you have any questions, comments, or need clarification on anything presented here, let me know by posting a reply.
Special thanks to Mcshay and Sirbomber for asking about why colony games crash when they are loaded, which was the reason I wrote this guide in the first place.