summaryrefslogtreecommitdiff
path: root/src/saveload/saveload.cpp
diff options
context:
space:
mode:
authorPatric Stout <truebrain@openttd.org>2021-06-14 10:05:30 +0200
committerPatric Stout <github@truebrain.nl>2021-07-02 22:21:58 +0200
commit7dd5fd6ed497e1da40c13075d6e37b54ab12a082 (patch)
tree19f2e49e0e9ad714cec2fcf917dca954d33d8c0a /src/saveload/saveload.cpp
parent513641f9baaa732ab8000bc452a26284ae601f32 (diff)
downloadopenttd-7dd5fd6ed497e1da40c13075d6e37b54ab12a082.tar.xz
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.
Diffstat (limited to 'src/saveload/saveload.cpp')
-rw-r--r--src/saveload/saveload.cpp421
1 files changed, 384 insertions, 37 deletions
diff --git a/src/saveload/saveload.cpp b/src/saveload/saveload.cpp
index 2184ef85b..82e96080e 100644
--- a/src/saveload/saveload.cpp
+++ b/src/saveload/saveload.cpp
@@ -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.
@@ -580,6 +581,39 @@ static inline uint SlGetArrayLength(size_t 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
* @param conv VarType type of variable that is used for calculating the size
@@ -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);
@@ -1502,6 +1560,34 @@ static inline bool SlIsObjectValidInSavegame(const SaveLoad &sld)
}
/**
+ * 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.
* @param slt The SaveLoad table with objects to save/load.
@@ -1765,6 +1851,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);
+
+ /* 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 (m) {
+ 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);
+
+ /* 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 (m) {
+ 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;
- 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");
+ 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 {
- SlErrorCorrupt("Invalid chunk type");
+ SlSkipBytes(len);
}
+ if (_sl.reader->GetSize() != endoffs) SlErrorCorrupt("Invalid chunk size");
+ break;
+ default:
+ 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;
+}