Date: Mon, 9 Nov 2009 14:17:28 -0800
From: Stefan O'Rear <stefanor@cox.net>
To: crawl-ref-discuss@lists.sourceforge.net
Message-ID: <20091109221728.GA7668@localhost.localdomain>
Subject: [Crawl-ref-discuss] Savefile compatibility: how and why

Robert asked me to write something for the list on the subject of
savefile compatibility, so here goes.

1. WHY

Having a savefile compatibility procedure is crucial in the stable
branches, as it allows applying bug fix releases in mid-game.  It's
also important in the development trees, although we currently give
it far less attention than it deserves.  The reason for this is that
properly testing many features, such as the recent portal refactors
that happened to make leaving Zot impossible, require a long game.
If it is impossible to upgrade in the middle of a game, then all bugs
for late-game features will be reported against old versions, need-
lessly complicating the bug fixing process.

The situation is exacerbated by the public beta server (Marc Thoben's
crawl.develz.org), since it can only be updated site-wide; it cannot
be updated between games as there are almost always saved games on
the server, so it is simply updated (with loss of saves) rarely.  And
rarely is bad.

2. HOW

[note added by galehar]
First, if you're adding a property to one of the actor, player or monster
class, consider storing it in the props hash (declared in actor.h). Especially
if the property is temporary or not applied to all monsters. Also, it won't
create any save compatibility issues.

If you are changing any of the code in tags.cc (which implements load
and save for savefiles), you probably need to be concerned about save
compatibility.  Any code which uses the marshall* and unmarshall*
functions is, likewise, probably unsafe to simply change.  Changing
the values of existing enums (for instance, adding a new option
before existing options) is not generally safe, as saves store the
numeric values of enum variables.

By way of example, suppose you wanted to add a new field to the player
structure, you.xyzzy.  Now you want to save this field, and without
breaking saves.  To do this, find the functions tag_read_you and
tag_construct_you in tags.cc.  Unfortunately, you can't just add
marshall and unmarshall calls, since if you get an old save, the wrong
value will be unmarshalled, synchronization is lost and things
fall apart.  So first, add a new option to the tag_minor_version enum
in tag-version.h.  Make sure the new option is at the end, and that you
update TAG_MINOR_VERSION to correspond to the new option.  Now, any
savefile created by the new code, or any later savefile, will have a
minor version >= your new version number.  So, make the marshall and
unmarshall code conditional on minorVersion, like so:

    if (minorVersion >= TAG_MINOR_JIYVA)
        you.second_god_name = unmarshallString(th);

...

    marshallString(th, you.second_god_name);

Is this clear?  If you want to change the representation of an existing
field, or delete an old field, use a check in the other direction for
saves created by *older* versions of Crawl; for instance, take this
code, which reads in either a Subversion or a Git revision number,
depending on version:

    if (minorVersion <  TAG_MINOR_GITREV)
    {
        std::string rev_str = unmarshallString(th);
        int rev_int = unmarshallLong(th);

        UNUSED(rev_str);
        UNUSED(rev_int);
    }

    if (minorVersion >= TAG_MINOR_GITREV)
    {
        std::string rev_str = unmarshallString(th);
        UNUSED(rev_str);
    }

If you want to change an enum without breaking savefile compatibility,
the cardinal rule is that the numeric values of all existing constants
must not change.  If you are adding an option, add it at the end; if
you are removing a value, leave a placeholder of some kind.  Be sure
to test your code well, as there are sometimes obscure requirements on
what needs to be done for a placeholder.  Case in point: if a
MUT_UNUSED_n placeholder exists in the mutation_type enumeration, but
no mutation definition exists in mutation_defs, it will work fine until
somebody plays with a Vampire, as generating the list of fully active
mutations requires looking at mutation definitions for all mutations.

In extreme cases, it may not be possible to preserve load compatibility.
If this is the case, and please do this sparingly, you should increase
TAG_MAJOR_VERSION.  This will ensure that old save files are correctly
rejected, instead of causing a crash.

Historically TAG_MAJOR_VERSION has always been equal to the submajor
version of Crawl itself; however, this buys us very little, as users
do not need to identify Crawl save versions by contents under any
normal circumstance, and has cost us dearly in time spent debugging
startup crashes.  Therefore we should not do it; TAG_MAJOR_VERSION
should be incremented on all incompatible changes.

When TAG_MAJOR_VERSION is incremented, all existing TAG_MINOR_ checks
are no longer necessary and can be removed, along with deleting the
values of TAG_MINOR_ and restarting minor numbering from 1.

Thank you for listening, I hope this was useful.

Stefan


A few comments on Tiles
-----------------------
Whenever an old tile is removed without replacement, a new tile is added,
or the order of tiles changes, saved games can look wonky when loaded.
This happens because the tiles for floor and wall tiles get rolled once on
level creation and are subsequently stored in the level files, so they don't
change every time you reload the game. Any time a tile's number is changed,
the actually displayed tile gets shifted a bit which can lead to floor
looking like walls, staircases like shops and similar odd occurences.
Items and monsters that are cached out of sight may similarly be affected.

This is not a big problem and in the development version, we don't even
try to prevent this, but in the stable branches it should simply not happen.
This means that we should never add new tiles for a minor release, unless
said tiles replace the same amount of existing ones.
If we expect that we might have to add new tiles in a stable branch later
on, we should add placeholder tiles before the official release, that can
later be replaced without affecting saved games.

Johanna


A few comments on map caches and Lua markers
--------------------------------------------
Map and vault definitions are read from the relevant .des files and are stored
in a binary format to prevent slow-down every time crawl starts. If new
attributes or properties are added to vault definitions, save-compatibility
needs to be ensured in much the same way as for normal saves. Until recently,
the .des cache used a different version number to the main major/minor system.
In theory, all that needs to be done now is to bump the minor version, which
will cause the being-loaded .des cache file to be considered invalid, and thus
rebuilt.

When modifying a Lua marker, specifically when adding new options, etc, you'll
need to likewise ensure save compatibility. The minor version is currently
accessible by calling file.minor_version(reader). This will return a numeric
value that has to be manually compared to the relevant minor tag.

The current major version of the software is accessible by calling
file.major_version(). There's no need to pass the reader in, as if the major
version of a file mis-matches, the game won't attempt to load it, and you will
never get to a stage of being able to look for it.

All Lua markers will need to be checked for minor-version checks when updating
the major version.

due


Scheduling compatibility code for removal
-----------------------------------------
In order to increase long-term maintainability, it is customary to wrap
save compatibility code in #if blocks that check for the current value
of TAG_MAJOR_VERSION:

    #if TAG_MAJOR_VERSION == 34
        if (you.mutation[MUT_TELEPORT_CONTROL] == 1)
            you.mutation[MUT_TELEPORT_CONTROL] = 0;
        if (you.mutation[MUT_TRAMPLE_RESISTANCE] > 1)
            you.mutation[MUT_TRAMPLE_RESISTANCE] = 1;
    #endif

Then when the major version is incremented and old saves are no longer
supported, the compatibility code will not be compiled, and can be
easily identified and removed for readability.

For code that should run only for newer saves, the control structure
itself, as well as any alternative code to handle older saves, should be
protected by such #ifs; but the new code should be outside them.

    #if TAG_MAJOR_VERSION == 34
        if (th.getMinorVersion() >= TAG_MINOR_LORC_TEMPERATURE)
        {
    #endif
            you.temperature = unmarshallFloat(th);  // new save: keep this
            you.temperature_last = unmarshallFloat(th);
    #if TAG_MAJOR_VERSION == 34
        }
        else
        {
            you.temperature = 0.0;
            you.temperature_last = 0.0;
        }
    #endif

Then when the major version is incremented, the code will preprocess to:

            you.temperature = unmarshallFloat(th);
            you.temperature_last = unmarshallFloat(th);

and the new behaviour will happen unconditionally.

Other targets for #if TAG_MAJOR_VERSION checks include enumerators that
are no longer used; and new enumerators that were added to the end of
their enum for compatibility reasons but make more sense somewhere else
in the list.  In the latter case one would use a pair of #ifs, one for
the current version and one for future versions:

        DNGN_ENTER_ABYSS,
        DNGN_EXIT_ABYSS,
    #if TAG_MAJOR_VERSION > 34   // new location
        DNGN_ABYSSAL_STAIR,
    #endif
        DNGN_STONE_ARCH,
. . .
        DNGN_UNKNOWN_PORTAL,
    #if TAG_MAJOR_VERSION == 34  // old location
        DNGN_ABYSSAL_STAIR,
        DNGN_BADLY_SEALED_DOOR,
    #endif

neil


Merges
------

Whenever you do a merge or rebase of a branch that introduces new monsters,
items, etc., it is very important to pay attention to enums. Unfortunately, git
will sometimes automatically resolve the merge in exactly the way you didn't
want, putting enumerators in their branch location rather than after the new
trunk enumerators, all without giving any kind of conflict warning. If such a
problem isn't caught before the servers are rebuilt, players will be left with
buggy saves and the problems can be very hard to fix, with nasty kludges to try
to guess the correct type of a monster. See for example the fixes in:
    0.13-a0-2175-ga079a5c
    0.14-a0-2325-gf687a29

To avoid situations like these, do not rely on conflict detection and
resolution to merge enums correctly! Manually review all enum changes to make
sure that nothing is inserted before an enumerator that already exists in
trunk. You can look at the overall changes, with intervening history squashed,
using a command like:

    ~$ git diff master..HEAD enum.h

neil
