Feature: framework to make savegames self-descriptive

We won't be able to make it fully self-descriptive (looking at you
MAP-chunks), but anything else can. With this framework, we can
add headers for each chunk explaining how each chunk looks like
in detail.

They also will all be tables, making it a lot easier to read in
external tooling, and opening the way to consider a database
(like SQLite) to use as savegame format.

Lastly, with the headers in the savegame, you can freely add
fields without needing a savegame version bump; older versions
of OpenTTD will simply ignore the new field. This also means
we can remove all the SLE_CONDNULL, as they are irrelevant.

The next few commits will start using this framework.
This commit is contained in:
Patric Stout
2021-06-14 10:05:30 +02:00
committed by Patric Stout
parent 513641f9ba
commit 7dd5fd6ed4
20 changed files with 620 additions and 188 deletions

View File

@@ -199,6 +199,7 @@ struct SaveLoadParams {
size_t obj_len; ///< the length of the current object we are busy with
int array_index, last_array_index; ///< in the case of an array, the current and last positions
bool expect_table_header; ///< In the case of a table, if the header is saved/loaded.
MemoryDumper *dumper; ///< Memory dumper to write the savegame to.
SaveFilter *sf; ///< Filter to write the savegame to.
@@ -579,6 +580,39 @@ static inline uint SlGetArrayLength(size_t length)
return SlGetGammaLength(length);
}
/**
* Return the type as saved/loaded inside the savegame.
*/
static uint8 GetSavegameFileType(const SaveLoad &sld)
{
switch (sld.cmd) {
case SL_VAR:
return GetVarFileType(sld.conv); break;
case SL_STR:
case SL_STDSTR:
case SL_ARR:
case SL_VECTOR:
case SL_DEQUE:
return GetVarFileType(sld.conv) | SLE_FILE_HAS_LENGTH_FIELD; break;
case SL_REF:
return IsSavegameVersionBefore(SLV_69) ? SLE_FILE_U16 : SLE_FILE_U32;
case SL_REFLIST:
return (IsSavegameVersionBefore(SLV_69) ? SLE_FILE_U16 : SLE_FILE_U32) | SLE_FILE_HAS_LENGTH_FIELD;
case SL_SAVEBYTE:
return SLE_FILE_U8;
case SL_STRUCT:
case SL_STRUCTLIST:
return SLE_FILE_STRUCT | SLE_FILE_HAS_LENGTH_FIELD;
default: NOT_REACHED();
}
}
/**
* Return the size in bytes of a certain type of normal/atomic variable
* as it appears in memory. See VarTypes
@@ -610,7 +644,7 @@ static inline uint SlCalcConvMemLen(VarType conv)
*/
static inline byte SlCalcConvFileLen(VarType conv)
{
static const byte conv_file_size[] = {1, 1, 2, 2, 4, 4, 8, 8, 2};
static const byte conv_file_size[] = {0, 1, 1, 2, 2, 4, 4, 8, 8, 2};
uint8 type = GetVarFileType(conv);
assert(type < lengthof(conv_file_size));
@@ -646,6 +680,7 @@ int SlIterateArray()
for (;;) {
uint length = SlReadArrayLength();
if (length == 0) {
assert(!_sl.expect_table_header);
_next_offs = 0;
return -1;
}
@@ -653,8 +688,15 @@ int SlIterateArray()
_sl.obj_len = --length;
_next_offs = _sl.reader->GetSize() + length;
if (_sl.expect_table_header) {
_sl.expect_table_header = false;
return INT32_MAX;
}
switch (_sl.block_mode) {
case CH_SPARSE_TABLE:
case CH_SPARSE_ARRAY: index = (int)SlReadSparseIndex(); break;
case CH_TABLE:
case CH_ARRAY: index = _sl.array_index++; break;
default:
Debug(sl, 0, "SlIterateArray error");
@@ -687,6 +729,12 @@ void SlSetLength(size_t length)
switch (_sl.need_length) {
case NL_WANTLENGTH:
_sl.need_length = NL_NONE;
if ((_sl.block_mode == CH_TABLE || _sl.block_mode == CH_SPARSE_TABLE) && _sl.expect_table_header) {
_sl.expect_table_header = false;
SlWriteArrayLength(length + 1);
break;
}
switch (_sl.block_mode) {
case CH_RIFF:
/* Ugly encoding of >16M RIFF chunks
@@ -695,6 +743,7 @@ void SlSetLength(size_t length)
assert(length < (1 << 28));
SlWriteUint32((uint32)((length & 0xFFFFFF) | ((length >> 24) << 28)));
break;
case CH_TABLE:
case CH_ARRAY:
assert(_sl.last_array_index <= _sl.array_index);
while (++_sl.last_array_index <= _sl.array_index) {
@@ -702,6 +751,7 @@ void SlSetLength(size_t length)
}
SlWriteArrayLength(length + 1);
break;
case CH_SPARSE_TABLE:
case CH_SPARSE_ARRAY:
SlWriteArrayLength(length + 1 + SlGetArrayLength(_sl.array_index)); // Also include length of sparse index.
SlWriteSparseIndex(_sl.array_index);
@@ -1142,7 +1192,15 @@ static void SlArray(void *array, size_t length, VarType conv)
case SLA_LOAD: {
if (!IsSavegameVersionBefore(SLV_SAVELOAD_LIST_LENGTH)) {
size_t sv_length = SlReadArrayLength();
if (sv_length != length) SlErrorCorrupt("Fixed-length array is of wrong length");
if (GetVarMemType(conv) == SLE_VAR_NULL) {
/* We don't know this field, so we assume the length in the savegame is correct. */
length = sv_length;
} else if (sv_length != length) {
/* If the SLE_ARR changes size, a savegame bump is required
* and the developer should have written conversion lines.
* Error out to make this more visible. */
SlErrorCorrupt("Fixed-length array is of wrong length");
}
}
SlCopyInternal(array, length, conv);
@@ -1501,6 +1559,34 @@ static inline bool SlIsObjectValidInSavegame(const SaveLoad &sld)
return (_sl_version >= sld.version_from && _sl_version < sld.version_to);
}
/**
* Calculate the size of the table header.
* @param slt The SaveLoad table with objects to save/load.
* @return size of given object.
*/
static size_t SlCalcTableHeader(const SaveLoadTable &slt)
{
size_t length = 0;
for (auto &sld : slt) {
if (!SlIsObjectValidInSavegame(sld)) continue;
length += SlCalcConvFileLen(SLE_UINT8);
length += SlCalcStdStringLen(&sld.name);
}
length += SlCalcConvFileLen(SLE_UINT8); // End-of-list entry.
for (auto &sld : slt) {
if (!SlIsObjectValidInSavegame(sld)) continue;
if (sld.cmd == SL_STRUCTLIST || sld.cmd == SL_STRUCT) {
length += SlCalcTableHeader(sld.handler->GetDescription());
}
}
return length;
}
/**
* Calculate the size of an object.
* @param object to be measured.
@@ -1764,6 +1850,233 @@ void SlObject(void *object, const SaveLoadTable &slt)
}
}
/**
* Handler that is assigned when there is a struct read in the savegame which
* is not known to the code. This means we are going to skip it.
*/
class SlSkipHandler : public SaveLoadHandler {
void Save(void *object) const override
{
NOT_REACHED();
}
void Load(void *object) const override
{
size_t length = SlGetStructListLength(UINT32_MAX);
for (; length > 0; length--) {
SlObject(object, this->GetLoadDescription());
}
}
void LoadCheck(void *object) const override
{
this->Load(object);
}
virtual SaveLoadTable GetDescription() const override
{
return {};
}
virtual SaveLoadCompatTable GetCompatDescription() const override
{
NOT_REACHED();
}
};
/**
* Save or Load a table header.
* @note a table-header can never contain more than 65535 fields.
* @param slt The SaveLoad table with objects to save/load.
* @return When loading, the ordered SaveLoad array to use; otherwise an empty list.
*/
std::vector<SaveLoad> SlTableHeader(const SaveLoadTable &slt)
{
/* You can only use SlTableHeader if you are a CH_TABLE. */
assert(_sl.block_mode == CH_TABLE || _sl.block_mode == CH_SPARSE_TABLE);
switch (_sl.action) {
case SLA_LOAD_CHECK:
case SLA_LOAD: {
std::vector<SaveLoad> saveloads;
/* Build a key lookup mapping based on the available fields. */
std::map<std::string, const SaveLoad *> key_lookup;
for (auto &sld : slt) {
if (!SlIsObjectValidInSavegame(sld)) continue;
/* Check that there is only one active SaveLoad for a given name. */
assert(key_lookup.find(sld.name) == key_lookup.end());
key_lookup[sld.name] = &sld;
}
while (true) {
uint8 type;
SlSaveLoadConv(&type, SLE_UINT8);
if (type == SLE_FILE_END) break;
std::string key;
SlStdString(&key, SLE_STR);
auto sld_it = key_lookup.find(key);
if (sld_it == key_lookup.end()) {
Debug(sl, 2, "Field '{}' of type 0x{:02x} not found, skipping", key, type);
std::shared_ptr<SaveLoadHandler> handler = nullptr;
SaveLoadType slt;
switch (type & SLE_FILE_TYPE_MASK) {
case SLE_FILE_STRING:
/* Strings are always marked with SLE_FILE_HAS_LENGTH_FIELD, as they are a list of chars. */
slt = SL_STR;
break;
case SLE_FILE_STRUCT:
/* Structs are always marked with SLE_FILE_HAS_LENGTH_FIELD as SL_STRUCT is seen as a list of 0/1 in length. */
slt = SL_STRUCTLIST;
handler = std::make_shared<SlSkipHandler>();
break;
default:
slt = (type & SLE_FILE_HAS_LENGTH_FIELD) ? SL_ARR : SL_VAR;
break;
}
/* We don't know this field, so read to nothing. */
saveloads.push_back({key, slt, ((VarType)type & SLE_FILE_TYPE_MASK) | SLE_VAR_NULL, 1, SL_MIN_VERSION, SL_MAX_VERSION, 0, nullptr, 0, handler});
continue;
}
/* Validate the type of the field. If it is changed, the
* savegame should have been bumped so we know how to do the
* conversion. If this error triggers, that clearly didn't
* happen and this is a friendly poke to the developer to bump
* the savegame version and add conversion code. */
uint8 correct_type = GetSavegameFileType(*sld_it->second);
if (correct_type != type) {
Debug(sl, 1, "Field type for '{}' was expected to be 0x{:02x} but 0x{:02x} was found", key, correct_type, type);
SlErrorCorrupt("Field type is different than expected");
}
saveloads.push_back(*sld_it->second);
}
for (auto &sld : saveloads) {
if (sld.cmd == SL_STRUCTLIST || sld.cmd == SL_STRUCT) {
sld.handler->load_description = SlTableHeader(sld.handler->GetDescription());
}
}
return saveloads;
}
case SLA_SAVE: {
/* Automatically calculate the length? */
if (_sl.need_length != NL_NONE) {
SlSetLength(SlCalcTableHeader(slt));
if (_sl.need_length == NL_CALCLENGTH) break;
}
for (auto &sld : slt) {
if (!SlIsObjectValidInSavegame(sld)) continue;
/* Make sure we are not storing empty keys. */
assert(!sld.name.empty());
uint8 type = GetSavegameFileType(sld);
assert(type != SLE_FILE_END);
SlSaveLoadConv(&type, SLE_UINT8);
SlStdString(const_cast<std::string *>(&sld.name), SLE_STR);
}
/* Add an end-of-header marker. */
uint8 type = SLE_FILE_END;
SlSaveLoadConv(&type, SLE_UINT8);
/* After the table, write down any sub-tables we might have. */
for (auto &sld : slt) {
if (!SlIsObjectValidInSavegame(sld)) continue;
if (sld.cmd == SL_STRUCTLIST || sld.cmd == SL_STRUCT) {
/* SlCalcTableHeader already looks in sub-lists, so avoid the length being added twice. */
NeedLength old_need_length = _sl.need_length;
_sl.need_length = NL_NONE;
SlTableHeader(sld.handler->GetDescription());
_sl.need_length = old_need_length;
}
}
break;
}
default: NOT_REACHED();
}
return std::vector<SaveLoad>();
}
/**
* Load a table header in a savegame compatible way. If the savegame was made
* before table headers were added, it will fall back to the
* SaveLoadCompatTable for the order of fields while loading.
*
* @note You only have to call this function if the chunk existed as a
* non-table type before converting it to a table. New chunks created as
* table can call SlTableHeader() directly.
*
* @param slt The SaveLoad table with objects to save/load.
* @param slct The SaveLoadCompat table the original order of the fields.
* @return When loading, the ordered SaveLoad array to use; otherwise an empty list.
*/
std::vector<SaveLoad> SlCompatTableHeader(const SaveLoadTable &slt, const SaveLoadCompatTable &slct)
{
assert(_sl.action == SLA_LOAD || _sl.action == SLA_LOAD_CHECK);
/* CH_TABLE / CH_SPARSE_TABLE always have a header. */
if (_sl.block_mode == CH_TABLE || _sl.block_mode == CH_SPARSE_TABLE) return SlTableHeader(slt);
std::vector<SaveLoad> saveloads;
/* Build a key lookup mapping based on the available fields. */
std::map<std::string, std::vector<const SaveLoad *>> key_lookup;
for (auto &sld : slt) {
/* All entries should have a name; otherwise the entry should just be removed. */
assert(!sld.name.empty());
key_lookup[sld.name].push_back(&sld);
}
for (auto &slc : slct) {
if (slc.name.empty()) {
/* In old savegames there can be data we no longer care for. We
* skip this by simply reading the amount of bytes indicated and
* send those to /dev/null. */
saveloads.push_back({"", SL_NULL, SLE_FILE_U8 | SLE_VAR_NULL, slc.length, slc.version_from, slc.version_to, 0, nullptr, 0, nullptr});
} else {
auto sld_it = key_lookup.find(slc.name);
/* If this branch triggers, it means that an entry in the
* SaveLoadCompat list is not mentioned in the SaveLoad list. Did
* you rename a field in one and not in the other? */
if (sld_it == key_lookup.end()) {
/* This isn't an assert, as that leaves no information what
* field was to blame. This way at least we have breadcrumbs. */
Debug(sl, 0, "internal error: saveload compatibility field '{}' not found", slc.name);
SlErrorCorrupt("Internal error with savegame compatibility");
}
for (auto &sld : sld_it->second) {
saveloads.push_back(*sld);
}
}
}
for (auto &sld : saveloads) {
if (!SlIsObjectValidInSavegame(sld)) continue;
if (sld.cmd == SL_STRUCTLIST || sld.cmd == SL_STRUCT) {
sld.handler->load_description = SlCompatTableHeader(sld.handler->GetDescription(), sld.handler->GetCompatDescription());
}
}
return saveloads;
}
/**
* Save or Load (a list of) global variables.
* @param slt The SaveLoad table with objects to save/load.
@@ -1811,33 +2124,43 @@ static void SlLoadChunk(const ChunkHandler &ch)
size_t len;
size_t endoffs;
_sl.block_mode = m;
_sl.block_mode = m & CH_TYPE_MASK;
_sl.obj_len = 0;
_sl.expect_table_header = (_sl.block_mode == CH_TABLE || _sl.block_mode == CH_SPARSE_TABLE);
switch (m) {
/* The header should always be at the start. Read the length; the
* load_proc() should as first action process the header. */
if (_sl.expect_table_header) {
SlIterateArray();
}
switch (_sl.block_mode) {
case CH_TABLE:
case CH_ARRAY:
_sl.array_index = 0;
ch.load_proc();
if (_next_offs != 0) SlErrorCorrupt("Invalid array length");
break;
case CH_SPARSE_TABLE:
case CH_SPARSE_ARRAY:
ch.load_proc();
if (_next_offs != 0) SlErrorCorrupt("Invalid array length");
break;
case CH_RIFF:
/* Read length */
len = (SlReadByte() << 16) | ((m >> 4) << 24);
len += SlReadUint16();
_sl.obj_len = len;
endoffs = _sl.reader->GetSize() + len;
ch.load_proc();
if (_sl.reader->GetSize() != endoffs) SlErrorCorrupt("Invalid chunk size");
break;
default:
if ((m & 0xF) == CH_RIFF) {
/* Read length */
len = (SlReadByte() << 16) | ((m >> 4) << 24);
len += SlReadUint16();
_sl.obj_len = len;
endoffs = _sl.reader->GetSize() + len;
ch.load_proc();
if (_sl.reader->GetSize() != endoffs) SlErrorCorrupt("Invalid chunk size");
} else {
SlErrorCorrupt("Invalid chunk type");
}
SlErrorCorrupt("Invalid chunk type");
break;
}
if (_sl.expect_table_header) SlErrorCorrupt("Table chunk without header");
}
/**
@@ -1851,43 +2174,54 @@ static void SlLoadCheckChunk(const ChunkHandler &ch)
size_t len;
size_t endoffs;
_sl.block_mode = m;
_sl.block_mode = m & CH_TYPE_MASK;
_sl.obj_len = 0;
_sl.expect_table_header = (_sl.block_mode == CH_TABLE || _sl.block_mode == CH_SPARSE_TABLE);
switch (m) {
/* The header should always be at the start. Read the length; the
* load_check_proc() should as first action process the header. */
if (_sl.expect_table_header && ch.load_check_proc != nullptr) {
/* If load_check_proc() is nullptr, SlSkipArray() will already skip the header. */
SlIterateArray();
}
switch (_sl.block_mode) {
case CH_TABLE:
case CH_ARRAY:
_sl.array_index = 0;
if (ch.load_check_proc) {
if (ch.load_check_proc != nullptr) {
ch.load_check_proc();
} else {
SlSkipArray();
}
break;
case CH_SPARSE_TABLE:
case CH_SPARSE_ARRAY:
if (ch.load_check_proc) {
if (ch.load_check_proc != nullptr) {
ch.load_check_proc();
} else {
SlSkipArray();
}
break;
case CH_RIFF:
/* Read length */
len = (SlReadByte() << 16) | ((m >> 4) << 24);
len += SlReadUint16();
_sl.obj_len = len;
endoffs = _sl.reader->GetSize() + len;
if (ch.load_check_proc) {
ch.load_check_proc();
} else {
SlSkipBytes(len);
}
if (_sl.reader->GetSize() != endoffs) SlErrorCorrupt("Invalid chunk size");
break;
default:
if ((m & 0xF) == CH_RIFF) {
/* Read length */
len = (SlReadByte() << 16) | ((m >> 4) << 24);
len += SlReadUint16();
_sl.obj_len = len;
endoffs = _sl.reader->GetSize() + len;
if (ch.load_check_proc) {
ch.load_check_proc();
} else {
SlSkipBytes(len);
}
if (_sl.reader->GetSize() != endoffs) SlErrorCorrupt("Invalid chunk size");
} else {
SlErrorCorrupt("Invalid chunk type");
}
SlErrorCorrupt("Invalid chunk type");
break;
}
if (_sl.expect_table_header) SlErrorCorrupt("Table chunk without header");
}
/**
@@ -1906,24 +2240,31 @@ static void SlSaveChunk(const ChunkHandler &ch)
Debug(sl, 2, "Saving chunk {:c}{:c}{:c}{:c}", ch.id >> 24, ch.id >> 16, ch.id >> 8, ch.id);
_sl.block_mode = ch.type;
switch (ch.type) {
_sl.expect_table_header = (_sl.block_mode == CH_TABLE || _sl.block_mode == CH_SPARSE_TABLE);
_sl.need_length = (_sl.expect_table_header || _sl.block_mode == CH_RIFF) ? NL_WANTLENGTH : NL_NONE;
switch (_sl.block_mode) {
case CH_RIFF:
_sl.need_length = NL_WANTLENGTH;
proc();
break;
case CH_TABLE:
case CH_ARRAY:
_sl.last_array_index = 0;
SlWriteByte(CH_ARRAY);
SlWriteByte(_sl.block_mode);
proc();
SlWriteArrayLength(0); // Terminate arrays
break;
case CH_SPARSE_TABLE:
case CH_SPARSE_ARRAY:
SlWriteByte(CH_SPARSE_ARRAY);
SlWriteByte(_sl.block_mode);
proc();
SlWriteArrayLength(0); // Terminate arrays
break;
default: NOT_REACHED();
}
if (_sl.expect_table_header) SlErrorCorrupt("Table chunk without header");
}
/** Save all chunks */
@@ -3068,3 +3409,9 @@ void FileToSaveLoad::SetTitle(const char *title)
{
strecpy(this->title, title, lastof(this->title));
}
SaveLoadTable SaveLoadHandler::GetLoadDescription() const
{
assert(this->load_description.has_value());
return *this->load_description;
}