forked from crawl/crawl
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsave_compatibility.txt
294 lines (241 loc) · 13.5 KB
/
save_compatibility.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
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 public servers that support trunk
games, since compatibility-breaking changes prevent players from
upgrading to receive future bugfixes, and result in bug reports
against obsolete versions.
2. Save compatibility architecture
----------------------------------
For save compatibility purposes, the actual version number (for example, 0.12.2
or 0.13-a0-2578-ged92a99) makes almost no difference. Instead, there are three
version numbers (which we call “tags” for historic reasons) associated with
each save and with each version of crawl: the character format, major version,
and minor version.
The character format tag is stored in the “character info” section of the save:
the basic information such as species and level that is displayed in the save
browser. If this section is changed in an incompatible way, the character
format version would be incremented. However, Crawl would not even see the old
saves; this would be rather bad (they might accidentally be overwritten, for
example), so we have never made such a change: the format is still version 0.
Appending new information to this section requires only incrementing the minor
tag (see below), which is much safer.
The major version tag is stored in each section of the save (each level,
character, transiting monsters, and so on). Changing this version indicates
that Crawl is no longer compatible with the old save: the save will be coloured
red in the save browser and cannot be loaded. The major tag is never
incremented within a release (e.g. between 0.12.0 and 0.12.1), only in trunk.
We try to avoid doing this when we can, because it means trunk players will be
unable to transfer their saves to newer versions to get bug fixes; it
furthermore means that local players will have to keep the previous stable
release installed until they have finished their games.
The minor version tag is also stored in each section of the save, and is used
to indicate changes that require special handling for loading old saves. The
save-loading code if full of checks that say “only do this if the save’s minor
tag is greater than N” or “initialize this new field if the minor tag is less
than N”. Older versions of Crawl will refuse to load a save with a too-new
minor tag (that is, we do not provide forward compatibility between versions),
but newer versions will see the old tag and apply the appropriate fixups on
load (which makes the save incompatible with the old version of crawl).
The minor tag is reset to zero whenever the major tag changes. A major bump
provides an opportunity to clean up the old save-compatibility code, since the
old saves won’t be loadable anyway; and to remove or rearrange enumeration
values that could not change without breaking compatibility.
3. Maintaining save compatibility
---------------------------------
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.
4. 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.
5. 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.
6. 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
Note that the function tag_read_char() should generally *not* use the
"#if TAG_MAJOR_VERSION" construct, as character chunks are intended to be
visible (in the saved game browser) even across major save bumps. In that
function, one would instead check the chunk's major version at runtime.
Additionally, minor tag comparisons in tag_read_char() typically use
numbers rather than tag_minor_version enumerators, so that they do not
have to be rewritten on major version bumps.
if (major > 34 || major == 34 && minor >= 29)
crawl_state.map = unmarshallString2(th);
A comment with the name of the enumerator would be apropos, but is not
required:
// TAG_MINOR_EXPLORE_MODE and TAG_MINOR_FIX_EXPLORE_MODE
if (major == 34 && (minor >= 121 && minor < 130))
you.explore = unmarshallBoolean(th);
7. Git 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