Improved Common::Serializer in several ways:

* Added support versioned serialization
* Added a convenience API for handling 'magic IDs' transparently
* Exposed the err()/clearErr() methods of the underlying streams
* Added a basic unit test for versioned loading (more should be added, in particular for saving)
* Removed the syncString(char *, uint16) alias for syncBytes(byte *buf, uint32 size)

svn-id: r40723
This commit is contained in:
Max Horn 2009-05-19 11:23:13 +00:00
parent 2b32ba7cb3
commit 42cd218400
4 changed files with 241 additions and 61 deletions

View file

@ -34,7 +34,9 @@ namespace Common {
#define SYNC_AS(SUFFIX,TYPE,SIZE) \
template <typename T> \
void syncAs ## SUFFIX(T &val) { \
void syncAs ## SUFFIX(T &val, Version minVersion = 0, Version maxVersion = kLastVersion) { \
if (_version < minVersion || _version > maxVersion) \
return; \
if (_loadStream) \
val = static_cast<T>(_loadStream->read ## SUFFIX()); \
else { \
@ -45,31 +47,130 @@ namespace Common {
}
// TODO: Write comment for this
// TODO: Inspired by the SCUMM engine -- move to common/ code and use in more engines?
/**
* This class allows syncing / serializing data (primarily game savestates)
* between memory and Read/WriteStreams.
* It optionally supports versioning the serialized data (client code must
* use the syncVersion() method for this). This makes it possible to support
* multiple versions of a savegame format with a single codepath
*
* This class was heavily inspired by the save/load code in the SCUMM engine.
*
* @todo Maybe rename this to Synchronizer?
*
* @todo One feature the SCUMM code has but that is missing here: Support for
* syncing arrays of a given type and *fixed* size; and also support
* for when the array size changed between versions. Also, support for
* 2D-arrays.
*
* @todo Proper error handling!
*/
class Serializer {
public:
typedef uint32 Version;
static const Version kLastVersion = 0xFFFFFFFF;
protected:
Common::SeekableReadStream *_loadStream;
Common::WriteStream *_saveStream;
uint _bytesSynced;
Version _version;
public:
Serializer(Common::SeekableReadStream *in, Common::WriteStream *out)
: _loadStream(in), _saveStream(out), _bytesSynced(0) {
: _loadStream(in), _saveStream(out), _bytesSynced(0), _version(0) {
assert(in || out);
}
virtual ~Serializer() {}
inline bool isSaving() { return (_saveStream != 0); }
inline bool isLoading() { return (_loadStream != 0); }
/**
* Returns true if an I/O failure occurred.
* This flag is never cleared automatically. In order to clear it,
* client code has to call clearErr() explicitly.
*/
bool err() const {
if (_saveStream)
return _saveStream->err();
else
return _loadStream->err();
}
/**
* Reset the I/O error status as returned by err().
*/
void clearErr() {
if (_saveStream)
_saveStream->clearErr();
else
_loadStream->clearErr();
}
/**
* Sync the "version" of the savegame we are loading/creating.
* @param currentVersion current format version, used when writing a new file
* @return true if the version of the savestate is not too new.
*/
bool syncVersion(Version currentVersion) {
_version = currentVersion;
syncAsUint32LE(_version);
return _version <= currentVersion;
}
/**
* Return the version of the savestate being serialized. Useful if the engine
* needs to perform additional adjustments when loading old savestates.
*/
Version getVersion() const { return _version; }
/**
* Sync a 'magic id' of up to 256 bytes, and return whether it matched.
* When saving, this will simply write out the magic id and return true.
* When loading, this will read the specified number of bytes, compare it
* to the given magic id and return true on a match, false otherwise.
*
* A typical magic id is a FOURCC like 'MAGI'.
*
* @param magic magic id as a byte sequence
* @param size length of the magic id in bytes
* @return true if the magic id matched, false otherwise
*
* @todo Should this have minVersion/maxVersion params, too?
*/
bool syncMagic(const char *magic, byte size) {
char buf[256];
bool match;
if (isSaving()) {
_saveStream->write(magic, size);
match = true;
} else {
_loadStream->read(buf, size);
match = (0 == memcmp(buf, magic, size));
}
_bytesSynced += size;
return match;
}
bool isSaving() { return (_saveStream != 0); }
bool isLoading() { return (_loadStream != 0); }
/**
* Return the total number of bytes synced so far.
*/
uint bytesSynced() const { return _bytesSynced; }
/**
* Skip a number of bytes in the data stream.
* This is useful to skip obsolete fields in old savestates.
*/
void skip(uint32 size) {
void skip(uint32 size, Version minVersion = 0, Version maxVersion = kLastVersion) {
if (_version < minVersion || _version > maxVersion)
return; // Ignore anything which is not supposed to be present in this save game version
_bytesSynced += size;
if (_loadStream)
if (isLoading())
_loadStream->skip(size);
else {
while (size--)
@ -80,8 +181,11 @@ public:
/**
* Sync a block of arbitrary fixed-length data.
*/
void syncBytes(byte *buf, uint32 size) {
if (_loadStream)
void syncBytes(byte *buf, uint32 size, Version minVersion = 0, Version maxVersion = kLastVersion) {
if (_version < minVersion || _version > maxVersion)
return; // Ignore anything which is not supposed to be present in this save game version
if (isLoading())
_loadStream->read(buf, size);
else
_saveStream->write(buf, size);
@ -90,9 +194,13 @@ public:
/**
* Sync a C-string, by treating it as a zero-terminated byte sequence.
* @todo Replace this method with a special Syncer class for Common::String
*/
void syncString(Common::String &str) {
if (_loadStream) {
void syncString(Common::String &str, Version minVersion = 0, Version maxVersion = kLastVersion) {
if (_version < minVersion || _version > maxVersion)
return; // Ignore anything which is not supposed to be present in this save game version
if (isLoading()) {
char c;
str.clear();
while ((c = _loadStream->readByte())) {
@ -107,13 +215,6 @@ public:
}
}
/**
* Sync a fixed length C-string
*/
void syncString(char *buf, uint16 size) {
syncBytes((byte *)buf, size);
}
SYNC_AS(Byte, byte, 1)
SYNC_AS(Uint16LE, uint16, 2)
@ -125,45 +226,13 @@ public:
SYNC_AS(Uint32BE, uint32, 4)
SYNC_AS(Sint32LE, int32, 4)
SYNC_AS(Sint32BE, int32, 4)
protected:
Common::SeekableReadStream *_loadStream;
Common::WriteStream *_saveStream;
uint _bytesSynced;
};
#undef SYNC_AS
// TODO: Make a subclass "VersionedSerializer", which makes it easy to support
// multiple versions of a savegame format (again inspired by SCUMM).
/*
class VersionedSerializer : public Serializer {
public:
// "version" is the version of the savegame we are loading/creating
VersionedSerializer(Common::SeekableReadStream *in, Common::OutSaveFile *out, int version)
: Serializer(in, out), _version(version) {
assert(in || out);
}
void syncBytes(byte *buf, uint16 size, int minVersion = 0, int maxVersion = INF) {
if (_version < minVersion || _version > maxVersion)
return; // Do nothing if too old or too new
if (_loadStream) {
_loadStream->read(buf, size);
} else {
_saveStream->write(buf, size);
}
}
...
};
*/
// Mixin class / interface
// TODO Maybe call it ISerializable or SerializableMixin ?
// TODO: Taken from SCUMM engine -- move to common/ code?
// TODO: Maybe rename this to Syncable ?
class Serializable {
public:
virtual ~Serializable() {}

View file

@ -148,8 +148,8 @@ static void syncBasicInfo(Common::Serializer &s) {
static void syncBackgroundTable(Common::Serializer &s) {
// restore backgroundTable
for (int i = 0; i < 8; i++) {
s.syncString(backgroundTable[i].name, 9);
s.syncString(backgroundTable[i].extention, 6);
s.syncBytes((byte *)backgroundTable[i].name, 9);
s.syncBytes((byte *)backgroundTable[i].extention, 6);
}
}
@ -189,7 +189,7 @@ static void syncFilesDatabase(Common::Serializer &s) {
}
s.syncAsSint16LE(fe.subData.index);
s.syncString(fe.subData.name, 13);
s.syncBytes((byte *)fe.subData.name, 13);
s.syncAsByte(dummyVal);
s.syncAsSint16LE(fe.subData.transparency);
@ -213,7 +213,7 @@ static void syncPreloadData(Common::Serializer &s) {
for (int i = 0; i < 64; i++) {
preloadStruct &pe = preloadData[i];
s.syncString(pe.name, 15);
s.syncBytes((byte *)pe.name, 15);
s.syncAsByte(dummyByte);
s.syncAsUint32LE(pe.size);
s.syncAsUint32LE(pe.sourceSize);
@ -231,7 +231,7 @@ static void syncOverlays1(Common::Serializer &s) {
for (int i = 0; i < numOfLoadedOverlay; i++) {
overlayStruct &oe = overlayTable[i];
s.syncString(oe.overlayName, 13);
s.syncBytes((byte *)oe.overlayName, 13);
s.syncAsByte(dummyByte);
s.syncAsUint32LE(dummyLong);
s.syncAsUint16LE(oe.alreadyLoaded);
@ -464,7 +464,7 @@ static void syncIncrust(Common::Serializer &s) {
s.syncAsSint16LE(t->saveSize);
s.syncAsSint16LE(t->savedX);
s.syncAsSint16LE(t->savedY);
s.syncString(t->name, 13);
s.syncBytes((byte *)t->name, 13);
s.syncAsByte(dummyByte);
s.syncAsSint16LE(t->spriteId);
s.syncAsUint16LE(dummyWord);
@ -597,7 +597,7 @@ static void DoSync(Common::Serializer &s) {
syncPalette(s, newPal);
syncPalette(s, workpal);
s.syncString(currentCtpName, 40);
s.syncBytes((byte *)currentCtpName, 40);
syncBackgroundTable(s);
syncPalScreen(s);

View file

@ -70,7 +70,7 @@ void MusicPlayer::resume() {
void MusicPlayer::doSync(Common::Serializer &s) {
// synchronise current music name, if any, state, and position
s.syncString(_musicName, 33);
s.syncBytes((byte *)_musicName, 33);
uint16 v = (uint16)songLoaded();
s.syncAsSint16LE(v);
s.syncAsSint16LE(_songPlayed);

111
test/common/serializer.h Normal file
View file

@ -0,0 +1,111 @@
#include <cxxtest/TestSuite.h>
#include "common/serializer.h"
#include "common/stream.h"
class SerializerTestSuite : public CxxTest::TestSuite {
Common::SeekableReadStream *_inStreamV1;
Common::SeekableReadStream *_inStreamV2;
public:
void setUp() {
// Our pseudo data format is as follows:
// * magic id - string "MAGI"
// * Version - uint32, LE
// * uint32, LE (available in v2 onward)
// * uint16, BE (available in v1 onward)
// * sint16, LE (available only in v1)
// * byte (always available)
static const byte contents_v1[] = {
'M', 'A', 'G', 'I', // magic id
0x01, 0x00, 0x00, 0x00, // Version
0x06, 0x07, // uint16, BE (available in v1 onward)
0xfe, 0xff, // sint16, LE (available only in v1)
0x0a // byte (always available)
};
static const byte contents_v2[] = {
'M', 'A', 'G', 'I', // magic id
0x02, 0x00, 0x00, 0x00, // Version
0x02, 0x03, 0x04, 0x05, // uint32, LE (available in v2 onward)
0x06, 0x07, // uint16, BE (available in v1 onward)
0x0a // byte (always available)
};
_inStreamV1 = new Common::MemoryReadStream(contents_v1, sizeof(contents_v1));
_inStreamV2 = new Common::MemoryReadStream(contents_v2, sizeof(contents_v2));
}
void tearDown() {
delete _inStreamV1;
delete _inStreamV2;
}
// A method which reads a v1 file
void readVersioned_v1(Common::SeekableReadStream *stream, int version) {
Common::Serializer ser(stream, 0);
TS_ASSERT(ser.syncMagic("MAGI", 4));
TS_ASSERT(ser.syncVersion(1));
TS_ASSERT_EQUALS(ser.getVersion(), version);
uint32 tmp;
ser.syncAsUint16BE(tmp, Common::Serializer::Version(1));
TS_ASSERT_EQUALS(tmp, 0x0607);
ser.syncAsSint16LE(tmp, Common::Serializer::Version(1));
TS_ASSERT_EQUALS(tmp, -2);
ser.syncAsByte(tmp);
TS_ASSERT_EQUALS(tmp, 0x0a);
}
// A method which reads a v2 file
void readVersioned_v2(Common::SeekableReadStream *stream, int version) {
Common::Serializer ser(stream, 0);
TS_ASSERT(ser.syncMagic("MAGI", 4));
TS_ASSERT(ser.syncVersion(2));
TS_ASSERT_EQUALS(ser.getVersion(), version);
uint32 tmp;
// Read a value only available starting with v2.
// Thus if we load an old save, it must be
// manually set. To simplify that, no sync method should
// modify the value passed to it if nothing was read!
tmp = 0x12345678;
ser.syncAsUint32LE(tmp, Common::Serializer::Version(2));
if (ser.getVersion() < 2) {
TS_ASSERT_EQUALS(tmp, 0x12345678);
} else {
TS_ASSERT_EQUALS(tmp, 0x05040302);
}
ser.syncAsUint16BE(tmp, Common::Serializer::Version(1));
TS_ASSERT_EQUALS(tmp, 0x0607);
// Skip over obsolete data
ser.skip(2, Common::Serializer::Version(1), Common::Serializer::Version(1));
ser.syncAsByte(tmp);
TS_ASSERT_EQUALS(tmp, 0x0a);
}
void test_read_v1_as_v1() {
readVersioned_v1(_inStreamV1, 1);
}
// There is no test_read_v2_as_v1() because a v1 parser cannot possibly
// read v2 data correctly. It should instead error out if it
// detects a version newer than its current version.
void test_read_v2_as_v1() {
readVersioned_v2(_inStreamV1, 1);
}
void test_read_v2_as_v2() {
readVersioned_v2(_inStreamV2, 2);
}
};