Save Game Detection

In an earlier coding entry I posted about various properties you could give QuakeC variables through naming them specially. Today we’re going to look at another property we can give a variable – the “nosave” property. As the name suggests, this variable will not be stored when the player saves a game. This is a keyword that old compilers don’t support, so grab FTEQCC if you want to code along at home. We’ll look at how save games are loaded by the engine, and what we can do with nosave variables.

In particular we’re going to look at the ideas in a func_msgboard discussion about cd tracks, and try to make them work across save games. We want to run the following function whenever the player loads a game, to restore the CD track stored in float cd_track_number;

void() LoadGame =
{
   if(!cd_track_number)
      return;
   
  WriteByte (MSG_ALL, SVC_CDTRACK);
  WriteByte (MSG_ALL, cd_track_number);
  WriteByte (MSG_ALL, cd_track_number);
}

Last Christmas we saw how save games are text files which list the entities and globals as they were at save time. To run the LoadGame function right after a saved game loads, we need to know exactly what the engine does with this file, and where the crucial differences with the normal start-up sequence lie. When a map begins afresh, the engine

  • loads all the entities from the map and runs a spawn function on each one
  • sets the server time to 0.1 seconds and runs a frame (without drawing it)
  • sets the server time to 0.2 seconds and runs another unseen frame
  • sets the server time to 0.5 seconds and beings running and drawing frames in real time

It turns out that lots of these steps are necessary when loading a saved game as well. For example, the spawn functions precache all the models and sounds the engine needs to run the map, and the static entities aren’t stored in the save file, so the engine has to run the spawn sequence to generate them. When a game is loaded the engine

  • loads all the entities from the map and runs a spawn function on each one
  • sets the server time to 0.1 seconds and runs a frame (without drawing it)
  • sets the server time to 0.2 seconds and runs another unseen frame
  • sets all the global variables to the values in the save file
  • updates all the entities to match the keys given in the save file
  • resumes running frames at the server time in the save file

When the server first runs frames at full speed we can distinguish between save games and fresh starts – the server time will be different in each case. It’s easier to work with the framecount than the actual time. Add a global nosave float sessionframecount; which we add 1 to in StartFrame right after framecount. For both new games and loaded games, the server starts full speed frames the third time that StartFrame is run. For saved games, the globals have just been restored at this moment, so framecount will be at some value greater than 2 when StartFrame begins. However, sessionframecount will always equal 2 at this point since it was not loaded from the save file. If it’s not a saved game, framecount will be equal to sessionframecount, so we have our test.

It’s actually a bit unnecessary to keep a counter running for an event we only need to detect once, so the following code rearranges the idea of the last paragraph to just use a flag: nosave float load_flag;. Add this code to StartFrame, after framecount is incremented.

if(!load_flag && framecount > 2)
{
   if(framecount != 3)
      LoadGame();
   load_flag = TRUE;
}

Obviously there’s loads of potential for things to do in LoadGame – Quoth for instance uses it to sort out the screen tints for the custom powerups, restore stuffcmds the mapper has applied, and warn you if you load an incompatible save from an earlier version of the mod. You could even do esoteric stuff, like re-rolling the random locations of treasure each time the player loads a save, so they can’t save-scum to always use their keys on the right chests.

However, it’s also worth noting that nosave isn’t a one-trick-pony that’s only good for enabling the LoadGame function. Another place it is vital is for properly using the CheckExtension system. The recommended way to check for QuakeC extensions is to set global floats as follows:

if (cvar("pr_checkextension")) {
   if (checkextension("DP_SV_SETCOLOR"))
      ext_setcolor = TRUE;
   ...

Then in the rest of the code, you switch codes paths based on the value of ext_setcolor. The crucial thing for today is that all these globals should be nosave! Otherwise, imagine you play a map in an engine which supports the extension, save a game, and then load that save in an engine which doesn’t have the extension. If the global is set to true in the save file, the mod will take the “extension supported” path on an engine which doesn’t support it – which is probably gonna crash the game. So to stand a chance, you need to make the global nosave.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.