Logo Pending


Save early, save… how?

This topic was suggested by Phil Fortier.

I can’t speak for the engines that came before, but saving and restoring a game in SCI11 is remarkably straightforward. The whole thing’s basically a dump of heap memory with some extra bits.

The hell is heap memory you might ask. Well, there’s two kinds of memory available to the actual game, provided by the engine, and those are heap and hunk. Every script whose objects or classes are used at a given time is loaded into heap space, most every other kind of resource goes into hunk space. In SCI11 specifically, scripts are split up into two parts, one with the actual code that goes in hunk space, and one with object definitions, local variables, and stuff like inline strings that goes on the heap. Correct me if I’m wrong on that one, Phil, cos I’m a little fuzzy on the details. (Update: I was.)

Save

When you save the game, the size of the heap and variables are written first, followed by the game’s version string. This part is why you can’t mix and match saved games between different versions of the same game. Then, since events are objects and thus in heap space, all pending key presses are flushed out. The engine source code explains this is to prevent stuff like re-restoring a just-restored game by having an F7 press in the buffer. Then all the unused space in the heap is zeroed out.

There’s a good reason for that. It’s because when the actual contents of the heap are saved, as one big blob, it’s compressed. Imploded, to be precise, almost but not entirely unlike how you’d make a ZIP file. And if you zero out the bits you don’t even use, it’ll compress much tighter. So a whole bunch of variables that are all packed together at a known location and the heap space are imploded and saved.

Then the plubus and grumbo are shaved away. Next, the state for any PalVary effects is saved, also in an imploded form. That there is any state to save at all is one of those variables I mentioned earlier.

The saved game’s file is closed, and the catalog file is updated. That being the ___SG.DIR file, which simply lists all the saved games’ names. The actual saves are those numbered ones, ___SG.000 and on.

Fun fact: the heap is about 64 kilobytes in size. Compression all but eliminates the unused, zeroed out parts. That leaves you with relatively small save game files. For example, this Dating Pool save is only 6203 bytes. But my unfinished shit is hardly representative… one of my Police Quest 3 games is 22.9 kilobytes.

Restore

Restoring a game starts out basically like the exact opposite of saving, exploding the variables and heap blob. Since all other resources are in hunk space, these are all unloaded at this point. Yeah, not a typo, I’m talking about the pictures and sounds and such that were already there. The hunk is cleared so that the stuff from the saved game can go there, obviously.

Throughout their lifetime, object properties and local variable values can and will change, of course (I’ll mention real quick that global variables are just the locals for script zero, which is never disposed of). Since those things are part of the heap, object and variable state is restored free of charge — all you really need to do manually is to get the code back. Given a list of supposedly-loaded scripts that should go in hunk space, again one of those variables, the restore routine can now iterate that list and repopulate the hunk.

At this point, PalVary state is handled if needed, the video mode is switched between Mode 13h and Mode X if it was different, and the PMachine is restarted. It knows we just restored a game, so the freshly-restarted PMachine knows to call the replay method instead of play. From that point, given a heap-restored state, the game can redraw its background at the time and enter the same endless doit loop as it would at the end of play. Any sounds, pictures, or views on the heap by now will have their resources loaded to hunk space as they are used.

Restart

Oh, didn’t expect that one! There’s this tricky pair of commands you can use in C, setjmp and longjmp, that to quote Wikipedia “provide control flow that deviates from the usual subroutine call and return sequence.” Right before the PMachine is spun up at the end of engine initialization, a sort of snapshot is taken to right that moment, consisting of all important CPU registers, including the one that determines where the next command will be. The SaveGame and RestartGame kernel calls use this to jump back in time and enter an all new PMachine loop. But what does restarting do exactly?

First, it sets the restart flag and disables PalVary. Jeez, does that subsystem start sounding hacky to anyone else or is that just me? Then it unloads every resource and resets the heap to one big block marked “free”. I didn’t say it zeroed out that block, because it doesn’t. Anything marked free is zeroed out while saving.

And the first thing the PMachine routine does? Get a handle on script 0, export 0 — the main game class. The last thing is to invoke its play or replay method, and since either of these methods end in a doit loop that doesn’t stop until gQuit is true, that invocation won’t return until then.

If C++ didn’t give me a headache, I’d look into how SCI32 may or may not be different from this. I know it has a bigger heap space, considering the whopping 117 kilobytes for a single save game from Love For Sail, but it’s the methodology that matters, right?

I suppose I owe you an answer to that question about the twenty game limit, don’t I? Well, I have a catalog file here listing about twenty-four so let’s see w–

Like
[ ]

5 thoughts on “Save early, save… how?

  1. Yet another thing, if you haven’t looked at my save-dumping code yet, SCI3 saves are compressed, so they are really bigger than the 113KB you cite (I had one LSL7 savegame come out to 1.3MB uncompressed). The SCI3 object structures are crazy big. That’s one thing that really puzzles me about it. Extend the memory model so there’s space to… change the object layout unnecessarily?!?

    SCI2 is smaller (around 80k uncompressed), but as with saves in general, they grow as the game progresses.

  2. There’s one interesting tidbit missing here, which is how deletion (SCI1 and later) is implemented. Namely by manipulating the .DIR file in the script, and not – as any sane person would do – with a kernel call.

Leave a Reply

Your email address will not be published.