mirror of
https://github.com/JGRennison/OpenTTD-patches.git
synced 2024-11-11 13:10:45 +00:00
d1f1a6942a
Use modified upstream saveload implementation for these versions Re-arrange headers to support multiple saveload implementations
3729 lines
110 KiB
C++
3729 lines
110 KiB
C++
/*
|
|
* This file is part of OpenTTD.
|
|
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
|
|
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/**
|
|
* @file saveload.cpp
|
|
* All actions handling saving and loading goes on in this file. The general actions
|
|
* are as follows for saving a game (loading is analogous):
|
|
* <ol>
|
|
* <li>initialize the writer by creating a temporary memory-buffer for it
|
|
* <li>go through all to-be saved elements, each 'chunk' (#ChunkHandler) prefixed by a label
|
|
* <li>use their description array (#SaveLoad) to know what elements to save and in what version
|
|
* of the game it was active (used when loading)
|
|
* <li>write all data byte-by-byte to the temporary buffer so it is endian-safe
|
|
* <li>when the buffer is full; flush it to the output (eg save to file) (_sl.buf, _sl.bufp, _sl.bufe)
|
|
* <li>repeat this until everything is done, and flush any remaining output to file
|
|
* </ol>
|
|
*/
|
|
|
|
#include "../stdafx.h"
|
|
#include "../debug.h"
|
|
#include "../station_base.h"
|
|
#include "../thread.h"
|
|
#include "../town.h"
|
|
#include "../network/network.h"
|
|
#include "../window_func.h"
|
|
#include "../strings_func.h"
|
|
#include "../core/endian_func.hpp"
|
|
#include "../vehicle_base.h"
|
|
#include "../company_func.h"
|
|
#include "../date_func.h"
|
|
#include "../autoreplace_base.h"
|
|
#include "../roadstop_base.h"
|
|
#include "../linkgraph/linkgraph.h"
|
|
#include "../linkgraph/linkgraphjob.h"
|
|
#include "../statusbar_gui.h"
|
|
#include "../fileio_func.h"
|
|
#include "../gamelog.h"
|
|
#include "../string_func.h"
|
|
#include "../string_func_extra.h"
|
|
#include "../fios.h"
|
|
#include "../error.h"
|
|
#include "../scope.h"
|
|
#include <atomic>
|
|
#include <deque>
|
|
#include <string>
|
|
#ifdef __EMSCRIPTEN__
|
|
# include <emscripten.h>
|
|
#endif
|
|
|
|
#include "../tbtr_template_vehicle.h"
|
|
|
|
#include "table/strings.h"
|
|
|
|
#include "saveload_internal.h"
|
|
#include "saveload_filter.h"
|
|
#include "saveload_buffer.h"
|
|
#include "extended_ver_sl.h"
|
|
|
|
#include <deque>
|
|
#include <vector>
|
|
|
|
#include "../thread.h"
|
|
#include <mutex>
|
|
#include <condition_variable>
|
|
#if defined(__MINGW32__)
|
|
#include "../3rdparty/mingw-std-threads/mingw.mutex.h"
|
|
#include "../3rdparty/mingw-std-threads/mingw.condition_variable.h"
|
|
#endif
|
|
|
|
#include "../safeguards.h"
|
|
|
|
extern const SaveLoadVersion SAVEGAME_VERSION = SLV_CUSTOM_SUBSIDY_DURATION; ///< Current savegame version of OpenTTD.
|
|
extern const SaveLoadVersion MAX_LOAD_SAVEGAME_VERSION = (SaveLoadVersion)(SL_MAX_VERSION - 1); ///< Max loadable savegame version of OpenTTD.
|
|
|
|
const SaveLoadVersion SAVEGAME_VERSION_EXT = (SaveLoadVersion)(0x8000); ///< Savegame extension indicator mask
|
|
|
|
SavegameType _savegame_type; ///< type of savegame we are loading
|
|
FileToSaveLoad _file_to_saveload; ///< File to save or load in the openttd loop.
|
|
|
|
uint32 _ttdp_version; ///< version of TTDP savegame (if applicable)
|
|
SaveLoadVersion _sl_version; ///< the major savegame version identifier
|
|
byte _sl_minor_version; ///< the minor savegame version, DO NOT USE!
|
|
std::string _savegame_format; ///< how to compress savegames
|
|
bool _do_autosave; ///< are we doing an autosave at the moment?
|
|
|
|
extern bool _sl_is_ext_version;
|
|
extern bool _sl_maybe_springpp;
|
|
extern bool _sl_maybe_chillpp;
|
|
extern bool _sl_upstream_mode;
|
|
|
|
namespace upstream_sl {
|
|
void SlNullPointers();
|
|
void SlLoadChunks();
|
|
void SlLoadCheckChunks();
|
|
void SlFixPointers();
|
|
}
|
|
|
|
/** What are we currently doing? */
|
|
enum SaveLoadAction {
|
|
SLA_LOAD, ///< loading
|
|
SLA_SAVE, ///< saving
|
|
SLA_PTRS, ///< fixing pointers
|
|
SLA_NULL, ///< null all pointers (on loading error)
|
|
SLA_LOAD_CHECK, ///< partial loading into #_load_check_data
|
|
};
|
|
|
|
enum NeedLength {
|
|
NL_NONE = 0, ///< not working in NeedLength mode
|
|
NL_WANTLENGTH = 1, ///< writing length and data
|
|
};
|
|
|
|
void ReadBuffer::SkipBytesSlowPath(size_t bytes)
|
|
{
|
|
bytes -= (this->bufe - this->bufp);
|
|
while (true) {
|
|
size_t len = this->reader->Read(this->buf, lengthof(this->buf));
|
|
if (len == 0) SlErrorCorrupt("Unexpected end of chunk");
|
|
this->read += len;
|
|
if (len >= bytes) {
|
|
this->bufp = this->buf + bytes;
|
|
this->bufe = this->buf + len;
|
|
return;
|
|
} else {
|
|
bytes -= len;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ReadBuffer::AcquireBytes()
|
|
{
|
|
size_t remainder = this->bufe - this->bufp;
|
|
if (remainder) {
|
|
memmove(this->buf, this->bufp, remainder);
|
|
}
|
|
size_t len = this->reader->Read(this->buf + remainder, lengthof(this->buf) - remainder);
|
|
if (len == 0) SlErrorCorrupt("Unexpected end of chunk");
|
|
|
|
this->read += len;
|
|
this->bufp = this->buf;
|
|
this->bufe = this->buf + remainder + len;
|
|
}
|
|
|
|
void MemoryDumper::FinaliseBlock()
|
|
{
|
|
assert(this->saved_buf == nullptr);
|
|
if (!this->blocks.empty()) {
|
|
size_t s = MEMORY_CHUNK_SIZE - (this->bufe - this->buf);
|
|
this->blocks.back().size = s;
|
|
this->completed_block_bytes += s;
|
|
}
|
|
this->buf = this->bufe = nullptr;
|
|
}
|
|
|
|
void MemoryDumper::AllocateBuffer()
|
|
{
|
|
if (this->saved_buf) {
|
|
const size_t offset = this->buf - this->autolen_buf;
|
|
const size_t size = (this->autolen_buf_end - this->autolen_buf) * 2;
|
|
this->autolen_buf = ReallocT<byte>(this->autolen_buf, size);
|
|
this->autolen_buf_end = this->autolen_buf + size;
|
|
this->buf = this->autolen_buf + offset;
|
|
this->bufe = this->autolen_buf_end;
|
|
return;
|
|
}
|
|
this->FinaliseBlock();
|
|
this->buf = MallocT<byte>(MEMORY_CHUNK_SIZE);
|
|
this->blocks.emplace_back(this->buf);
|
|
this->bufe = this->buf + MEMORY_CHUNK_SIZE;
|
|
}
|
|
|
|
/**
|
|
* Flush this dumper into a writer.
|
|
* @param writer The filter we want to use.
|
|
*/
|
|
void MemoryDumper::Flush(SaveFilter *writer)
|
|
{
|
|
this->FinaliseBlock();
|
|
|
|
size_t block_count = this->blocks.size();
|
|
for (size_t i = 0; i < block_count; i++) {
|
|
writer->Write(this->blocks[i].data, this->blocks[i].size);
|
|
}
|
|
|
|
writer->Finish();
|
|
}
|
|
|
|
void MemoryDumper::StartAutoLength()
|
|
{
|
|
assert(this->saved_buf == nullptr);
|
|
|
|
this->saved_buf = this->buf;
|
|
this->saved_bufe = this->bufe;
|
|
this->buf = this->autolen_buf;
|
|
this->bufe = this->autolen_buf_end;
|
|
}
|
|
|
|
std::pair<byte *, size_t> MemoryDumper::StopAutoLength()
|
|
{
|
|
assert(this->saved_buf != nullptr);
|
|
auto res = std::make_pair(this->autolen_buf, this->buf - this->autolen_buf);
|
|
|
|
this->buf = this->saved_buf;
|
|
this->bufe = this->saved_bufe;
|
|
this->saved_buf = this->saved_bufe = nullptr;
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Get the size of the memory dump made so far.
|
|
* @return The size.
|
|
*/
|
|
size_t MemoryDumper::GetSize() const
|
|
{
|
|
assert(this->saved_buf == nullptr);
|
|
return this->completed_block_bytes + (this->bufe ? (MEMORY_CHUNK_SIZE - (this->bufe - this->buf)) : 0);
|
|
}
|
|
|
|
/** The saveload struct, containing reader-writer functions, buffer, version, etc. */
|
|
struct SaveLoadParams {
|
|
SaveLoadAction action; ///< are we doing a save or a load atm.
|
|
NeedLength need_length; ///< working in NeedLength (Autolength) mode?
|
|
byte block_mode; ///< ???
|
|
bool error; ///< did an error occur or not
|
|
|
|
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
|
|
|
|
MemoryDumper *dumper; ///< Memory dumper to write the savegame to.
|
|
SaveFilter *sf; ///< Filter to write the savegame to.
|
|
|
|
ReadBuffer *reader; ///< Savegame reading buffer.
|
|
LoadFilter *lf; ///< Filter to read the savegame from.
|
|
|
|
StringID error_str; ///< the translatable error message to show
|
|
char *extra_msg; ///< the error message
|
|
|
|
uint16 game_speed; ///< The game speed when saving started.
|
|
bool saveinprogress; ///< Whether there is currently a save in progress.
|
|
SaveModeFlags save_flags; ///< Save mode flags
|
|
};
|
|
|
|
static SaveLoadParams _sl; ///< Parameters used for/at saveload.
|
|
|
|
ReadBuffer *ReadBuffer::GetCurrent()
|
|
{
|
|
return _sl.reader;
|
|
}
|
|
|
|
MemoryDumper *MemoryDumper::GetCurrent()
|
|
{
|
|
return _sl.dumper;
|
|
}
|
|
|
|
static const std::vector<ChunkHandler> &ChunkHandlers()
|
|
{
|
|
/* These define the chunks */
|
|
extern const ChunkHandlerTable _version_ext_chunk_handlers;
|
|
extern const ChunkHandlerTable _gamelog_chunk_handlers;
|
|
extern const ChunkHandlerTable _map_chunk_handlers;
|
|
extern const ChunkHandlerTable _misc_chunk_handlers;
|
|
extern const ChunkHandlerTable _name_chunk_handlers;
|
|
extern const ChunkHandlerTable _cheat_chunk_handlers;
|
|
extern const ChunkHandlerTable _setting_chunk_handlers;
|
|
extern const ChunkHandlerTable _company_chunk_handlers;
|
|
extern const ChunkHandlerTable _engine_chunk_handlers;
|
|
extern const ChunkHandlerTable _veh_chunk_handlers;
|
|
extern const ChunkHandlerTable _waypoint_chunk_handlers;
|
|
extern const ChunkHandlerTable _depot_chunk_handlers;
|
|
extern const ChunkHandlerTable _order_chunk_handlers;
|
|
extern const ChunkHandlerTable _town_chunk_handlers;
|
|
extern const ChunkHandlerTable _sign_chunk_handlers;
|
|
extern const ChunkHandlerTable _station_chunk_handlers;
|
|
extern const ChunkHandlerTable _industry_chunk_handlers;
|
|
extern const ChunkHandlerTable _economy_chunk_handlers;
|
|
extern const ChunkHandlerTable _subsidy_chunk_handlers;
|
|
extern const ChunkHandlerTable _cargomonitor_chunk_handlers;
|
|
extern const ChunkHandlerTable _goal_chunk_handlers;
|
|
extern const ChunkHandlerTable _story_page_chunk_handlers;
|
|
extern const ChunkHandlerTable _ai_chunk_handlers;
|
|
extern const ChunkHandlerTable _game_chunk_handlers;
|
|
extern const ChunkHandlerTable _animated_tile_chunk_handlers;
|
|
extern const ChunkHandlerTable _newgrf_chunk_handlers;
|
|
extern const ChunkHandlerTable _group_chunk_handlers;
|
|
extern const ChunkHandlerTable _cargopacket_chunk_handlers;
|
|
extern const ChunkHandlerTable _autoreplace_chunk_handlers;
|
|
extern const ChunkHandlerTable _labelmaps_chunk_handlers;
|
|
extern const ChunkHandlerTable _linkgraph_chunk_handlers;
|
|
extern const ChunkHandlerTable _airport_chunk_handlers;
|
|
extern const ChunkHandlerTable _object_chunk_handlers;
|
|
extern const ChunkHandlerTable _persistent_storage_chunk_handlers;
|
|
extern const ChunkHandlerTable _trace_restrict_chunk_handlers;
|
|
extern const ChunkHandlerTable _signal_chunk_handlers;
|
|
extern const ChunkHandlerTable _plan_chunk_handlers;
|
|
extern const ChunkHandlerTable _template_replacement_chunk_handlers;
|
|
extern const ChunkHandlerTable _template_vehicle_chunk_handlers;
|
|
extern const ChunkHandlerTable _bridge_signal_chunk_handlers;
|
|
extern const ChunkHandlerTable _tunnel_chunk_handlers;
|
|
extern const ChunkHandlerTable _train_speed_adaptation_chunk_handlers;
|
|
extern const ChunkHandlerTable _debug_chunk_handlers;
|
|
|
|
/** List of all chunks in a savegame. */
|
|
static const ChunkHandlerTable _chunk_handler_tables[] = {
|
|
_version_ext_chunk_handlers,
|
|
_gamelog_chunk_handlers,
|
|
_map_chunk_handlers,
|
|
_misc_chunk_handlers,
|
|
_name_chunk_handlers,
|
|
_cheat_chunk_handlers,
|
|
_setting_chunk_handlers,
|
|
_veh_chunk_handlers,
|
|
_waypoint_chunk_handlers,
|
|
_depot_chunk_handlers,
|
|
_order_chunk_handlers,
|
|
_industry_chunk_handlers,
|
|
_economy_chunk_handlers,
|
|
_subsidy_chunk_handlers,
|
|
_cargomonitor_chunk_handlers,
|
|
_goal_chunk_handlers,
|
|
_story_page_chunk_handlers,
|
|
_engine_chunk_handlers,
|
|
_town_chunk_handlers,
|
|
_sign_chunk_handlers,
|
|
_station_chunk_handlers,
|
|
_company_chunk_handlers,
|
|
_ai_chunk_handlers,
|
|
_game_chunk_handlers,
|
|
_animated_tile_chunk_handlers,
|
|
_newgrf_chunk_handlers,
|
|
_group_chunk_handlers,
|
|
_cargopacket_chunk_handlers,
|
|
_autoreplace_chunk_handlers,
|
|
_labelmaps_chunk_handlers,
|
|
_linkgraph_chunk_handlers,
|
|
_airport_chunk_handlers,
|
|
_object_chunk_handlers,
|
|
_persistent_storage_chunk_handlers,
|
|
_trace_restrict_chunk_handlers,
|
|
_signal_chunk_handlers,
|
|
_plan_chunk_handlers,
|
|
_template_replacement_chunk_handlers,
|
|
_template_vehicle_chunk_handlers,
|
|
_bridge_signal_chunk_handlers,
|
|
_tunnel_chunk_handlers,
|
|
_train_speed_adaptation_chunk_handlers,
|
|
_debug_chunk_handlers,
|
|
};
|
|
|
|
static std::vector<ChunkHandler> _chunk_handlers;
|
|
|
|
if (_chunk_handlers.empty()) {
|
|
for (auto &chunk_handler_table : _chunk_handler_tables) {
|
|
for (auto &chunk_handler : chunk_handler_table) {
|
|
_chunk_handlers.push_back(chunk_handler);
|
|
}
|
|
}
|
|
}
|
|
|
|
return _chunk_handlers;
|
|
}
|
|
|
|
/** Null all pointers (convert index -> nullptr) */
|
|
static void SlNullPointers()
|
|
{
|
|
if (_sl_upstream_mode) {
|
|
upstream_sl::SlNullPointers();
|
|
return;
|
|
}
|
|
|
|
_sl.action = SLA_NULL;
|
|
|
|
/* We don't want any savegame conversion code to run
|
|
* during NULLing; especially those that try to get
|
|
* pointers from other pools. */
|
|
_sl_version = SAVEGAME_VERSION;
|
|
SlXvSetCurrentState();
|
|
|
|
for (auto &ch : ChunkHandlers()) {
|
|
if (ch.ptrs_proc != nullptr) {
|
|
DEBUG(sl, 3, "Nulling pointers for %c%c%c%c", ch.id >> 24, ch.id >> 16, ch.id >> 8, ch.id);
|
|
ch.ptrs_proc();
|
|
}
|
|
}
|
|
|
|
assert(_sl.action == SLA_NULL);
|
|
}
|
|
|
|
struct ThreadSlErrorException {
|
|
StringID string;
|
|
const char *extra_msg;
|
|
};
|
|
|
|
/**
|
|
* Error handler. Sets everything up to show an error message and to clean
|
|
* up the mess of a partial savegame load.
|
|
* @param string The translatable error message to show.
|
|
* @param extra_msg An extra error message coming from one of the APIs.
|
|
* @note This function does never return as it throws an exception to
|
|
* break out of all the saveload code.
|
|
*/
|
|
void NORETURN SlError(StringID string, const char *extra_msg, bool already_malloced)
|
|
{
|
|
char *str = nullptr;
|
|
if (extra_msg != nullptr) {
|
|
str = already_malloced ? const_cast<char *>(extra_msg) : stredup(extra_msg);
|
|
}
|
|
|
|
if (IsNonMainThread() && IsNonGameThread() && _sl.action != SLA_SAVE) {
|
|
throw ThreadSlErrorException{ string, extra_msg };
|
|
}
|
|
|
|
/* Distinguish between loading into _load_check_data vs. normal save/load. */
|
|
if (_sl.action == SLA_LOAD_CHECK) {
|
|
_load_check_data.error = string;
|
|
free(_load_check_data.error_data);
|
|
_load_check_data.error_data = str;
|
|
} else {
|
|
_sl.error_str = string;
|
|
free(_sl.extra_msg);
|
|
_sl.extra_msg = str;
|
|
}
|
|
|
|
/* We have to nullptr all pointers here; we might be in a state where
|
|
* the pointers are actually filled with indices, which means that
|
|
* when we access them during cleaning the pool dereferences of
|
|
* those indices will be made with segmentation faults as result. */
|
|
if (_sl.action == SLA_LOAD || _sl.action == SLA_PTRS) SlNullPointers();
|
|
|
|
/* Logging could be active. */
|
|
GamelogStopAnyAction();
|
|
|
|
throw std::exception();
|
|
}
|
|
|
|
/**
|
|
* As SlError, except that it takes a format string and additional parameters
|
|
*/
|
|
void NORETURN CDECL SlErrorFmt(StringID string, const char *msg, ...)
|
|
{
|
|
va_list va;
|
|
va_start(va, msg);
|
|
char *str = str_vfmt(msg, va);
|
|
va_end(va);
|
|
SlError(string, str, true);
|
|
}
|
|
|
|
/**
|
|
* Error handler for corrupt savegames. Sets everything up to show the
|
|
* error message and to clean up the mess of a partial savegame load.
|
|
* @param msg Location the corruption has been spotted.
|
|
* @note This function does never return as it throws an exception to
|
|
* break out of all the saveload code.
|
|
*/
|
|
void NORETURN SlErrorCorrupt(const char *msg, bool already_malloced)
|
|
{
|
|
SlError(STR_GAME_SAVELOAD_ERROR_BROKEN_SAVEGAME, msg, already_malloced);
|
|
}
|
|
|
|
/**
|
|
* Issue an SlErrorCorrupt with a format string.
|
|
* @param format format string
|
|
* @param ... arguments to format string
|
|
* @note This function does never return as it throws an exception to
|
|
* break out of all the saveload code.
|
|
*/
|
|
void NORETURN CDECL SlErrorCorruptFmt(const char *format, ...)
|
|
{
|
|
va_list va;
|
|
va_start(va, format);
|
|
char *str = str_vfmt(format, va);
|
|
va_end(va);
|
|
SlError(STR_GAME_SAVELOAD_ERROR_BROKEN_SAVEGAME, str, true);
|
|
}
|
|
|
|
typedef void (*AsyncSaveFinishProc)(); ///< Callback for when the savegame loading is finished.
|
|
static std::atomic<AsyncSaveFinishProc> _async_save_finish; ///< Callback to call when the savegame loading is finished.
|
|
static std::thread _save_thread; ///< The thread we're using to compress and write a savegame
|
|
|
|
/**
|
|
* Called by save thread to tell we finished saving.
|
|
* @param proc The callback to call when saving is done.
|
|
*/
|
|
static void SetAsyncSaveFinish(AsyncSaveFinishProc proc)
|
|
{
|
|
if (_exit_game) return;
|
|
while (_async_save_finish.load(std::memory_order_acquire) != nullptr) CSleep(10);
|
|
|
|
_async_save_finish.store(proc, std::memory_order_release);
|
|
}
|
|
|
|
/**
|
|
* Handle async save finishes.
|
|
*/
|
|
void ProcessAsyncSaveFinish()
|
|
{
|
|
AsyncSaveFinishProc proc = _async_save_finish.exchange(nullptr, std::memory_order_acq_rel);
|
|
if (proc == nullptr) return;
|
|
|
|
proc();
|
|
|
|
if (_save_thread.joinable()) {
|
|
_save_thread.join();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wrapper for reading a byte from the buffer.
|
|
* @return The read byte.
|
|
*/
|
|
byte SlReadByte()
|
|
{
|
|
return _sl.reader->ReadByte();
|
|
}
|
|
|
|
/**
|
|
* Read in bytes from the file/data structure but don't do
|
|
* anything with them, discarding them in effect
|
|
* @param length The amount of bytes that is being treated this way
|
|
*/
|
|
void SlSkipBytes(size_t length)
|
|
{
|
|
return _sl.reader->SkipBytes(length);
|
|
}
|
|
|
|
int SlReadUint16()
|
|
{
|
|
_sl.reader->CheckBytes(2);
|
|
return _sl.reader->RawReadUint16();
|
|
}
|
|
|
|
uint32 SlReadUint32()
|
|
{
|
|
_sl.reader->CheckBytes(4);
|
|
return _sl.reader->RawReadUint32();
|
|
}
|
|
|
|
uint64 SlReadUint64()
|
|
{
|
|
_sl.reader->CheckBytes(8);
|
|
return _sl.reader->RawReadUint64();
|
|
}
|
|
|
|
/**
|
|
* Wrapper for writing a byte to the dumper.
|
|
* @param b The byte to write.
|
|
*/
|
|
void SlWriteByte(byte b)
|
|
{
|
|
_sl.dumper->WriteByte(b);
|
|
}
|
|
|
|
void SlWriteUint16(uint16 v)
|
|
{
|
|
_sl.dumper->CheckBytes(2);
|
|
_sl.dumper->RawWriteUint16(v);
|
|
}
|
|
|
|
void SlWriteUint32(uint32 v)
|
|
{
|
|
_sl.dumper->CheckBytes(4);
|
|
_sl.dumper->RawWriteUint32(v);
|
|
}
|
|
|
|
void SlWriteUint64(uint64 v)
|
|
{
|
|
_sl.dumper->CheckBytes(8);
|
|
_sl.dumper->RawWriteUint64(v);
|
|
}
|
|
|
|
/**
|
|
* Returns number of bytes read so far
|
|
* May only be called during a load/load check action
|
|
*/
|
|
size_t SlGetBytesRead()
|
|
{
|
|
assert(_sl.action == SLA_LOAD || _sl.action == SLA_LOAD_CHECK);
|
|
return _sl.reader->GetSize();
|
|
}
|
|
|
|
/**
|
|
* Returns number of bytes written so far
|
|
* May only be called during a save action
|
|
*/
|
|
size_t SlGetBytesWritten()
|
|
{
|
|
assert(_sl.action == SLA_SAVE);
|
|
return _sl.dumper->GetSize();
|
|
}
|
|
|
|
/**
|
|
* Read in the header descriptor of an object or an array.
|
|
* If the highest bit is set (7), then the index is bigger than 127
|
|
* elements, so use the next byte to read in the real value.
|
|
* The actual value is then both bytes added with the first shifted
|
|
* 8 bits to the left, and dropping the highest bit (which only indicated a big index).
|
|
* x = ((x & 0x7F) << 8) + SlReadByte();
|
|
* @return Return the value of the index
|
|
*/
|
|
static uint SlReadSimpleGamma()
|
|
{
|
|
uint i = SlReadByte();
|
|
if (HasBit(i, 7)) {
|
|
i &= ~0x80;
|
|
if (HasBit(i, 6)) {
|
|
i &= ~0x40;
|
|
if (HasBit(i, 5)) {
|
|
i &= ~0x20;
|
|
if (HasBit(i, 4)) {
|
|
i &= ~0x10;
|
|
if (HasBit(i, 3)) {
|
|
SlErrorCorrupt("Unsupported gamma");
|
|
}
|
|
i = SlReadByte(); // 32 bits only.
|
|
}
|
|
i = (i << 8) | SlReadByte();
|
|
}
|
|
i = (i << 8) | SlReadByte();
|
|
}
|
|
i = (i << 8) | SlReadByte();
|
|
}
|
|
return i;
|
|
}
|
|
|
|
/**
|
|
* Write the header descriptor of an object or an array.
|
|
* If the element is bigger than 127, use 2 bytes for saving
|
|
* and use the highest byte of the first written one as a notice
|
|
* that the length consists of 2 bytes, etc.. like this:
|
|
* 0xxxxxxx
|
|
* 10xxxxxx xxxxxxxx
|
|
* 110xxxxx xxxxxxxx xxxxxxxx
|
|
* 1110xxxx xxxxxxxx xxxxxxxx xxxxxxxx
|
|
* 11110--- xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
|
|
* We could extend the scheme ad infinum to support arbitrarily
|
|
* large chunks, but as sizeof(size_t) == 4 is still very common
|
|
* we don't support anything above 32 bits. That's why in the last
|
|
* case the 3 most significant bits are unused.
|
|
* @param i Index being written
|
|
*/
|
|
|
|
static void SlWriteSimpleGamma(size_t i)
|
|
{
|
|
if (i >= (1 << 7)) {
|
|
if (i >= (1 << 14)) {
|
|
if (i >= (1 << 21)) {
|
|
if (i >= (1 << 28)) {
|
|
assert(i <= UINT32_MAX); // We can only support 32 bits for now.
|
|
SlWriteByte((byte)(0xF0));
|
|
SlWriteByte((byte)(i >> 24));
|
|
} else {
|
|
SlWriteByte((byte)(0xE0 | (i >> 24)));
|
|
}
|
|
SlWriteByte((byte)(i >> 16));
|
|
} else {
|
|
SlWriteByte((byte)(0xC0 | (i >> 16)));
|
|
}
|
|
SlWriteByte((byte)(i >> 8));
|
|
} else {
|
|
SlWriteByte((byte)(0x80 | (i >> 8)));
|
|
}
|
|
}
|
|
SlWriteByte((byte)i);
|
|
}
|
|
|
|
/** Return how many bytes used to encode a gamma value */
|
|
static inline uint SlGetGammaLength(size_t i)
|
|
{
|
|
return 1 + (i >= (1 << 7)) + (i >= (1 << 14)) + (i >= (1 << 21)) + (i >= (1 << 28));
|
|
}
|
|
|
|
static inline uint SlReadSparseIndex()
|
|
{
|
|
return SlReadSimpleGamma();
|
|
}
|
|
|
|
static inline void SlWriteSparseIndex(uint index)
|
|
{
|
|
SlWriteSimpleGamma(index);
|
|
}
|
|
|
|
static inline uint SlReadArrayLength()
|
|
{
|
|
return SlReadSimpleGamma();
|
|
}
|
|
|
|
static inline void SlWriteArrayLength(size_t length)
|
|
{
|
|
SlWriteSimpleGamma(length);
|
|
}
|
|
|
|
static inline uint SlGetArrayLength(size_t length)
|
|
{
|
|
return SlGetGammaLength(length);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @return Return the size of this type in bytes
|
|
*/
|
|
static inline uint SlCalcConvMemLen(VarType conv)
|
|
{
|
|
static const byte conv_mem_size[] = {1, 1, 1, 2, 2, 4, 4, 8, 8, 0};
|
|
|
|
switch (GetVarMemType(conv)) {
|
|
case SLE_VAR_STRB:
|
|
case SLE_VAR_STR:
|
|
case SLE_VAR_STRQ:
|
|
return SlReadArrayLength();
|
|
|
|
default:
|
|
uint8 type = GetVarMemType(conv) >> 4;
|
|
assert(type < lengthof(conv_mem_size));
|
|
return conv_mem_size[type];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the size in bytes of a certain type of normal/atomic variable
|
|
* as it appears in a saved game. See VarTypes
|
|
* @param conv VarType type of variable that is used for calculating the size
|
|
* @return Return the size of this type in bytes
|
|
*/
|
|
static inline byte SlCalcConvFileLen(VarType conv)
|
|
{
|
|
uint8 type = GetVarFileType(conv);
|
|
if (type == SLE_FILE_VEHORDERID) return SlXvIsFeaturePresent(XSLFI_MORE_VEHICLE_ORDERS) ? 2 : 1;
|
|
static const byte conv_file_size[] = {1, 1, 2, 2, 4, 4, 8, 8, 2};
|
|
assert(type < lengthof(conv_file_size));
|
|
return conv_file_size[type];
|
|
}
|
|
|
|
/** Return the size in bytes of a reference (pointer) */
|
|
static inline size_t SlCalcRefLen()
|
|
{
|
|
return IsSavegameVersionBefore(SLV_69) ? 2 : 4;
|
|
}
|
|
|
|
void SlSetArrayIndex(uint index)
|
|
{
|
|
_sl.need_length = NL_WANTLENGTH;
|
|
_sl.array_index = index;
|
|
}
|
|
|
|
static size_t _next_offs;
|
|
|
|
/**
|
|
* Iterate through the elements of an array and read the whole thing
|
|
* @return The index of the object, or -1 if we have reached the end of current block
|
|
*/
|
|
int SlIterateArray()
|
|
{
|
|
int index;
|
|
|
|
/* After reading in the whole array inside the loop
|
|
* we must have read in all the data, so we must be at end of current block. */
|
|
if (_next_offs != 0 && _sl.reader->GetSize() != _next_offs) {
|
|
DEBUG(sl, 1, "Invalid chunk size: " PRINTF_SIZE " != " PRINTF_SIZE, _sl.reader->GetSize(), _next_offs);
|
|
SlErrorCorrupt("Invalid chunk size");
|
|
}
|
|
|
|
for (;;) {
|
|
uint length = SlReadArrayLength();
|
|
if (length == 0) {
|
|
_next_offs = 0;
|
|
return -1;
|
|
}
|
|
|
|
_sl.obj_len = --length;
|
|
_next_offs = _sl.reader->GetSize() + length;
|
|
|
|
switch (_sl.block_mode) {
|
|
case CH_SPARSE_ARRAY: index = (int)SlReadSparseIndex(); break;
|
|
case CH_ARRAY: index = _sl.array_index++; break;
|
|
default:
|
|
DEBUG(sl, 0, "SlIterateArray error");
|
|
return -1; // error
|
|
}
|
|
|
|
if (length != 0) return index;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Skip an array or sparse array
|
|
*/
|
|
void SlSkipArray()
|
|
{
|
|
while (SlIterateArray() != -1) {
|
|
SlSkipBytes(_next_offs - _sl.reader->GetSize());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the length of either a RIFF object or the number of items in an array.
|
|
* This lets us load an object or an array of arbitrary size
|
|
* @param length The length of the sought object/array
|
|
*/
|
|
void SlSetLength(size_t length)
|
|
{
|
|
assert(_sl.action == SLA_SAVE);
|
|
|
|
switch (_sl.need_length) {
|
|
case NL_WANTLENGTH:
|
|
_sl.need_length = NL_NONE;
|
|
switch (_sl.block_mode) {
|
|
case CH_RIFF:
|
|
/* Ugly encoding of >16M RIFF chunks
|
|
* The lower 24 bits are normal
|
|
* The uppermost 4 bits are bits 24:27
|
|
*
|
|
* If we have more than 28 bits, use an extra uint32 and
|
|
* signal this using the extended chunk header */
|
|
#ifdef POINTER_IS_64BIT
|
|
assert(length < (1LL << 32));
|
|
#endif
|
|
if (length >= (1 << 28)) {
|
|
/* write out extended chunk header */
|
|
SlWriteByte(CH_EXT_HDR);
|
|
SlWriteUint32(static_cast<uint32>(SLCEHF_BIG_RIFF));
|
|
}
|
|
SlWriteUint32(static_cast<uint32>((length & 0xFFFFFF) | ((length >> 24) << 28)));
|
|
if (length >= (1 << 28)) {
|
|
SlWriteUint32(static_cast<uint32>(length >> 28));
|
|
}
|
|
break;
|
|
case CH_ARRAY:
|
|
assert(_sl.last_array_index <= _sl.array_index);
|
|
while (++_sl.last_array_index <= _sl.array_index) {
|
|
SlWriteArrayLength(1);
|
|
}
|
|
SlWriteArrayLength(length + 1);
|
|
break;
|
|
case CH_SPARSE_ARRAY:
|
|
SlWriteArrayLength(length + 1 + SlGetArrayLength(_sl.array_index)); // Also include length of sparse index.
|
|
SlWriteSparseIndex(_sl.array_index);
|
|
break;
|
|
default: NOT_REACHED();
|
|
}
|
|
break;
|
|
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save/Load bytes. These do not need to be converted to Little/Big Endian
|
|
* so directly write them or read them to/from file
|
|
* @param ptr The source or destination of the object being manipulated
|
|
* @param length number of bytes this fast CopyBytes lasts
|
|
*/
|
|
static void SlCopyBytes(void *ptr, size_t length)
|
|
{
|
|
byte *p = (byte *)ptr;
|
|
|
|
switch (_sl.action) {
|
|
case SLA_LOAD_CHECK:
|
|
case SLA_LOAD:
|
|
_sl.reader->CopyBytes(p, length);
|
|
break;
|
|
case SLA_SAVE:
|
|
_sl.dumper->CopyBytes(p, length);
|
|
break;
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/** Get the length of the current object */
|
|
size_t SlGetFieldLength()
|
|
{
|
|
return _sl.obj_len;
|
|
}
|
|
|
|
/**
|
|
* Return a signed-long version of the value of a setting
|
|
* @param ptr pointer to the variable
|
|
* @param conv type of variable, can be a non-clean
|
|
* type, eg one with other flags because it is parsed
|
|
* @return returns the value of the pointer-setting
|
|
*/
|
|
int64 ReadValue(const void *ptr, VarType conv)
|
|
{
|
|
switch (GetVarMemType(conv)) {
|
|
case SLE_VAR_BL: return (*(const bool *)ptr != 0);
|
|
case SLE_VAR_I8: return *(const int8 *)ptr;
|
|
case SLE_VAR_U8: return *(const byte *)ptr;
|
|
case SLE_VAR_I16: return *(const int16 *)ptr;
|
|
case SLE_VAR_U16: return *(const uint16*)ptr;
|
|
case SLE_VAR_I32: return *(const int32 *)ptr;
|
|
case SLE_VAR_U32: return *(const uint32*)ptr;
|
|
case SLE_VAR_I64: return *(const int64 *)ptr;
|
|
case SLE_VAR_U64: return *(const uint64*)ptr;
|
|
case SLE_VAR_NULL:return 0;
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write the value of a setting
|
|
* @param ptr pointer to the variable
|
|
* @param conv type of variable, can be a non-clean type, eg
|
|
* with other flags. It is parsed upon read
|
|
* @param val the new value being given to the variable
|
|
*/
|
|
void WriteValue(void *ptr, VarType conv, int64 val)
|
|
{
|
|
switch (GetVarMemType(conv)) {
|
|
case SLE_VAR_BL: *(bool *)ptr = (val != 0); break;
|
|
case SLE_VAR_I8: *(int8 *)ptr = val; break;
|
|
case SLE_VAR_U8: *(byte *)ptr = val; break;
|
|
case SLE_VAR_I16: *(int16 *)ptr = val; break;
|
|
case SLE_VAR_U16: *(uint16*)ptr = val; break;
|
|
case SLE_VAR_I32: *(int32 *)ptr = val; break;
|
|
case SLE_VAR_U32: *(uint32*)ptr = val; break;
|
|
case SLE_VAR_I64: *(int64 *)ptr = val; break;
|
|
case SLE_VAR_U64: *(uint64*)ptr = val; break;
|
|
case SLE_VAR_NAME: *reinterpret_cast<std::string *>(ptr) = CopyFromOldName(val); break;
|
|
case SLE_VAR_CNAME: *(TinyString*)ptr = CopyFromOldName(val); break;
|
|
case SLE_VAR_NULL: break;
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle all conversion and typechecking of variables here.
|
|
* In the case of saving, read in the actual value from the struct
|
|
* and then write them to file, endian safely. Loading a value
|
|
* goes exactly the opposite way
|
|
* @param ptr The object being filled/read
|
|
* @param conv VarType type of the current element of the struct
|
|
*/
|
|
template <SaveLoadAction action>
|
|
static void SlSaveLoadConvGeneric(void *ptr, VarType conv)
|
|
{
|
|
switch (action) {
|
|
case SLA_SAVE: {
|
|
int64 x = ReadValue(ptr, conv);
|
|
|
|
/* Write the value to the file and check if its value is in the desired range */
|
|
switch (GetVarFileType(conv)) {
|
|
case SLE_FILE_I8: assert(x >= -128 && x <= 127); SlWriteByte(x);break;
|
|
case SLE_FILE_U8: assert(x >= 0 && x <= 255); SlWriteByte(x);break;
|
|
case SLE_FILE_I16:assert(x >= -32768 && x <= 32767); SlWriteUint16(x);break;
|
|
case SLE_FILE_STRINGID:
|
|
case SLE_FILE_VEHORDERID:
|
|
case SLE_FILE_U16:assert(x >= 0 && x <= 65535); SlWriteUint16(x);break;
|
|
case SLE_FILE_I32:
|
|
case SLE_FILE_U32: SlWriteUint32((uint32)x);break;
|
|
case SLE_FILE_I64:
|
|
case SLE_FILE_U64: SlWriteUint64(x);break;
|
|
default: NOT_REACHED();
|
|
}
|
|
break;
|
|
}
|
|
case SLA_LOAD_CHECK:
|
|
case SLA_LOAD: {
|
|
int64 x;
|
|
/* Read a value from the file */
|
|
switch (GetVarFileType(conv)) {
|
|
case SLE_FILE_I8: x = (int8 )SlReadByte(); break;
|
|
case SLE_FILE_U8: x = (byte )SlReadByte(); break;
|
|
case SLE_FILE_I16: x = (int16 )SlReadUint16(); break;
|
|
case SLE_FILE_U16: x = (uint16)SlReadUint16(); break;
|
|
case SLE_FILE_I32: x = (int32 )SlReadUint32(); break;
|
|
case SLE_FILE_U32: x = (uint32)SlReadUint32(); break;
|
|
case SLE_FILE_I64: x = (int64 )SlReadUint64(); break;
|
|
case SLE_FILE_U64: x = (uint64)SlReadUint64(); break;
|
|
case SLE_FILE_STRINGID: x = RemapOldStringID((uint16)SlReadUint16()); break;
|
|
case SLE_FILE_VEHORDERID:
|
|
if (SlXvIsFeaturePresent(XSLFI_MORE_VEHICLE_ORDERS)) {
|
|
x = (uint16)SlReadUint16();
|
|
} else {
|
|
VehicleOrderID id = (byte)SlReadByte();
|
|
x = (id == 0xFF) ? INVALID_VEH_ORDER_ID : id;
|
|
}
|
|
break;
|
|
default: NOT_REACHED();
|
|
}
|
|
|
|
/* Write The value to the struct. These ARE endian safe. */
|
|
WriteValue(ptr, conv, x);
|
|
break;
|
|
}
|
|
case SLA_PTRS: break;
|
|
case SLA_NULL: break;
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
void SlSaveLoadConv(void *ptr, VarType conv)
|
|
{
|
|
switch (_sl.action) {
|
|
case SLA_SAVE:
|
|
SlSaveLoadConvGeneric<SLA_SAVE>(ptr, conv);
|
|
return;
|
|
case SLA_LOAD_CHECK:
|
|
case SLA_LOAD:
|
|
SlSaveLoadConvGeneric<SLA_LOAD>(ptr, conv);
|
|
return;
|
|
case SLA_PTRS:
|
|
case SLA_NULL:
|
|
return;
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate the net length of a string. This is in almost all cases
|
|
* just strlen(), but if the string is not properly terminated, we'll
|
|
* resort to the maximum length of the buffer.
|
|
* @param ptr pointer to the stringbuffer
|
|
* @param length maximum length of the string (buffer). If -1 we don't care
|
|
* about a maximum length, but take string length as it is.
|
|
* @return return the net length of the string
|
|
*/
|
|
static inline size_t SlCalcNetStringLen(const char *ptr, size_t length)
|
|
{
|
|
if (ptr == nullptr) return 0;
|
|
return std::min(strlen(ptr), length - 1);
|
|
}
|
|
|
|
/**
|
|
* Calculate the gross length of the std::string that it
|
|
* will occupy in the savegame. This includes the real length,
|
|
* and the length that the index will occupy.
|
|
* @param str reference to the std::string
|
|
* @return return the gross length of the string
|
|
*/
|
|
static inline size_t SlCalcStdStrLen(const std::string &str)
|
|
{
|
|
return str.size() + SlGetArrayLength(str.size()); // also include the length of the index
|
|
}
|
|
|
|
/**
|
|
* Calculate the gross length of the string that it
|
|
* will occupy in the savegame. This includes the real length, returned
|
|
* by SlCalcNetStringLen and the length that the index will occupy.
|
|
* @param ptr pointer to the stringbuffer
|
|
* @param length maximum length of the string (buffer size, etc.)
|
|
* @param conv type of data been used
|
|
* @return return the gross length of the string
|
|
*/
|
|
static inline size_t SlCalcStringLen(const void *ptr, size_t length, VarType conv)
|
|
{
|
|
size_t len;
|
|
const char *str;
|
|
|
|
switch (GetVarMemType(conv)) {
|
|
default: NOT_REACHED();
|
|
case SLE_VAR_STR:
|
|
case SLE_VAR_STRQ:
|
|
str = *(const char * const *)ptr;
|
|
len = SIZE_MAX;
|
|
break;
|
|
case SLE_VAR_STRB:
|
|
str = (const char *)ptr;
|
|
len = length;
|
|
break;
|
|
}
|
|
|
|
len = SlCalcNetStringLen(str, len);
|
|
return len + SlGetArrayLength(len); // also include the length of the index
|
|
}
|
|
|
|
/**
|
|
* Save/Load a string.
|
|
* @param ptr the string being manipulated
|
|
* @param length of the string (full length)
|
|
* @param conv must be SLE_FILE_STRING
|
|
*/
|
|
static void SlString(void *ptr, size_t length, VarType conv)
|
|
{
|
|
switch (_sl.action) {
|
|
case SLA_SAVE: {
|
|
size_t len;
|
|
switch (GetVarMemType(conv)) {
|
|
default: NOT_REACHED();
|
|
case SLE_VAR_STRB:
|
|
len = SlCalcNetStringLen((char *)ptr, length);
|
|
break;
|
|
case SLE_VAR_STR:
|
|
case SLE_VAR_STRQ:
|
|
ptr = *(char **)ptr;
|
|
len = SlCalcNetStringLen((char *)ptr, SIZE_MAX);
|
|
break;
|
|
}
|
|
|
|
SlWriteArrayLength(len);
|
|
SlCopyBytes(ptr, len);
|
|
break;
|
|
}
|
|
case SLA_LOAD_CHECK:
|
|
case SLA_LOAD: {
|
|
size_t len = SlReadArrayLength();
|
|
|
|
switch (GetVarMemType(conv)) {
|
|
default: NOT_REACHED();
|
|
case SLE_VAR_NULL:
|
|
SlSkipBytes(len);
|
|
return;
|
|
case SLE_VAR_STRB:
|
|
if (len >= length) {
|
|
DEBUG(sl, 1, "String length in savegame is bigger than buffer, truncating");
|
|
SlCopyBytes(ptr, length);
|
|
SlSkipBytes(len - length);
|
|
len = length - 1;
|
|
} else {
|
|
SlCopyBytes(ptr, len);
|
|
}
|
|
break;
|
|
case SLE_VAR_STR:
|
|
case SLE_VAR_STRQ: // Malloc'd string, free previous incarnation, and allocate
|
|
free(*(char **)ptr);
|
|
if (len == 0) {
|
|
*(char **)ptr = nullptr;
|
|
return;
|
|
} else {
|
|
*(char **)ptr = MallocT<char>(len + 1); // terminating '\0'
|
|
ptr = *(char **)ptr;
|
|
SlCopyBytes(ptr, len);
|
|
}
|
|
break;
|
|
}
|
|
|
|
((char *)ptr)[len] = '\0'; // properly terminate the string
|
|
StringValidationSettings settings = SVS_REPLACE_WITH_QUESTION_MARK;
|
|
if ((conv & SLF_ALLOW_CONTROL) != 0) {
|
|
settings = settings | SVS_ALLOW_CONTROL_CODE;
|
|
if (IsSavegameVersionBefore(SLV_169)) {
|
|
str_fix_scc_encoded((char *)ptr, (char *)ptr + len);
|
|
}
|
|
}
|
|
if ((conv & SLF_ALLOW_NEWLINE) != 0) {
|
|
settings = settings | SVS_ALLOW_NEWLINE;
|
|
}
|
|
StrMakeValidInPlace((char *)ptr, (char *)ptr + len, settings);
|
|
break;
|
|
}
|
|
case SLA_PTRS: break;
|
|
case SLA_NULL: break;
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save/Load a \c std::string.
|
|
* @param ptr the string being manipulated
|
|
* @param conv must be SLE_FILE_STRING
|
|
*/
|
|
static void SlStdString(std::string &str, VarType conv)
|
|
{
|
|
switch (_sl.action) {
|
|
case SLA_SAVE: {
|
|
SlWriteArrayLength(str.size());
|
|
SlCopyBytes(str.data(), str.size());
|
|
break;
|
|
}
|
|
case SLA_LOAD_CHECK:
|
|
case SLA_LOAD: {
|
|
size_t len = SlReadArrayLength();
|
|
if (GetVarMemType(conv) == SLE_VAR_NULL) {
|
|
SlSkipBytes(len);
|
|
return;
|
|
}
|
|
|
|
str.resize(len);
|
|
SlCopyBytes(str.data(), len);
|
|
|
|
StringValidationSettings settings = SVS_REPLACE_WITH_QUESTION_MARK;
|
|
if ((conv & SLF_ALLOW_CONTROL) != 0) {
|
|
settings = settings | SVS_ALLOW_CONTROL_CODE;
|
|
if (IsSavegameVersionBefore(SLV_169)) {
|
|
char *buf = str.data();
|
|
str.resize(str_fix_scc_encoded(buf, buf + str.size()) - buf);
|
|
}
|
|
}
|
|
if ((conv & SLF_ALLOW_NEWLINE) != 0) {
|
|
settings = settings | SVS_ALLOW_NEWLINE;
|
|
}
|
|
StrMakeValidInPlace(str, settings);
|
|
break;
|
|
}
|
|
case SLA_PTRS: break;
|
|
case SLA_NULL: break;
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the size in bytes of a certain type of atomic array
|
|
* @param length The length of the array counted in elements
|
|
* @param conv VarType type of the variable that is used in calculating the size
|
|
*/
|
|
static inline size_t SlCalcArrayLen(size_t length, VarType conv)
|
|
{
|
|
return SlCalcConvFileLen(conv) * length;
|
|
}
|
|
|
|
/**
|
|
* Save/Load an array.
|
|
* @param array The array being manipulated
|
|
* @param length The length of the array in elements
|
|
* @param conv VarType type of the atomic array (int, byte, uint64, etc.)
|
|
*/
|
|
void SlArray(void *array, size_t length, VarType conv)
|
|
{
|
|
if (_sl.action == SLA_PTRS || _sl.action == SLA_NULL) return;
|
|
|
|
/* Automatically calculate the length? */
|
|
if (_sl.need_length != NL_NONE) {
|
|
SlSetLength(SlCalcArrayLen(length, conv));
|
|
}
|
|
|
|
/* NOTICE - handle some buggy stuff, in really old versions everything was saved
|
|
* as a byte-type. So detect this, and adjust array size accordingly */
|
|
if (_sl.action != SLA_SAVE && _sl_version == 0) {
|
|
/* all arrays except difficulty settings */
|
|
if (conv == SLE_INT16 || conv == SLE_UINT16 || conv == SLE_STRINGID ||
|
|
conv == SLE_INT32 || conv == SLE_UINT32) {
|
|
SlCopyBytes(array, length * SlCalcConvFileLen(conv));
|
|
return;
|
|
}
|
|
/* used for conversion of Money 32bit->64bit */
|
|
if (conv == (SLE_FILE_I32 | SLE_VAR_I64)) {
|
|
for (uint i = 0; i < length; i++) {
|
|
((int64*)array)[i] = (int32)BSWAP32(SlReadUint32());
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* If the size of elements is 1 byte both in file and memory, no special
|
|
* conversion is needed, use specialized copy-copy function to speed up things */
|
|
if (conv == SLE_INT8 || conv == SLE_UINT8) {
|
|
SlCopyBytes(array, length);
|
|
} else {
|
|
byte *a = (byte*)array;
|
|
byte mem_size = SlCalcConvMemLen(conv);
|
|
|
|
for (; length != 0; length --) {
|
|
SlSaveLoadConv(a, conv);
|
|
a += mem_size; // get size
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Pointers cannot be saved to a savegame, so this functions gets
|
|
* the index of the item, and if not available, it hussles with
|
|
* pointers (looks really bad :()
|
|
* Remember that a nullptr item has value 0, and all
|
|
* indices have +1, so vehicle 0 is saved as index 1.
|
|
* @param obj The object that we want to get the index of
|
|
* @param rt SLRefType type of the object the index is being sought of
|
|
* @return Return the pointer converted to an index of the type pointed to
|
|
*/
|
|
static size_t ReferenceToInt(const void *obj, SLRefType rt)
|
|
{
|
|
assert(_sl.action == SLA_SAVE);
|
|
|
|
if (obj == nullptr) return 0;
|
|
|
|
switch (rt) {
|
|
case REF_VEHICLE_OLD: // Old vehicles we save as new ones
|
|
case REF_VEHICLE: return ((const Vehicle*)obj)->index + 1;
|
|
case REF_TEMPLATE_VEHICLE: return ((const TemplateVehicle*)obj)->index + 1;
|
|
case REF_STATION: return ((const Station*)obj)->index + 1;
|
|
case REF_TOWN: return ((const Town*)obj)->index + 1;
|
|
case REF_ORDER: return ((const Order*)obj)->index + 1;
|
|
case REF_ROADSTOPS: return ((const RoadStop*)obj)->index + 1;
|
|
case REF_ENGINE_RENEWS: return ((const EngineRenew*)obj)->index + 1;
|
|
case REF_CARGO_PACKET: return ((const CargoPacket*)obj)->index + 1;
|
|
case REF_ORDERLIST: return ((const OrderList*)obj)->index + 1;
|
|
case REF_STORAGE: return ((const PersistentStorage*)obj)->index + 1;
|
|
case REF_LINK_GRAPH: return ((const LinkGraph*)obj)->index + 1;
|
|
case REF_LINK_GRAPH_JOB: return ((const LinkGraphJob*)obj)->index + 1;
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pointers cannot be loaded from a savegame, so this function
|
|
* gets the index from the savegame and returns the appropriate
|
|
* pointer from the already loaded base.
|
|
* Remember that an index of 0 is a nullptr pointer so all indices
|
|
* are +1 so vehicle 0 is saved as 1.
|
|
* @param index The index that is being converted to a pointer
|
|
* @param rt SLRefType type of the object the pointer is sought of
|
|
* @return Return the index converted to a pointer of any type
|
|
*/
|
|
static void *IntToReference(size_t index, SLRefType rt)
|
|
{
|
|
static_assert(sizeof(size_t) <= sizeof(void *));
|
|
|
|
assert(_sl.action == SLA_PTRS);
|
|
|
|
/* After version 4.3 REF_VEHICLE_OLD is saved as REF_VEHICLE,
|
|
* and should be loaded like that */
|
|
if (rt == REF_VEHICLE_OLD && !IsSavegameVersionBefore(SLV_4, 4)) {
|
|
rt = REF_VEHICLE;
|
|
}
|
|
|
|
/* No need to look up nullptr pointers, just return immediately */
|
|
if (index == (rt == REF_VEHICLE_OLD ? 0xFFFF : 0)) return nullptr;
|
|
|
|
/* Correct index. Old vehicles were saved differently:
|
|
* invalid vehicle was 0xFFFF, now we use 0x0000 for everything invalid. */
|
|
if (rt != REF_VEHICLE_OLD) index--;
|
|
|
|
switch (rt) {
|
|
case REF_ORDERLIST:
|
|
if (OrderList::IsValidID(index)) return OrderList::Get(index);
|
|
SlErrorCorrupt("Referencing invalid OrderList");
|
|
|
|
case REF_ORDER:
|
|
if (Order::IsValidID(index)) return Order::Get(index);
|
|
/* in old versions, invalid order was used to mark end of order list */
|
|
if (IsSavegameVersionBefore(SLV_5, 2)) return nullptr;
|
|
SlErrorCorrupt("Referencing invalid Order");
|
|
|
|
case REF_VEHICLE_OLD:
|
|
case REF_VEHICLE:
|
|
if (Vehicle::IsValidID(index)) return Vehicle::Get(index);
|
|
SlErrorCorrupt("Referencing invalid Vehicle");
|
|
|
|
case REF_TEMPLATE_VEHICLE:
|
|
if (TemplateVehicle::IsValidID(index)) return TemplateVehicle::Get(index);
|
|
SlErrorCorrupt("Referencing invalid TemplateVehicle");
|
|
|
|
case REF_STATION:
|
|
if (Station::IsValidID(index)) return Station::Get(index);
|
|
SlErrorCorrupt("Referencing invalid Station");
|
|
|
|
case REF_TOWN:
|
|
if (Town::IsValidID(index)) return Town::Get(index);
|
|
SlErrorCorrupt("Referencing invalid Town");
|
|
|
|
case REF_ROADSTOPS:
|
|
if (RoadStop::IsValidID(index)) return RoadStop::Get(index);
|
|
SlErrorCorrupt("Referencing invalid RoadStop");
|
|
|
|
case REF_ENGINE_RENEWS:
|
|
if (EngineRenew::IsValidID(index)) return EngineRenew::Get(index);
|
|
SlErrorCorrupt("Referencing invalid EngineRenew");
|
|
|
|
case REF_CARGO_PACKET:
|
|
if (CargoPacket::IsValidID(index)) return CargoPacket::Get(index);
|
|
SlErrorCorrupt("Referencing invalid CargoPacket");
|
|
|
|
case REF_STORAGE:
|
|
if (PersistentStorage::IsValidID(index)) return PersistentStorage::Get(index);
|
|
SlErrorCorrupt("Referencing invalid PersistentStorage");
|
|
|
|
case REF_LINK_GRAPH:
|
|
if (LinkGraph::IsValidID(index)) return LinkGraph::Get(index);
|
|
SlErrorCorrupt("Referencing invalid LinkGraph");
|
|
|
|
case REF_LINK_GRAPH_JOB:
|
|
if (LinkGraphJob::IsValidID(index)) return LinkGraphJob::Get(index);
|
|
SlErrorCorrupt("Referencing invalid LinkGraphJob");
|
|
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle conversion for references.
|
|
* @param ptr The object being filled/read.
|
|
* @param conv VarType type of the current element of the struct.
|
|
*/
|
|
void SlSaveLoadRef(void *ptr, VarType conv)
|
|
{
|
|
switch (_sl.action) {
|
|
case SLA_SAVE:
|
|
SlWriteUint32((uint32)ReferenceToInt(*(void **)ptr, (SLRefType)conv));
|
|
break;
|
|
case SLA_LOAD_CHECK:
|
|
case SLA_LOAD:
|
|
*(size_t *)ptr = IsSavegameVersionBefore(SLV_69) ? SlReadUint16() : SlReadUint32();
|
|
break;
|
|
case SLA_PTRS:
|
|
*(void **)ptr = IntToReference(*(size_t *)ptr, (SLRefType)conv);
|
|
break;
|
|
case SLA_NULL:
|
|
*(void **)ptr = nullptr;
|
|
break;
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Template class to help with list-like types.
|
|
*/
|
|
template <template<typename, typename> typename Tstorage, typename Tvar, typename Tallocator = std::allocator<Tvar>>
|
|
class SlStorageHelper {
|
|
typedef Tstorage<Tvar, Tallocator> SlStorageT;
|
|
public:
|
|
/**
|
|
* Internal templated helper to return the size in bytes of a list-like type.
|
|
* @param storage The storage to find the size of
|
|
* @param conv VarType type of variable that is used for calculating the size
|
|
* @param cmd The SaveLoadType ware are saving/loading.
|
|
*/
|
|
static size_t SlCalcLen(const void *storage, VarType conv, SaveLoadType cmd = SL_VAR)
|
|
{
|
|
assert(cmd == SL_VAR || cmd == SL_REF);
|
|
|
|
const SlStorageT *list = static_cast<const SlStorageT *>(storage);
|
|
|
|
int type_size = SlCalcConvFileLen(SLE_FILE_U32); // Size of the length of the list.
|
|
int item_size = SlCalcConvFileLen(cmd == SL_VAR ? conv : (VarType)SLE_FILE_U32);
|
|
return list->size() * item_size + type_size;
|
|
}
|
|
|
|
static void SlSaveLoadMember(SaveLoadType cmd, Tvar *item, VarType conv)
|
|
{
|
|
switch (cmd) {
|
|
case SL_VAR: SlSaveLoadConv(item, conv); break;
|
|
case SL_REF: SlSaveLoadRef(item, conv); break;
|
|
default:
|
|
NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal templated helper to save/load a list-like type.
|
|
* @param storage The storage being manipulated.
|
|
* @param conv VarType type of variable that is used for calculating the size.
|
|
* @param cmd The SaveLoadType ware are saving/loading.
|
|
*/
|
|
static void SlSaveLoad(void *storage, VarType conv, SaveLoadType cmd = SL_VAR)
|
|
{
|
|
assert(cmd == SL_VAR || cmd == SL_REF);
|
|
|
|
SlStorageT *list = static_cast<SlStorageT *>(storage);
|
|
|
|
switch (_sl.action) {
|
|
case SLA_SAVE:
|
|
SlWriteUint32((uint32)list->size());
|
|
|
|
for (auto &item : *list) {
|
|
SlSaveLoadMember(cmd, &item, conv);
|
|
}
|
|
break;
|
|
|
|
case SLA_LOAD_CHECK:
|
|
case SLA_LOAD: {
|
|
size_t length;
|
|
switch (cmd) {
|
|
case SL_VAR: length = SlReadUint32(); break;
|
|
case SL_REF: length = IsSavegameVersionBefore(SLV_69) ? SlReadUint16() : SlReadUint32(); break;
|
|
default: NOT_REACHED();
|
|
}
|
|
|
|
/* Load each value and push to the end of the storage. */
|
|
for (size_t i = 0; i < length; i++) {
|
|
Tvar &data = list->emplace_back();
|
|
SlSaveLoadMember(cmd, &data, conv);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case SLA_PTRS:
|
|
for (auto &item : *list) {
|
|
SlSaveLoadMember(cmd, &item, conv);
|
|
}
|
|
break;
|
|
|
|
case SLA_NULL:
|
|
list->clear();
|
|
break;
|
|
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Return the size in bytes of a list.
|
|
* @param list The std::list to find the size of.
|
|
* @param conv VarType type of variable that is used for calculating the size.
|
|
*/
|
|
template<typename PtrList>
|
|
static inline size_t SlCalcRefListLen(const void *list)
|
|
{
|
|
const PtrList *l = (const PtrList *) list;
|
|
|
|
int type_size = IsSavegameVersionBefore(SLV_69) ? 2 : 4;
|
|
/* Each entry is saved as type_size bytes, plus type_size bytes are used for the length
|
|
* of the list */
|
|
return l->size() * type_size + type_size;
|
|
}
|
|
|
|
/**
|
|
* Return the size in bytes of a list
|
|
* @param list The std::list to find the size of
|
|
*/
|
|
template<typename PtrList>
|
|
static inline size_t SlCalcVarListLen(const void *list, size_t item_size)
|
|
{
|
|
const PtrList *l = (const PtrList *) list;
|
|
/* Each entry is saved as item_size bytes, plus 4 bytes are used for the length
|
|
* of the list */
|
|
return l->size() * item_size + 4;
|
|
}
|
|
|
|
/**
|
|
* Save/Load a list.
|
|
* @param list The list being manipulated.
|
|
* @param conv VarType type of variable that is used for calculating the size.
|
|
*/
|
|
template<typename PtrList>
|
|
static void SlRefList(void *list, SLRefType conv)
|
|
{
|
|
/* Automatically calculate the length? */
|
|
if (_sl.need_length != NL_NONE) {
|
|
SlSetLength(SlCalcRefListLen<PtrList>(list));
|
|
}
|
|
|
|
PtrList *l = (PtrList *)list;
|
|
|
|
switch (_sl.action) {
|
|
case SLA_SAVE: {
|
|
SlWriteUint32((uint32)l->size());
|
|
|
|
typename PtrList::iterator iter;
|
|
for (iter = l->begin(); iter != l->end(); ++iter) {
|
|
void *ptr = *iter;
|
|
SlWriteUint32((uint32)ReferenceToInt(ptr, conv));
|
|
}
|
|
break;
|
|
}
|
|
case SLA_LOAD_CHECK:
|
|
case SLA_LOAD: {
|
|
size_t length = IsSavegameVersionBefore(SLV_69) ? SlReadUint16() : SlReadUint32();
|
|
|
|
/* Load each reference and push to the end of the list */
|
|
for (size_t i = 0; i < length; i++) {
|
|
size_t data = IsSavegameVersionBefore(SLV_69) ? SlReadUint16() : SlReadUint32();
|
|
l->push_back((void *)data);
|
|
}
|
|
break;
|
|
}
|
|
case SLA_PTRS: {
|
|
PtrList temp = *l;
|
|
|
|
l->clear();
|
|
typename PtrList::iterator iter;
|
|
for (iter = temp.begin(); iter != temp.end(); ++iter) {
|
|
void *ptr = IntToReference((size_t)*iter, conv);
|
|
l->push_back(ptr);
|
|
}
|
|
break;
|
|
}
|
|
case SLA_NULL:
|
|
l->clear();
|
|
break;
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save/Load a list.
|
|
* @param list The list being manipulated
|
|
* @param conv VarType type of the list
|
|
*/
|
|
template<typename PtrList>
|
|
static void SlVarList(void *list, VarType conv)
|
|
{
|
|
const size_t size_len = SlCalcConvMemLen(conv);
|
|
/* Automatically calculate the length? */
|
|
if (_sl.need_length != NL_NONE) {
|
|
SlSetLength(SlCalcVarListLen<PtrList>(list, size_len));
|
|
}
|
|
|
|
PtrList *l = (PtrList *)list;
|
|
|
|
switch (_sl.action) {
|
|
case SLA_SAVE: {
|
|
SlWriteUint32((uint32)l->size());
|
|
|
|
typename PtrList::iterator iter;
|
|
for (iter = l->begin(); iter != l->end(); ++iter) {
|
|
SlSaveLoadConv(&(*iter), conv);
|
|
}
|
|
break;
|
|
}
|
|
case SLA_LOAD_CHECK:
|
|
case SLA_LOAD: {
|
|
size_t length = SlReadUint32();
|
|
l->resize(length);
|
|
|
|
typename PtrList::iterator iter;
|
|
iter = l->begin();
|
|
|
|
for (size_t i = 0; i < length; i++) {
|
|
SlSaveLoadConv(&(*iter), conv);
|
|
++iter;
|
|
}
|
|
break;
|
|
}
|
|
case SLA_PTRS: break;
|
|
case SLA_NULL:
|
|
l->clear();
|
|
break;
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the size in bytes of a std::deque.
|
|
* @param deque The std::deque to find the size of
|
|
* @param conv VarType type of variable that is used for calculating the size
|
|
*/
|
|
static inline size_t SlCalcDequeLen(const void *deque, VarType conv)
|
|
{
|
|
switch (GetVarMemType(conv)) {
|
|
case SLE_VAR_BL: return SlStorageHelper<std::deque, bool>::SlCalcLen(deque, conv);
|
|
case SLE_VAR_I8: return SlStorageHelper<std::deque, int8>::SlCalcLen(deque, conv);
|
|
case SLE_VAR_U8: return SlStorageHelper<std::deque, uint8>::SlCalcLen(deque, conv);
|
|
case SLE_VAR_I16: return SlStorageHelper<std::deque, int16>::SlCalcLen(deque, conv);
|
|
case SLE_VAR_U16: return SlStorageHelper<std::deque, uint16>::SlCalcLen(deque, conv);
|
|
case SLE_VAR_I32: return SlStorageHelper<std::deque, int32>::SlCalcLen(deque, conv);
|
|
case SLE_VAR_U32: return SlStorageHelper<std::deque, uint32>::SlCalcLen(deque, conv);
|
|
case SLE_VAR_I64: return SlStorageHelper<std::deque, int64>::SlCalcLen(deque, conv);
|
|
case SLE_VAR_U64: return SlStorageHelper<std::deque, uint64>::SlCalcLen(deque, conv);
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save/load a std::deque.
|
|
* @param deque The std::deque being manipulated
|
|
* @param conv VarType type of variable that is used for calculating the size
|
|
*/
|
|
static void SlDeque(void *deque, VarType conv)
|
|
{
|
|
switch (GetVarMemType(conv)) {
|
|
case SLE_VAR_BL: SlStorageHelper<std::deque, bool>::SlSaveLoad(deque, conv); break;
|
|
case SLE_VAR_I8: SlStorageHelper<std::deque, int8>::SlSaveLoad(deque, conv); break;
|
|
case SLE_VAR_U8: SlStorageHelper<std::deque, uint8>::SlSaveLoad(deque, conv); break;
|
|
case SLE_VAR_I16: SlStorageHelper<std::deque, int16>::SlSaveLoad(deque, conv); break;
|
|
case SLE_VAR_U16: SlStorageHelper<std::deque, uint16>::SlSaveLoad(deque, conv); break;
|
|
case SLE_VAR_I32: SlStorageHelper<std::deque, int32>::SlSaveLoad(deque, conv); break;
|
|
case SLE_VAR_U32: SlStorageHelper<std::deque, uint32>::SlSaveLoad(deque, conv); break;
|
|
case SLE_VAR_I64: SlStorageHelper<std::deque, int64>::SlSaveLoad(deque, conv); break;
|
|
case SLE_VAR_U64: SlStorageHelper<std::deque, uint64>::SlSaveLoad(deque, conv); break;
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/** Are we going to save this object or not? */
|
|
static inline bool SlIsObjectValidInSavegame(const SaveLoad &sld)
|
|
{
|
|
return sld.ext_feature_test.IsFeaturePresent(_sl_version, sld.version_from, sld.version_to);
|
|
}
|
|
|
|
/**
|
|
* Calculate the size of an object.
|
|
* @param object to be measured.
|
|
* @param slt The SaveLoad table with objects to save/load.
|
|
* @return size of given object.
|
|
*/
|
|
size_t SlCalcObjLength(const void *object, const SaveLoadTable &slt)
|
|
{
|
|
size_t length = 0;
|
|
|
|
/* Need to determine the length and write a length tag. */
|
|
for (auto &sld : slt) {
|
|
length += SlCalcObjMemberLength(object, sld);
|
|
}
|
|
return length;
|
|
}
|
|
|
|
size_t SlCalcObjMemberLength(const void *object, const SaveLoad &sld)
|
|
{
|
|
assert(_sl.action == SLA_SAVE);
|
|
|
|
switch (sld.cmd) {
|
|
case SL_VAR:
|
|
case SL_REF:
|
|
case SL_ARR:
|
|
case SL_STR:
|
|
case SL_REFLIST:
|
|
case SL_PTRDEQ:
|
|
case SL_VEC:
|
|
case SL_DEQUE:
|
|
case SL_STDSTR:
|
|
case SL_VARVEC:
|
|
/* CONDITIONAL saveload types depend on the savegame version */
|
|
if (!SlIsObjectValidInSavegame(sld)) break;
|
|
|
|
switch (sld.cmd) {
|
|
case SL_VAR: return SlCalcConvFileLen(sld.conv);
|
|
case SL_REF: return SlCalcRefLen();
|
|
case SL_ARR: return SlCalcArrayLen(sld.length, sld.conv);
|
|
case SL_STR: return SlCalcStringLen(GetVariableAddress(object, sld), sld.length, sld.conv);
|
|
case SL_REFLIST: return SlCalcRefListLen<std::list<void *>>(GetVariableAddress(object, sld));
|
|
case SL_PTRDEQ: return SlCalcRefListLen<std::deque<void *>>(GetVariableAddress(object, sld));
|
|
case SL_VEC: return SlCalcRefListLen<std::vector<void *>>(GetVariableAddress(object, sld));
|
|
case SL_DEQUE: return SlCalcDequeLen(GetVariableAddress(object, sld), sld.conv);
|
|
case SL_VARVEC: {
|
|
const size_t size_len = SlCalcConvMemLen(sld.conv);
|
|
switch (size_len) {
|
|
case 1: return SlCalcVarListLen<std::vector<byte>>(GetVariableAddress(object, sld), 1);
|
|
case 2: return SlCalcVarListLen<std::vector<uint16>>(GetVariableAddress(object, sld), 2);
|
|
case 4: return SlCalcVarListLen<std::vector<uint32>>(GetVariableAddress(object, sld), 4);
|
|
case 8: return SlCalcVarListLen<std::vector<uint64>>(GetVariableAddress(object, sld), 8);
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
case SL_STDSTR: return SlCalcStdStrLen(*static_cast<std::string *>(GetVariableAddress(object, sld)));
|
|
default: NOT_REACHED();
|
|
}
|
|
break;
|
|
case SL_WRITEBYTE: return 1; // a byte is logically of size 1
|
|
case SL_VEH_INCLUDE: return SlCalcObjLength(object, GetVehicleDescription(VEH_END));
|
|
case SL_ST_INCLUDE: return SlCalcObjLength(object, GetBaseStationDescription());
|
|
default: NOT_REACHED();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Check whether the variable size of the variable in the saveload configuration
|
|
* matches with the actual variable size.
|
|
* @param sld The saveload configuration to test.
|
|
*/
|
|
[[maybe_unused]] static bool IsVariableSizeRight(const SaveLoad &sld)
|
|
{
|
|
if (GetVarMemType(sld.conv) == SLE_VAR_NULL) return true;
|
|
|
|
switch (sld.cmd) {
|
|
case SL_VAR:
|
|
switch (GetVarMemType(sld.conv)) {
|
|
case SLE_VAR_BL:
|
|
return sld.size == sizeof(bool);
|
|
case SLE_VAR_I8:
|
|
case SLE_VAR_U8:
|
|
return sld.size == sizeof(int8);
|
|
case SLE_VAR_I16:
|
|
case SLE_VAR_U16:
|
|
return sld.size == sizeof(int16);
|
|
case SLE_VAR_I32:
|
|
case SLE_VAR_U32:
|
|
return sld.size == sizeof(int32);
|
|
case SLE_VAR_I64:
|
|
case SLE_VAR_U64:
|
|
return sld.size == sizeof(int64);
|
|
case SLE_VAR_NAME:
|
|
return sld.size == sizeof(std::string);
|
|
default:
|
|
return sld.size == sizeof(void *);
|
|
}
|
|
case SL_REF:
|
|
/* These should all be pointer sized. */
|
|
return sld.size == sizeof(void *);
|
|
|
|
case SL_STR:
|
|
/* These should be pointer sized, or fixed array. */
|
|
return sld.size == sizeof(void *) || sld.size == sld.length;
|
|
|
|
case SL_STDSTR:
|
|
/* These should be all pointers to std::string. */
|
|
return sld.size == sizeof(std::string);
|
|
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void SlFilterObject(const SaveLoadTable &slt, std::vector<SaveLoad> &save);
|
|
|
|
static void SlFilterObjectMember(const SaveLoad &sld, std::vector<SaveLoad> &save)
|
|
{
|
|
assert(IsVariableSizeRight(sld));
|
|
|
|
switch (sld.cmd) {
|
|
case SL_VAR:
|
|
case SL_REF:
|
|
case SL_ARR:
|
|
case SL_STR:
|
|
case SL_REFLIST:
|
|
case SL_PTRDEQ:
|
|
case SL_VEC:
|
|
case SL_DEQUE:
|
|
case SL_STDSTR:
|
|
case SL_VARVEC:
|
|
/* CONDITIONAL saveload types depend on the savegame version */
|
|
if (!SlIsObjectValidInSavegame(sld)) return;
|
|
|
|
switch (_sl.action) {
|
|
case SLA_SAVE:
|
|
case SLA_LOAD_CHECK:
|
|
case SLA_LOAD:
|
|
break;
|
|
case SLA_PTRS:
|
|
case SLA_NULL:
|
|
switch (sld.cmd) {
|
|
case SL_REF:
|
|
case SL_REFLIST:
|
|
case SL_PTRDEQ:
|
|
case SL_VEC:
|
|
break;
|
|
|
|
/* non-ptr types do not require SLA_PTRS or SLA_NULL actions */
|
|
default:
|
|
return;
|
|
}
|
|
break;
|
|
default: NOT_REACHED();
|
|
}
|
|
|
|
save.push_back(sld);
|
|
break;
|
|
|
|
/* SL_WRITEBYTE writes a value to the savegame to identify the type of an object.
|
|
* When loading, the value is read explictly with SlReadByte() to determine which
|
|
* object description to use. */
|
|
case SL_WRITEBYTE:
|
|
if (_sl.action == SLA_SAVE) save.push_back(sld);
|
|
break;
|
|
|
|
/* SL_VEH_INCLUDE loads common code for vehicles */
|
|
case SL_VEH_INCLUDE:
|
|
SlFilterObject(GetVehicleDescription(VEH_END), save);
|
|
break;
|
|
|
|
case SL_ST_INCLUDE:
|
|
SlFilterObject(GetBaseStationDescription(), save);
|
|
break;
|
|
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
void SlFilterObject(const SaveLoadTable &slt, std::vector<SaveLoad> &save)
|
|
{
|
|
for (auto &sld : slt) {
|
|
SlFilterObjectMember(sld, save);
|
|
}
|
|
}
|
|
|
|
std::vector<SaveLoad> SlFilterObject(const SaveLoadTable &slt)
|
|
{
|
|
std::vector<SaveLoad> save;
|
|
SlFilterObject(slt, save);
|
|
return save;
|
|
}
|
|
|
|
template <SaveLoadAction action, bool check_version>
|
|
bool SlObjectMemberGeneric(void *object, const SaveLoad &sld)
|
|
{
|
|
void *ptr = GetVariableAddress(object, sld);
|
|
|
|
if (check_version) assert(IsVariableSizeRight(sld));
|
|
|
|
VarType conv = GB(sld.conv, 0, 8);
|
|
switch (sld.cmd) {
|
|
case SL_VAR:
|
|
case SL_REF:
|
|
case SL_ARR:
|
|
case SL_STR:
|
|
case SL_REFLIST:
|
|
case SL_PTRDEQ:
|
|
case SL_VEC:
|
|
case SL_DEQUE:
|
|
case SL_STDSTR:
|
|
case SL_VARVEC:
|
|
/* CONDITIONAL saveload types depend on the savegame version */
|
|
if (check_version) {
|
|
if (!SlIsObjectValidInSavegame(sld)) return false;
|
|
}
|
|
|
|
switch (sld.cmd) {
|
|
case SL_VAR: SlSaveLoadConvGeneric<action>(ptr, conv); break;
|
|
case SL_REF: // Reference variable, translate
|
|
switch (action) {
|
|
case SLA_SAVE:
|
|
SlWriteUint32((uint32)ReferenceToInt(*(void **)ptr, (SLRefType)conv));
|
|
break;
|
|
case SLA_LOAD_CHECK:
|
|
case SLA_LOAD:
|
|
*(size_t *)ptr = IsSavegameVersionBefore(SLV_69) ? SlReadUint16() : SlReadUint32();
|
|
break;
|
|
case SLA_PTRS:
|
|
*(void **)ptr = IntToReference(*(size_t *)ptr, (SLRefType)conv);
|
|
break;
|
|
case SLA_NULL:
|
|
*(void **)ptr = nullptr;
|
|
break;
|
|
default: NOT_REACHED();
|
|
}
|
|
break;
|
|
case SL_ARR: SlArray(ptr, sld.length, conv); break;
|
|
case SL_STR: SlString(ptr, sld.length, sld.conv); break;
|
|
case SL_REFLIST: SlRefList<std::list<void *>>(ptr, (SLRefType)conv); break;
|
|
case SL_PTRDEQ: SlRefList<std::deque<void *>>(ptr, (SLRefType)conv); break;
|
|
case SL_VEC: SlRefList<std::vector<void *>>(ptr, (SLRefType)conv); break;
|
|
case SL_DEQUE: SlDeque(ptr, conv); break;
|
|
case SL_VARVEC: {
|
|
const size_t size_len = SlCalcConvMemLen(sld.conv);
|
|
switch (size_len) {
|
|
case 1: SlVarList<std::vector<byte>>(ptr, conv); break;
|
|
case 2: SlVarList<std::vector<uint16>>(ptr, conv); break;
|
|
case 4: SlVarList<std::vector<uint32>>(ptr, conv); break;
|
|
case 8: SlVarList<std::vector<uint64>>(ptr, conv); break;
|
|
default: NOT_REACHED();
|
|
}
|
|
break;
|
|
}
|
|
case SL_STDSTR: SlStdString(*static_cast<std::string *>(ptr), sld.conv); break;
|
|
default: NOT_REACHED();
|
|
}
|
|
break;
|
|
|
|
/* SL_WRITEBYTE writes a value to the savegame to identify the type of an object.
|
|
* When loading, the value is read explicitly with SlReadByte() to determine which
|
|
* object description to use. */
|
|
case SL_WRITEBYTE:
|
|
switch (action) {
|
|
case SLA_SAVE: SlWriteByte(*(uint8 *)ptr); break;
|
|
case SLA_LOAD_CHECK:
|
|
case SLA_LOAD:
|
|
case SLA_PTRS:
|
|
case SLA_NULL: break;
|
|
default: NOT_REACHED();
|
|
}
|
|
break;
|
|
|
|
/* SL_VEH_INCLUDE loads common code for vehicles */
|
|
case SL_VEH_INCLUDE:
|
|
SlObject(ptr, GetVehicleDescription(VEH_END));
|
|
break;
|
|
|
|
case SL_ST_INCLUDE:
|
|
SlObject(ptr, GetBaseStationDescription());
|
|
break;
|
|
|
|
default: NOT_REACHED();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool SlObjectMember(void *object, const SaveLoad &sld)
|
|
{
|
|
switch (_sl.action) {
|
|
case SLA_SAVE:
|
|
return SlObjectMemberGeneric<SLA_SAVE, true>(object, sld);
|
|
case SLA_LOAD_CHECK:
|
|
case SLA_LOAD:
|
|
return SlObjectMemberGeneric<SLA_LOAD, true>(object, sld);
|
|
case SLA_PTRS:
|
|
return SlObjectMemberGeneric<SLA_PTRS, true>(object, sld);
|
|
case SLA_NULL:
|
|
return SlObjectMemberGeneric<SLA_NULL, true>(object, sld);
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main SaveLoad function.
|
|
* @param object The object that is being saved or loaded.
|
|
* @param slt The SaveLoad table with objects to save/load.
|
|
*/
|
|
void SlObject(void *object, const SaveLoadTable &slt)
|
|
{
|
|
/* Automatically calculate the length? */
|
|
if (_sl.need_length != NL_NONE) {
|
|
SlSetLength(SlCalcObjLength(object, slt));
|
|
}
|
|
|
|
for (auto &sld : slt) {
|
|
SlObjectMember(object, sld);
|
|
}
|
|
}
|
|
|
|
template <SaveLoadAction action, bool check_version>
|
|
void SlObjectIterateBase(void *object, const SaveLoadTable &slt)
|
|
{
|
|
for (auto &sld : slt) {
|
|
SlObjectMemberGeneric<action, check_version>(object, sld);
|
|
}
|
|
}
|
|
|
|
void SlObjectSaveFiltered(void *object, const SaveLoadTable &slt)
|
|
{
|
|
if (_sl.need_length != NL_NONE) {
|
|
_sl.need_length = NL_NONE;
|
|
_sl.dumper->StartAutoLength();
|
|
SlObjectIterateBase<SLA_SAVE, false>(object, slt);
|
|
auto result = _sl.dumper->StopAutoLength();
|
|
_sl.need_length = NL_WANTLENGTH;
|
|
SlSetLength(result.second);
|
|
_sl.dumper->CopyBytes(result.first, result.second);
|
|
} else {
|
|
SlObjectIterateBase<SLA_SAVE, false>(object, slt);
|
|
}
|
|
}
|
|
|
|
void SlObjectLoadFiltered(void *object, const SaveLoadTable &slt)
|
|
{
|
|
SlObjectIterateBase<SLA_LOAD, false>(object, slt);
|
|
}
|
|
|
|
void SlObjectPtrOrNullFiltered(void *object, const SaveLoadTable &slt)
|
|
{
|
|
switch (_sl.action) {
|
|
case SLA_PTRS:
|
|
SlObjectIterateBase<SLA_PTRS, false>(object, slt);
|
|
return;
|
|
case SLA_NULL:
|
|
SlObjectIterateBase<SLA_NULL, false>(object, slt);
|
|
return;
|
|
default: NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save or Load (a list of) global variables.
|
|
* @param slt The SaveLoad table with objects to save/load.
|
|
*/
|
|
void SlGlobList(const SaveLoadTable &slt)
|
|
{
|
|
SlObject(nullptr, slt);
|
|
}
|
|
|
|
/**
|
|
* Do something of which I have no idea what it is :P
|
|
* @param proc The callback procedure that is called
|
|
* @param arg The variable that will be used for the callback procedure
|
|
*/
|
|
void SlAutolength(AutolengthProc *proc, void *arg)
|
|
{
|
|
assert(_sl.action == SLA_SAVE);
|
|
assert(_sl.need_length == NL_WANTLENGTH);
|
|
|
|
_sl.need_length = NL_NONE;
|
|
_sl.dumper->StartAutoLength();
|
|
proc(arg);
|
|
auto result = _sl.dumper->StopAutoLength();
|
|
/* Setup length */
|
|
_sl.need_length = NL_WANTLENGTH;
|
|
SlSetLength(result.second);
|
|
_sl.dumper->CopyBytes(result.first, result.second);
|
|
}
|
|
|
|
/*
|
|
* Notes on extended chunk header:
|
|
*
|
|
* If the chunk type is CH_EXT_HDR (15), then a u32 flags field follows.
|
|
* This flag field may define additional fields which follow the flags field in future.
|
|
* The standard chunk header follows, though it my be modified by the flags field.
|
|
* At present SLCEHF_BIG_RIFF increases the RIFF size limit to a theoretical 60 bits,
|
|
* by adding a further u32 field for the high bits after the existing RIFF size field.
|
|
*/
|
|
|
|
inline void SlRIFFSpringPPCheck(size_t len)
|
|
{
|
|
if (_sl_maybe_springpp) {
|
|
_sl_maybe_springpp = false;
|
|
if (len == 0) {
|
|
extern void SlXvSpringPPSpecialSavegameVersions();
|
|
SlXvSpringPPSpecialSavegameVersions();
|
|
} else if (_sl_version > MAX_LOAD_SAVEGAME_VERSION) {
|
|
SlError(STR_GAME_SAVELOAD_ERROR_TOO_NEW_SAVEGAME);
|
|
} else if (_sl_version >= SLV_START_PATCHPACKS && _sl_version <= SLV_END_PATCHPACKS) {
|
|
SlError(STR_GAME_SAVELOAD_ERROR_PATCHPACK);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load a chunk of data (eg vehicles, stations, etc.)
|
|
* @param ch The chunkhandler that will be used for the operation
|
|
*/
|
|
static void SlLoadChunk(const ChunkHandler &ch)
|
|
{
|
|
byte m = SlReadByte();
|
|
size_t len;
|
|
size_t endoffs;
|
|
|
|
_sl.block_mode = m;
|
|
_sl.obj_len = 0;
|
|
|
|
SaveLoadChunkExtHeaderFlags ext_flags = static_cast<SaveLoadChunkExtHeaderFlags>(0);
|
|
if ((m & 0xF) == CH_EXT_HDR) {
|
|
ext_flags = static_cast<SaveLoadChunkExtHeaderFlags>(SlReadUint32());
|
|
|
|
/* read in real header */
|
|
m = SlReadByte();
|
|
_sl.block_mode = m;
|
|
}
|
|
|
|
switch (m) {
|
|
case CH_ARRAY:
|
|
_sl.array_index = 0;
|
|
ch.load_proc();
|
|
if (_next_offs != 0) SlErrorCorrupt("Invalid array length");
|
|
break;
|
|
case CH_SPARSE_ARRAY:
|
|
ch.load_proc();
|
|
if (_next_offs != 0) SlErrorCorrupt("Invalid array length");
|
|
break;
|
|
default:
|
|
if ((m & 0xF) == CH_RIFF) {
|
|
/* Read length */
|
|
len = (SlReadByte() << 16) | ((m >> 4) << 24);
|
|
len += SlReadUint16();
|
|
SlRIFFSpringPPCheck(len);
|
|
if (SlXvIsFeaturePresent(XSLFI_RIFF_HEADER_60_BIT)) {
|
|
if (len != 0) {
|
|
SlErrorCorrupt("RIFF chunk too large");
|
|
}
|
|
len = SlReadUint32();
|
|
}
|
|
if (ext_flags & SLCEHF_BIG_RIFF) {
|
|
len |= SlReadUint32() << 28;
|
|
}
|
|
|
|
_sl.obj_len = len;
|
|
endoffs = _sl.reader->GetSize() + len;
|
|
ch.load_proc();
|
|
if (_sl.reader->GetSize() != endoffs) {
|
|
DEBUG(sl, 1, "Invalid chunk size: " PRINTF_SIZE " != " PRINTF_SIZE ", (" PRINTF_SIZE ")", _sl.reader->GetSize(), endoffs, len);
|
|
SlErrorCorrupt("Invalid chunk size");
|
|
}
|
|
} else {
|
|
SlErrorCorrupt("Invalid chunk type");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load a chunk of data for checking savegames.
|
|
* If the chunkhandler is nullptr, the chunk is skipped.
|
|
* @param ch The chunkhandler that will be used for the operation, this may be nullptr
|
|
*/
|
|
static void SlLoadCheckChunk(const ChunkHandler *ch)
|
|
{
|
|
byte m = SlReadByte();
|
|
size_t len;
|
|
size_t endoffs;
|
|
|
|
_sl.block_mode = m;
|
|
_sl.obj_len = 0;
|
|
|
|
SaveLoadChunkExtHeaderFlags ext_flags = static_cast<SaveLoadChunkExtHeaderFlags>(0);
|
|
if ((m & 0xF) == CH_EXT_HDR) {
|
|
ext_flags = static_cast<SaveLoadChunkExtHeaderFlags>(SlReadUint32());
|
|
|
|
/* read in real header */
|
|
m = SlReadByte();
|
|
_sl.block_mode = m;
|
|
}
|
|
|
|
switch (m) {
|
|
case CH_ARRAY:
|
|
_sl.array_index = 0;
|
|
if (ext_flags) {
|
|
SlErrorCorruptFmt("CH_ARRAY does not take chunk header extension flags: 0x%X", ext_flags);
|
|
}
|
|
if (ch && ch->load_check_proc) {
|
|
ch->load_check_proc();
|
|
} else {
|
|
SlSkipArray();
|
|
}
|
|
break;
|
|
case CH_SPARSE_ARRAY:
|
|
if (ext_flags) {
|
|
SlErrorCorruptFmt("CH_SPARSE_ARRAY does not take chunk header extension flags: 0x%X", ext_flags);
|
|
}
|
|
if (ch && ch->load_check_proc) {
|
|
ch->load_check_proc();
|
|
} else {
|
|
SlSkipArray();
|
|
}
|
|
break;
|
|
default:
|
|
if ((m & 0xF) == CH_RIFF) {
|
|
if (ext_flags != (ext_flags & SLCEHF_BIG_RIFF)) {
|
|
SlErrorCorruptFmt("Unknown chunk header extension flags for CH_RIFF: 0x%X", ext_flags);
|
|
}
|
|
/* Read length */
|
|
len = (SlReadByte() << 16) | ((m >> 4) << 24);
|
|
len += SlReadUint16();
|
|
SlRIFFSpringPPCheck(len);
|
|
if (SlXvIsFeaturePresent(XSLFI_RIFF_HEADER_60_BIT)) {
|
|
if (len != 0) {
|
|
SlErrorCorrupt("RIFF chunk too large");
|
|
}
|
|
len = SlReadUint32();
|
|
if (ext_flags & SLCEHF_BIG_RIFF) SlErrorCorrupt("XSLFI_RIFF_HEADER_60_BIT and SLCEHF_BIG_RIFF both present");
|
|
}
|
|
if (ext_flags & SLCEHF_BIG_RIFF) {
|
|
uint64 full_len = len | (static_cast<uint64>(SlReadUint32()) << 28);
|
|
if (full_len >= (1LL << 32)) {
|
|
SlErrorCorrupt("Chunk size too large: " OTTD_PRINTFHEX64, full_len);
|
|
}
|
|
len = static_cast<size_t>(full_len);
|
|
}
|
|
_sl.obj_len = len;
|
|
endoffs = _sl.reader->GetSize() + len;
|
|
if (ch && ch->load_check_proc) {
|
|
ch->load_check_proc();
|
|
} else {
|
|
SlSkipBytes(len);
|
|
}
|
|
if (_sl.reader->GetSize() != endoffs) {
|
|
DEBUG(sl, 1, "Invalid chunk size: " PRINTF_SIZE " != " PRINTF_SIZE ", (" PRINTF_SIZE ")", _sl.reader->GetSize(), endoffs, len);
|
|
SlErrorCorrupt("Invalid chunk size");
|
|
}
|
|
} else {
|
|
SlErrorCorrupt("Invalid chunk type");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save a chunk of data (eg. vehicles, stations, etc.). Each chunk is
|
|
* prefixed by an ID identifying it, followed by data, and terminator where appropriate
|
|
* @param ch The chunkhandler that will be used for the operation
|
|
*/
|
|
static void SlSaveChunk(const ChunkHandler &ch)
|
|
{
|
|
ChunkSaveLoadProc *proc = ch.save_proc;
|
|
|
|
/* Don't save any chunk information if there is no save handler. */
|
|
if (proc == nullptr) return;
|
|
|
|
SlWriteUint32(ch.id);
|
|
DEBUG(sl, 2, "Saving chunk %c%c%c%c", ch.id >> 24, ch.id >> 16, ch.id >> 8, ch.id);
|
|
|
|
size_t written = 0;
|
|
if (_debug_sl_level >= 3) written = SlGetBytesWritten();
|
|
|
|
_sl.block_mode = ch.type;
|
|
switch (ch.type) {
|
|
case CH_RIFF:
|
|
_sl.need_length = NL_WANTLENGTH;
|
|
proc();
|
|
break;
|
|
case CH_ARRAY:
|
|
_sl.last_array_index = 0;
|
|
SlWriteByte(CH_ARRAY);
|
|
proc();
|
|
SlWriteArrayLength(0); // Terminate arrays
|
|
break;
|
|
case CH_SPARSE_ARRAY:
|
|
SlWriteByte(CH_SPARSE_ARRAY);
|
|
proc();
|
|
SlWriteArrayLength(0); // Terminate arrays
|
|
break;
|
|
default: NOT_REACHED();
|
|
}
|
|
|
|
DEBUG(sl, 3, "Saved chunk %c%c%c%c (" PRINTF_SIZE " bytes)", ch.id >> 24, ch.id >> 16, ch.id >> 8, ch.id, SlGetBytesWritten() - written);
|
|
}
|
|
|
|
/** Save all chunks */
|
|
static void SlSaveChunks()
|
|
{
|
|
for (auto &ch : ChunkHandlers()) {
|
|
SlSaveChunk(ch);
|
|
}
|
|
|
|
/* Terminator */
|
|
SlWriteUint32(0);
|
|
}
|
|
|
|
/**
|
|
* Find the ChunkHandler that will be used for processing the found
|
|
* chunk in the savegame or in memory
|
|
* @param id the chunk in question
|
|
* @return returns the appropriate chunkhandler
|
|
*/
|
|
static const ChunkHandler *SlFindChunkHandler(uint32 id)
|
|
{
|
|
for (auto &ch : ChunkHandlers()) if (ch.id == id) return &ch;
|
|
return nullptr;
|
|
}
|
|
|
|
/** Load all chunks */
|
|
static void SlLoadChunks()
|
|
{
|
|
if (_sl_upstream_mode) {
|
|
upstream_sl::SlLoadChunks();
|
|
return;
|
|
}
|
|
|
|
for (uint32 id = SlReadUint32(); id != 0; id = SlReadUint32()) {
|
|
DEBUG(sl, 2, "Loading chunk %c%c%c%c", id >> 24, id >> 16, id >> 8, id);
|
|
size_t read = 0;
|
|
if (_debug_sl_level >= 3) read = SlGetBytesRead();
|
|
|
|
if (SlXvIsChunkDiscardable(id)) {
|
|
DEBUG(sl, 1, "Discarding chunk %c%c%c%c", id >> 24, id >> 16, id >> 8, id);
|
|
SlLoadCheckChunk(nullptr);
|
|
} else {
|
|
const ChunkHandler *ch = SlFindChunkHandler(id);
|
|
if (ch == nullptr) {
|
|
SlErrorCorrupt("Unknown chunk type");
|
|
} else {
|
|
SlLoadChunk(*ch);
|
|
}
|
|
}
|
|
DEBUG(sl, 3, "Loaded chunk %c%c%c%c (" PRINTF_SIZE " bytes)", id >> 24, id >> 16, id >> 8, id, SlGetBytesRead() - read);
|
|
}
|
|
}
|
|
|
|
/** Load all chunks for savegame checking */
|
|
static void SlLoadCheckChunks()
|
|
{
|
|
if (_sl_upstream_mode) {
|
|
upstream_sl::SlLoadCheckChunks();
|
|
return;
|
|
}
|
|
|
|
uint32 id;
|
|
const ChunkHandler *ch;
|
|
|
|
for (id = SlReadUint32(); id != 0; id = SlReadUint32()) {
|
|
DEBUG(sl, 2, "Loading chunk %c%c%c%c", id >> 24, id >> 16, id >> 8, id);
|
|
size_t read = 0;
|
|
if (_debug_sl_level >= 3) read = SlGetBytesRead();
|
|
|
|
if (SlXvIsChunkDiscardable(id)) {
|
|
ch = nullptr;
|
|
} else {
|
|
ch = SlFindChunkHandler(id);
|
|
if (ch == nullptr) SlErrorCorrupt("Unknown chunk type");
|
|
}
|
|
SlLoadCheckChunk(ch);
|
|
DEBUG(sl, 3, "Loaded chunk %c%c%c%c (" PRINTF_SIZE " bytes)", id >> 24, id >> 16, id >> 8, id, SlGetBytesRead() - read);
|
|
}
|
|
}
|
|
|
|
/** Fix all pointers (convert index -> pointer) */
|
|
static void SlFixPointers()
|
|
{
|
|
if (_sl_upstream_mode) {
|
|
upstream_sl::SlFixPointers();
|
|
return;
|
|
}
|
|
|
|
_sl.action = SLA_PTRS;
|
|
|
|
for (auto &ch : ChunkHandlers()) {
|
|
if (ch.ptrs_proc != nullptr) {
|
|
DEBUG(sl, 3, "Fixing pointers for %c%c%c%c", ch.id >> 24, ch.id >> 16, ch.id >> 8, ch.id);
|
|
ch.ptrs_proc();
|
|
}
|
|
}
|
|
|
|
assert(_sl.action == SLA_PTRS);
|
|
}
|
|
|
|
|
|
/** Yes, simply reading from a file. */
|
|
struct FileReader : LoadFilter {
|
|
FILE *file; ///< The file to read from.
|
|
long begin; ///< The begin of the file.
|
|
|
|
/**
|
|
* Create the file reader, so it reads from a specific file.
|
|
* @param file The file to read from.
|
|
*/
|
|
FileReader(FILE *file) : LoadFilter(nullptr), file(file), begin(ftell(file))
|
|
{
|
|
}
|
|
|
|
/** Make sure everything is cleaned up. */
|
|
~FileReader()
|
|
{
|
|
if (this->file != nullptr) fclose(this->file);
|
|
this->file = nullptr;
|
|
|
|
/* Make sure we don't double free. */
|
|
_sl.sf = nullptr;
|
|
}
|
|
|
|
size_t Read(byte *buf, size_t size) override
|
|
{
|
|
/* We're in the process of shutting down, i.e. in "failure" mode. */
|
|
if (this->file == nullptr) return 0;
|
|
|
|
return fread(buf, 1, size, this->file);
|
|
}
|
|
|
|
void Reset() override
|
|
{
|
|
clearerr(this->file);
|
|
if (fseek(this->file, this->begin, SEEK_SET)) {
|
|
DEBUG(sl, 1, "Could not reset the file reading");
|
|
}
|
|
}
|
|
};
|
|
|
|
/** Yes, simply writing to a file. */
|
|
struct FileWriter : SaveFilter {
|
|
FILE *file; ///< The file to write to.
|
|
|
|
/**
|
|
* Create the file writer, so it writes to a specific file.
|
|
* @param file The file to write to.
|
|
*/
|
|
FileWriter(FILE *file) : SaveFilter(nullptr), file(file)
|
|
{
|
|
}
|
|
|
|
/** Make sure everything is cleaned up. */
|
|
~FileWriter()
|
|
{
|
|
this->Finish();
|
|
|
|
/* Make sure we don't double free. */
|
|
_sl.sf = nullptr;
|
|
}
|
|
|
|
void Write(byte *buf, size_t size) override
|
|
{
|
|
/* We're in the process of shutting down, i.e. in "failure" mode. */
|
|
if (this->file == nullptr) return;
|
|
|
|
if (fwrite(buf, 1, size, this->file) != size) SlError(STR_GAME_SAVELOAD_ERROR_FILE_NOT_WRITEABLE);
|
|
}
|
|
|
|
void Finish() override
|
|
{
|
|
if (this->file != nullptr) fclose(this->file);
|
|
this->file = nullptr;
|
|
}
|
|
};
|
|
|
|
/*******************************************
|
|
********** START OF LZO CODE **************
|
|
*******************************************/
|
|
|
|
#ifdef WITH_LZO
|
|
#include <lzo/lzo1x.h>
|
|
|
|
/** Buffer size for the LZO compressor */
|
|
static const uint LZO_BUFFER_SIZE = 8192;
|
|
|
|
/** Filter using LZO compression. */
|
|
struct LZOLoadFilter : LoadFilter {
|
|
/**
|
|
* Initialise this filter.
|
|
* @param chain The next filter in this chain.
|
|
*/
|
|
LZOLoadFilter(LoadFilter *chain) : LoadFilter(chain)
|
|
{
|
|
if (lzo_init() != LZO_E_OK) SlError(STR_GAME_SAVELOAD_ERROR_BROKEN_INTERNAL_ERROR, "cannot initialize decompressor");
|
|
}
|
|
|
|
size_t Read(byte *buf, size_t ssize) override
|
|
{
|
|
assert(ssize >= LZO_BUFFER_SIZE);
|
|
|
|
/* Buffer size is from the LZO docs plus the chunk header size. */
|
|
byte out[LZO_BUFFER_SIZE + LZO_BUFFER_SIZE / 16 + 64 + 3 + sizeof(uint32) * 2];
|
|
uint32 tmp[2];
|
|
uint32 size;
|
|
lzo_uint len = ssize;
|
|
|
|
/* Read header*/
|
|
if (this->chain->Read((byte*)tmp, sizeof(tmp)) != sizeof(tmp)) SlError(STR_GAME_SAVELOAD_ERROR_FILE_NOT_READABLE, "File read failed");
|
|
|
|
/* Check if size is bad */
|
|
((uint32*)out)[0] = size = tmp[1];
|
|
|
|
if (_sl_version != SL_MIN_VERSION) {
|
|
tmp[0] = TO_BE32(tmp[0]);
|
|
size = TO_BE32(size);
|
|
}
|
|
|
|
if (size >= sizeof(out)) SlErrorCorrupt("Inconsistent size");
|
|
|
|
/* Read block */
|
|
if (this->chain->Read(out + sizeof(uint32), size) != size) SlError(STR_GAME_SAVELOAD_ERROR_FILE_NOT_READABLE);
|
|
|
|
/* Verify checksum */
|
|
if (tmp[0] != lzo_adler32(0, out, size + sizeof(uint32))) SlErrorCorrupt("Bad checksum");
|
|
|
|
/* Decompress */
|
|
int ret = lzo1x_decompress_safe(out + sizeof(uint32) * 1, size, buf, &len, nullptr);
|
|
if (ret != LZO_E_OK) SlError(STR_GAME_SAVELOAD_ERROR_FILE_NOT_READABLE);
|
|
return len;
|
|
}
|
|
};
|
|
|
|
/** Filter using LZO compression. */
|
|
struct LZOSaveFilter : SaveFilter {
|
|
/**
|
|
* Initialise this filter.
|
|
* @param chain The next filter in this chain.
|
|
* @param compression_level The requested level of compression.
|
|
*/
|
|
LZOSaveFilter(SaveFilter *chain, byte compression_level) : SaveFilter(chain)
|
|
{
|
|
if (lzo_init() != LZO_E_OK) SlError(STR_GAME_SAVELOAD_ERROR_BROKEN_INTERNAL_ERROR, "cannot initialize compressor");
|
|
}
|
|
|
|
void Write(byte *buf, size_t size) override
|
|
{
|
|
const lzo_bytep in = buf;
|
|
/* Buffer size is from the LZO docs plus the chunk header size. */
|
|
byte out[LZO_BUFFER_SIZE + LZO_BUFFER_SIZE / 16 + 64 + 3 + sizeof(uint32) * 2];
|
|
byte wrkmem[LZO1X_1_MEM_COMPRESS];
|
|
lzo_uint outlen;
|
|
|
|
do {
|
|
/* Compress up to LZO_BUFFER_SIZE bytes at once. */
|
|
lzo_uint len = size > LZO_BUFFER_SIZE ? LZO_BUFFER_SIZE : (lzo_uint)size;
|
|
lzo1x_1_compress(in, len, out + sizeof(uint32) * 2, &outlen, wrkmem);
|
|
((uint32*)out)[1] = TO_BE32((uint32)outlen);
|
|
((uint32*)out)[0] = TO_BE32(lzo_adler32(0, out + sizeof(uint32), outlen + sizeof(uint32)));
|
|
this->chain->Write(out, outlen + sizeof(uint32) * 2);
|
|
|
|
/* Move to next data chunk. */
|
|
size -= len;
|
|
in += len;
|
|
} while (size > 0);
|
|
}
|
|
};
|
|
|
|
#endif /* WITH_LZO */
|
|
|
|
/*********************************************
|
|
******** START OF NOCOMP CODE (uncompressed)*
|
|
*********************************************/
|
|
|
|
/** Filter without any compression. */
|
|
struct NoCompLoadFilter : LoadFilter {
|
|
/**
|
|
* Initialise this filter.
|
|
* @param chain The next filter in this chain.
|
|
*/
|
|
NoCompLoadFilter(LoadFilter *chain) : LoadFilter(chain)
|
|
{
|
|
}
|
|
|
|
size_t Read(byte *buf, size_t size) override
|
|
{
|
|
return this->chain->Read(buf, size);
|
|
}
|
|
};
|
|
|
|
/** Filter without any compression. */
|
|
struct NoCompSaveFilter : SaveFilter {
|
|
/**
|
|
* Initialise this filter.
|
|
* @param chain The next filter in this chain.
|
|
* @param compression_level The requested level of compression.
|
|
*/
|
|
NoCompSaveFilter(SaveFilter *chain, byte compression_level) : SaveFilter(chain)
|
|
{
|
|
}
|
|
|
|
void Write(byte *buf, size_t size) override
|
|
{
|
|
this->chain->Write(buf, size);
|
|
}
|
|
};
|
|
|
|
/********************************************
|
|
********** START OF ZLIB CODE **************
|
|
********************************************/
|
|
|
|
#if defined(WITH_ZLIB)
|
|
#include <zlib.h>
|
|
|
|
/** Filter using Zlib compression. */
|
|
struct ZlibLoadFilter : LoadFilter {
|
|
z_stream z; ///< Stream state we are reading from.
|
|
byte fread_buf[MEMORY_CHUNK_SIZE]; ///< Buffer for reading from the file.
|
|
|
|
/**
|
|
* Initialise this filter.
|
|
* @param chain The next filter in this chain.
|
|
*/
|
|
ZlibLoadFilter(LoadFilter *chain) : LoadFilter(chain)
|
|
{
|
|
memset(&this->z, 0, sizeof(this->z));
|
|
if (inflateInit(&this->z) != Z_OK) SlError(STR_GAME_SAVELOAD_ERROR_BROKEN_INTERNAL_ERROR, "cannot initialize decompressor");
|
|
}
|
|
|
|
/** Clean everything up. */
|
|
~ZlibLoadFilter()
|
|
{
|
|
inflateEnd(&this->z);
|
|
}
|
|
|
|
size_t Read(byte *buf, size_t size) override
|
|
{
|
|
this->z.next_out = buf;
|
|
this->z.avail_out = (uint)size;
|
|
|
|
do {
|
|
/* read more bytes from the file? */
|
|
if (this->z.avail_in == 0) {
|
|
this->z.next_in = this->fread_buf;
|
|
this->z.avail_in = (uint)this->chain->Read(this->fread_buf, sizeof(this->fread_buf));
|
|
}
|
|
|
|
/* inflate the data */
|
|
int r = inflate(&this->z, 0);
|
|
if (r == Z_STREAM_END) break;
|
|
|
|
if (r != Z_OK) SlError(STR_GAME_SAVELOAD_ERROR_BROKEN_INTERNAL_ERROR, "inflate() failed");
|
|
} while (this->z.avail_out != 0);
|
|
|
|
return size - this->z.avail_out;
|
|
}
|
|
};
|
|
|
|
/** Filter using Zlib compression. */
|
|
struct ZlibSaveFilter : SaveFilter {
|
|
z_stream z; ///< Stream state we are writing to.
|
|
|
|
/**
|
|
* Initialise this filter.
|
|
* @param chain The next filter in this chain.
|
|
* @param compression_level The requested level of compression.
|
|
*/
|
|
ZlibSaveFilter(SaveFilter *chain, byte compression_level) : SaveFilter(chain)
|
|
{
|
|
memset(&this->z, 0, sizeof(this->z));
|
|
if (deflateInit(&this->z, compression_level) != Z_OK) SlError(STR_GAME_SAVELOAD_ERROR_BROKEN_INTERNAL_ERROR, "cannot initialize compressor");
|
|
}
|
|
|
|
/** Clean up what we allocated. */
|
|
~ZlibSaveFilter()
|
|
{
|
|
deflateEnd(&this->z);
|
|
}
|
|
|
|
/**
|
|
* Helper loop for writing the data.
|
|
* @param p The bytes to write.
|
|
* @param len Amount of bytes to write.
|
|
* @param mode Mode for deflate.
|
|
*/
|
|
void WriteLoop(byte *p, size_t len, int mode)
|
|
{
|
|
byte buf[MEMORY_CHUNK_SIZE]; // output buffer
|
|
uint n;
|
|
this->z.next_in = p;
|
|
this->z.avail_in = (uInt)len;
|
|
do {
|
|
this->z.next_out = buf;
|
|
this->z.avail_out = sizeof(buf);
|
|
|
|
/**
|
|
* For the poor next soul who sees many valgrind warnings of the
|
|
* "Conditional jump or move depends on uninitialised value(s)" kind:
|
|
* According to the author of zlib it is not a bug and it won't be fixed.
|
|
* http://groups.google.com/group/comp.compression/browse_thread/thread/b154b8def8c2a3ef/cdf9b8729ce17ee2
|
|
* [Mark Adler, Feb 24 2004, 'zlib-1.2.1 valgrind warnings' in the newsgroup comp.compression]
|
|
*/
|
|
int r = deflate(&this->z, mode);
|
|
|
|
/* bytes were emitted? */
|
|
if ((n = sizeof(buf) - this->z.avail_out) != 0) {
|
|
this->chain->Write(buf, n);
|
|
}
|
|
if (r == Z_STREAM_END) break;
|
|
|
|
if (r != Z_OK) SlError(STR_GAME_SAVELOAD_ERROR_BROKEN_INTERNAL_ERROR, "zlib returned error code");
|
|
} while (this->z.avail_in || !this->z.avail_out);
|
|
}
|
|
|
|
void Write(byte *buf, size_t size) override
|
|
{
|
|
this->WriteLoop(buf, size, 0);
|
|
}
|
|
|
|
void Finish() override
|
|
{
|
|
this->WriteLoop(nullptr, 0, Z_FINISH);
|
|
this->chain->Finish();
|
|
}
|
|
};
|
|
|
|
#endif /* WITH_ZLIB */
|
|
|
|
/********************************************
|
|
********** START OF LZMA CODE **************
|
|
********************************************/
|
|
|
|
#if defined(WITH_LIBLZMA)
|
|
#include <lzma.h>
|
|
|
|
/**
|
|
* Have a copy of an initialised LZMA stream. We need this as it's
|
|
* impossible to "re"-assign LZMA_STREAM_INIT to a variable in some
|
|
* compilers, i.e. LZMA_STREAM_INIT can't be used to set something.
|
|
* This var has to be used instead.
|
|
*/
|
|
static const lzma_stream _lzma_init = LZMA_STREAM_INIT;
|
|
|
|
/** Filter without any compression. */
|
|
struct LZMALoadFilter : LoadFilter {
|
|
lzma_stream lzma; ///< Stream state that we are reading from.
|
|
byte fread_buf[MEMORY_CHUNK_SIZE]; ///< Buffer for reading from the file.
|
|
|
|
/**
|
|
* Initialise this filter.
|
|
* @param chain The next filter in this chain.
|
|
*/
|
|
LZMALoadFilter(LoadFilter *chain) : LoadFilter(chain), lzma(_lzma_init)
|
|
{
|
|
/* Allow saves up to 256 MB uncompressed */
|
|
if (lzma_auto_decoder(&this->lzma, 1 << 28, 0) != LZMA_OK) SlError(STR_GAME_SAVELOAD_ERROR_BROKEN_INTERNAL_ERROR, "cannot initialize decompressor");
|
|
}
|
|
|
|
/** Clean everything up. */
|
|
~LZMALoadFilter()
|
|
{
|
|
lzma_end(&this->lzma);
|
|
}
|
|
|
|
size_t Read(byte *buf, size_t size) override
|
|
{
|
|
this->lzma.next_out = buf;
|
|
this->lzma.avail_out = size;
|
|
|
|
do {
|
|
/* read more bytes from the file? */
|
|
if (this->lzma.avail_in == 0) {
|
|
this->lzma.next_in = this->fread_buf;
|
|
this->lzma.avail_in = this->chain->Read(this->fread_buf, sizeof(this->fread_buf));
|
|
}
|
|
|
|
/* inflate the data */
|
|
lzma_ret r = lzma_code(&this->lzma, LZMA_RUN);
|
|
if (r == LZMA_STREAM_END) break;
|
|
if (r != LZMA_OK) SlErrorFmt(STR_GAME_SAVELOAD_ERROR_BROKEN_INTERNAL_ERROR, "liblzma returned error code: %u", r);
|
|
} while (this->lzma.avail_out != 0);
|
|
|
|
return size - this->lzma.avail_out;
|
|
}
|
|
};
|
|
|
|
/** Filter using LZMA compression. */
|
|
struct LZMASaveFilter : SaveFilter {
|
|
lzma_stream lzma; ///< Stream state that we are writing to.
|
|
|
|
/**
|
|
* Initialise this filter.
|
|
* @param chain The next filter in this chain.
|
|
* @param compression_level The requested level of compression.
|
|
*/
|
|
LZMASaveFilter(SaveFilter *chain, byte compression_level) : SaveFilter(chain), lzma(_lzma_init)
|
|
{
|
|
if (lzma_easy_encoder(&this->lzma, compression_level, LZMA_CHECK_CRC32) != LZMA_OK) SlError(STR_GAME_SAVELOAD_ERROR_BROKEN_INTERNAL_ERROR, "cannot initialize compressor");
|
|
}
|
|
|
|
/** Clean up what we allocated. */
|
|
~LZMASaveFilter()
|
|
{
|
|
lzma_end(&this->lzma);
|
|
}
|
|
|
|
/**
|
|
* Helper loop for writing the data.
|
|
* @param p The bytes to write.
|
|
* @param len Amount of bytes to write.
|
|
* @param action Action for lzma_code.
|
|
*/
|
|
void WriteLoop(byte *p, size_t len, lzma_action action)
|
|
{
|
|
byte buf[MEMORY_CHUNK_SIZE]; // output buffer
|
|
size_t n;
|
|
this->lzma.next_in = p;
|
|
this->lzma.avail_in = len;
|
|
do {
|
|
this->lzma.next_out = buf;
|
|
this->lzma.avail_out = sizeof(buf);
|
|
|
|
lzma_ret r = lzma_code(&this->lzma, action);
|
|
|
|
/* bytes were emitted? */
|
|
if ((n = sizeof(buf) - this->lzma.avail_out) != 0) {
|
|
this->chain->Write(buf, n);
|
|
}
|
|
if (r == LZMA_STREAM_END) break;
|
|
if (r != LZMA_OK) SlErrorFmt(STR_GAME_SAVELOAD_ERROR_BROKEN_INTERNAL_ERROR, "liblzma returned error code: %u", r);
|
|
} while (this->lzma.avail_in || !this->lzma.avail_out);
|
|
}
|
|
|
|
void Write(byte *buf, size_t size) override
|
|
{
|
|
this->WriteLoop(buf, size, LZMA_RUN);
|
|
}
|
|
|
|
void Finish() override
|
|
{
|
|
this->WriteLoop(nullptr, 0, LZMA_FINISH);
|
|
this->chain->Finish();
|
|
}
|
|
};
|
|
|
|
#endif /* WITH_LIBLZMA */
|
|
|
|
/********************************************
|
|
********** START OF ZSTD CODE **************
|
|
********************************************/
|
|
|
|
#if defined(WITH_ZSTD)
|
|
#include <zstd.h>
|
|
|
|
|
|
/** Filter using ZSTD compression. */
|
|
struct ZSTDLoadFilter : LoadFilter {
|
|
ZSTD_DCtx *zstd; ///< ZSTD decompression context
|
|
byte fread_buf[MEMORY_CHUNK_SIZE]; ///< Buffer for reading from the file
|
|
ZSTD_inBuffer input; ///< ZSTD input buffer for fread_buf
|
|
|
|
/**
|
|
* Initialise this filter.
|
|
* @param chain The next filter in this chain.
|
|
*/
|
|
ZSTDLoadFilter(LoadFilter *chain) : LoadFilter(chain)
|
|
{
|
|
this->zstd = ZSTD_createDCtx();
|
|
if (!this->zstd) SlError(STR_GAME_SAVELOAD_ERROR_BROKEN_INTERNAL_ERROR, "cannot initialize compressor");
|
|
this->input = {this->fread_buf, 0, 0};
|
|
}
|
|
|
|
/** Clean everything up. */
|
|
~ZSTDLoadFilter()
|
|
{
|
|
ZSTD_freeDCtx(this->zstd);
|
|
}
|
|
|
|
size_t Read(byte *buf, size_t size) override
|
|
{
|
|
ZSTD_outBuffer output{buf, size, 0};
|
|
|
|
do {
|
|
/* read more bytes from the file? */
|
|
if (this->input.pos == this->input.size) {
|
|
this->input.size = this->chain->Read(this->fread_buf, sizeof(this->fread_buf));
|
|
this->input.pos = 0;
|
|
if (this->input.size == 0) break;
|
|
}
|
|
|
|
size_t ret = ZSTD_decompressStream(this->zstd, &output, &this->input);
|
|
if (ZSTD_isError(ret)) SlError(STR_GAME_SAVELOAD_ERROR_BROKEN_INTERNAL_ERROR, "libzstd returned error code");
|
|
if (ret == 0) break;
|
|
} while (output.pos < output.size);
|
|
|
|
return output.pos;
|
|
}
|
|
};
|
|
|
|
/** Filter using ZSTD compression. */
|
|
struct ZSTDSaveFilter : SaveFilter {
|
|
ZSTD_CCtx *zstd; ///< ZSTD compression context
|
|
|
|
/**
|
|
* Initialise this filter.
|
|
* @param chain The next filter in this chain.
|
|
* @param compression_level The requested level of compression.
|
|
*/
|
|
ZSTDSaveFilter(SaveFilter *chain, byte compression_level) : SaveFilter(chain)
|
|
{
|
|
this->zstd = ZSTD_createCCtx();
|
|
if (!this->zstd) SlError(STR_GAME_SAVELOAD_ERROR_BROKEN_INTERNAL_ERROR, "cannot initialize compressor");
|
|
if (ZSTD_isError(ZSTD_CCtx_setParameter(this->zstd, ZSTD_c_compressionLevel, (int)compression_level - 100))) {
|
|
ZSTD_freeCCtx(this->zstd);
|
|
SlError(STR_GAME_SAVELOAD_ERROR_BROKEN_INTERNAL_ERROR, "invalid compresison level");
|
|
}
|
|
}
|
|
|
|
/** Clean up what we allocated. */
|
|
~ZSTDSaveFilter()
|
|
{
|
|
ZSTD_freeCCtx(this->zstd);
|
|
}
|
|
|
|
/**
|
|
* Helper loop for writing the data.
|
|
* @param p The bytes to write.
|
|
* @param len Amount of bytes to write.
|
|
* @param mode Mode for ZSTD_compressStream2.
|
|
*/
|
|
void WriteLoop(byte *p, size_t len, ZSTD_EndDirective mode)
|
|
{
|
|
byte buf[MEMORY_CHUNK_SIZE]; // output buffer
|
|
ZSTD_inBuffer input{p, len, 0};
|
|
|
|
bool finished;
|
|
do {
|
|
ZSTD_outBuffer output{buf, sizeof(buf), 0};
|
|
size_t remaining = ZSTD_compressStream2(this->zstd, &output, &input, mode);
|
|
if (ZSTD_isError(remaining)) SlError(STR_GAME_SAVELOAD_ERROR_BROKEN_INTERNAL_ERROR, "libzstd returned error code");
|
|
|
|
if (output.pos != 0) this->chain->Write(buf, output.pos);
|
|
|
|
finished = (mode == ZSTD_e_end ? (remaining == 0) : (input.pos == input.size));
|
|
} while (!finished);
|
|
}
|
|
|
|
void Write(byte *buf, size_t size) override
|
|
{
|
|
this->WriteLoop(buf, size, ZSTD_e_continue);
|
|
}
|
|
|
|
void Finish() override
|
|
{
|
|
this->WriteLoop(nullptr, 0, ZSTD_e_end);
|
|
this->chain->Finish();
|
|
}
|
|
};
|
|
|
|
#endif /* WITH_LIBZSTD */
|
|
|
|
/*******************************************
|
|
************* END OF CODE *****************
|
|
*******************************************/
|
|
|
|
enum SaveLoadFormatFlags : byte {
|
|
SLF_NONE = 0,
|
|
SLF_NO_THREADED_LOAD = 1 << 0, ///< Unsuitable for threaded loading
|
|
SLF_REQUIRES_ZSTD = 1 << 1, ///< Automatic selection requires the zstd flag
|
|
};
|
|
DECLARE_ENUM_AS_BIT_SET(SaveLoadFormatFlags);
|
|
|
|
/** The format for a reader/writer type of a savegame */
|
|
struct SaveLoadFormat {
|
|
const char *name; ///< name of the compressor/decompressor (debug-only)
|
|
uint32 tag; ///< the 4-letter tag by which it is identified in the savegame
|
|
|
|
LoadFilter *(*init_load)(LoadFilter *chain); ///< Constructor for the load filter.
|
|
SaveFilter *(*init_write)(SaveFilter *chain, byte compression); ///< Constructor for the save filter.
|
|
|
|
byte min_compression; ///< the minimum compression level of this format
|
|
byte default_compression; ///< the default compression level of this format
|
|
byte max_compression; ///< the maximum compression level of this format
|
|
SaveLoadFormatFlags flags; ///< flags
|
|
};
|
|
|
|
/** The different saveload formats known/understood by OpenTTD. */
|
|
static const SaveLoadFormat _saveload_formats[] = {
|
|
#if defined(WITH_LZO)
|
|
/* Roughly 75% larger than zlib level 6 at only ~7% of the CPU usage. */
|
|
{"lzo", TO_BE32X('OTTD'), CreateLoadFilter<LZOLoadFilter>, CreateSaveFilter<LZOSaveFilter>, 0, 0, 0, SLF_NO_THREADED_LOAD},
|
|
#else
|
|
{"lzo", TO_BE32X('OTTD'), nullptr, nullptr, 0, 0, 0, SLF_NO_THREADED_LOAD},
|
|
#endif
|
|
/* Roughly 5 times larger at only 1% of the CPU usage over zlib level 6. */
|
|
{"none", TO_BE32X('OTTN'), CreateLoadFilter<NoCompLoadFilter>, CreateSaveFilter<NoCompSaveFilter>, 0, 0, 0, SLF_NONE},
|
|
#if defined(WITH_ZLIB)
|
|
/* After level 6 the speed reduction is significant (1.5x to 2.5x slower per level), but the reduction in filesize is
|
|
* fairly insignificant (~1% for each step). Lower levels become ~5-10% bigger by each level than level 6 while level
|
|
* 1 is "only" 3 times as fast. Level 0 results in uncompressed savegames at about 8 times the cost of "none". */
|
|
{"zlib", TO_BE32X('OTTZ'), CreateLoadFilter<ZlibLoadFilter>, CreateSaveFilter<ZlibSaveFilter>, 0, 6, 9, SLF_NONE},
|
|
#else
|
|
{"zlib", TO_BE32X('OTTZ'), nullptr, nullptr, 0, 0, 0, SLF_NONE},
|
|
#endif
|
|
#if defined(WITH_LIBLZMA)
|
|
/* Level 2 compression is speed wise as fast as zlib level 6 compression (old default), but results in ~10% smaller saves.
|
|
* Higher compression levels are possible, and might improve savegame size by up to 25%, but are also up to 10 times slower.
|
|
* The next significant reduction in file size is at level 4, but that is already 4 times slower. Level 3 is primarily 50%
|
|
* slower while not improving the filesize, while level 0 and 1 are faster, but don't reduce savegame size much.
|
|
* It's OTTX and not e.g. OTTL because liblzma is part of xz-utils and .tar.xz is preferred over .tar.lzma. */
|
|
{"lzma", TO_BE32X('OTTX'), CreateLoadFilter<LZMALoadFilter>, CreateSaveFilter<LZMASaveFilter>, 0, 2, 9, SLF_NONE},
|
|
#else
|
|
{"lzma", TO_BE32X('OTTX'), nullptr, nullptr, 0, 0, 0, SLF_NONE},
|
|
#endif
|
|
#if defined(WITH_ZSTD)
|
|
/* Zstd provides a decent compression rate at a very high compression/decompression speed. Compared to lzma level 2
|
|
* zstd saves are about 40% larger (on level 1) but it has about 30x faster compression and 5x decompression making it
|
|
* a good choice for multiplayer servers. And zstd level 1 seems to be the optimal one for client connection speed
|
|
* (compress + 10 MB/s download + decompress time), about 3x faster than lzma:2 and 1.5x than zlib:2 and lzo.
|
|
* As zstd has negative compression levels the values were increased by 100 moving zstd level range -100..22 into
|
|
* openttd 0..122. Also note that value 100 mathes zstd level 0 which is a special value for default level 3 (openttd 103) */
|
|
{"zstd", TO_BE32X('OTTS'), CreateLoadFilter<ZSTDLoadFilter>, CreateSaveFilter<ZSTDSaveFilter>, 0, 101, 122, SLF_REQUIRES_ZSTD},
|
|
#else
|
|
{"zstd", TO_BE32X('OTTS'), nullptr, nullptr, 0, 0, 0, SLF_REQUIRES_ZSTD},
|
|
#endif
|
|
};
|
|
|
|
/**
|
|
* Return the savegameformat of the game. Whether it was created with ZLIB compression
|
|
* uncompressed, or another type
|
|
* @param full_name Name of the savegame format. If empty it picks the first available one
|
|
* @param compression_level Output for telling what compression level we want.
|
|
* @return Pointer to SaveLoadFormat struct giving all characteristics of this type of savegame
|
|
*/
|
|
static const SaveLoadFormat *GetSavegameFormat(const std::string &full_name, byte *compression_level, SaveModeFlags flags)
|
|
{
|
|
const SaveLoadFormat *def = lastof(_saveload_formats);
|
|
|
|
/* find default savegame format, the highest one with which files can be written */
|
|
while (!def->init_write || ((def->flags & SLF_REQUIRES_ZSTD) && !(flags & SMF_ZSTD_OK))) def--;
|
|
|
|
if (!full_name.empty()) {
|
|
/* Get the ":..." of the compression level out of the way */
|
|
size_t separator = full_name.find(':');
|
|
bool has_comp_level = separator != std::string::npos;
|
|
const std::string name(full_name, 0, has_comp_level ? separator : full_name.size());
|
|
|
|
for (const SaveLoadFormat *slf = &_saveload_formats[0]; slf != endof(_saveload_formats); slf++) {
|
|
if (slf->init_write != nullptr && name.compare(slf->name) == 0) {
|
|
*compression_level = slf->default_compression;
|
|
if (has_comp_level) {
|
|
const std::string complevel(full_name, separator + 1);
|
|
|
|
/* Get the level and determine whether all went fine. */
|
|
size_t processed;
|
|
long level = std::stol(complevel, &processed, 10);
|
|
if (processed == 0 || level != Clamp(level, slf->min_compression, slf->max_compression)) {
|
|
SetDParamStr(0, complevel);
|
|
ShowErrorMessage(STR_CONFIG_ERROR, STR_CONFIG_ERROR_INVALID_SAVEGAME_COMPRESSION_LEVEL, WL_CRITICAL);
|
|
} else {
|
|
*compression_level = level;
|
|
}
|
|
}
|
|
return slf;
|
|
}
|
|
}
|
|
|
|
SetDParamStr(0, name);
|
|
SetDParamStr(1, def->name);
|
|
ShowErrorMessage(STR_CONFIG_ERROR, STR_CONFIG_ERROR_INVALID_SAVEGAME_COMPRESSION_ALGORITHM, WL_CRITICAL);
|
|
}
|
|
*compression_level = def->default_compression;
|
|
return def;
|
|
}
|
|
|
|
/* actual loader/saver function */
|
|
void InitializeGame(uint size_x, uint size_y, bool reset_date, bool reset_settings);
|
|
extern bool AfterLoadGame();
|
|
extern bool LoadOldSaveGame(const std::string &file);
|
|
|
|
/**
|
|
* Clear temporary data that is passed between various saveload phases.
|
|
*/
|
|
static void ResetSaveloadData()
|
|
{
|
|
ResetTempEngineData();
|
|
ResetLabelMaps();
|
|
ResetOldWaypoints();
|
|
}
|
|
|
|
/**
|
|
* Clear/free saveload state.
|
|
*/
|
|
static inline void ClearSaveLoadState()
|
|
{
|
|
delete _sl.dumper;
|
|
_sl.dumper = nullptr;
|
|
|
|
delete _sl.sf;
|
|
_sl.sf = nullptr;
|
|
|
|
delete _sl.reader;
|
|
_sl.reader = nullptr;
|
|
|
|
delete _sl.lf;
|
|
_sl.lf = nullptr;
|
|
|
|
_sl.save_flags = SMF_NONE;
|
|
|
|
GamelogStopAnyAction();
|
|
}
|
|
|
|
/**
|
|
* Update the gui accordingly when starting saving
|
|
* and set locks on saveload. Also turn off fast-forward cause with that
|
|
* saving takes Aaaaages
|
|
*/
|
|
static void SaveFileStart()
|
|
{
|
|
_sl.game_speed = _game_speed;
|
|
_game_speed = 100;
|
|
SetMouseCursorBusy(true);
|
|
|
|
InvalidateWindowData(WC_STATUS_BAR, 0, SBI_SAVELOAD_START);
|
|
_sl.saveinprogress = true;
|
|
}
|
|
|
|
/** Update the gui accordingly when saving is done and release locks on saveload. */
|
|
static void SaveFileDone()
|
|
{
|
|
if (_game_mode != GM_MENU) _game_speed = _sl.game_speed;
|
|
SetMouseCursorBusy(false);
|
|
|
|
InvalidateWindowData(WC_STATUS_BAR, 0, SBI_SAVELOAD_FINISH);
|
|
_sl.saveinprogress = false;
|
|
|
|
#ifdef __EMSCRIPTEN__
|
|
EM_ASM(if (window["openttd_syncfs"]) openttd_syncfs());
|
|
#endif
|
|
}
|
|
|
|
/** Set the error message from outside of the actual loading/saving of the game (AfterLoadGame and friends) */
|
|
void SetSaveLoadError(StringID str)
|
|
{
|
|
_sl.error_str = str;
|
|
}
|
|
|
|
/** Get the string representation of the error message */
|
|
const char *GetSaveLoadErrorString()
|
|
{
|
|
SetDParam(0, _sl.error_str);
|
|
SetDParamStr(1, _sl.extra_msg);
|
|
|
|
static char err_str[512];
|
|
GetString(err_str, _sl.action == SLA_SAVE ? STR_ERROR_GAME_SAVE_FAILED : STR_ERROR_GAME_LOAD_FAILED, lastof(err_str));
|
|
return err_str;
|
|
}
|
|
|
|
/** Show a gui message when saving has failed */
|
|
static void SaveFileError()
|
|
{
|
|
SetDParamStr(0, GetSaveLoadErrorString());
|
|
ShowErrorMessage(STR_JUST_RAW_STRING, INVALID_STRING_ID, WL_ERROR);
|
|
SaveFileDone();
|
|
}
|
|
|
|
/**
|
|
* We have written the whole game into memory, _memory_savegame, now find
|
|
* and appropriate compressor and start writing to file.
|
|
*/
|
|
static SaveOrLoadResult SaveFileToDisk(bool threaded)
|
|
{
|
|
try {
|
|
byte compression;
|
|
const SaveLoadFormat *fmt = GetSavegameFormat(_savegame_format, &compression, _sl.save_flags);
|
|
|
|
DEBUG(sl, 3, "Using compression format: %s, level: %u", fmt->name, compression);
|
|
|
|
/* We have written our stuff to memory, now write it to file! */
|
|
uint32 hdr[2] = { fmt->tag, TO_BE32((uint32) (SAVEGAME_VERSION | SAVEGAME_VERSION_EXT) << 16) };
|
|
_sl.sf->Write((byte*)hdr, sizeof(hdr));
|
|
|
|
_sl.sf = fmt->init_write(_sl.sf, compression);
|
|
_sl.dumper->Flush(_sl.sf);
|
|
|
|
ClearSaveLoadState();
|
|
|
|
if (threaded) SetAsyncSaveFinish(SaveFileDone);
|
|
|
|
return SL_OK;
|
|
} catch (...) {
|
|
ClearSaveLoadState();
|
|
|
|
AsyncSaveFinishProc asfp = SaveFileDone;
|
|
|
|
/* We don't want to shout when saving is just
|
|
* cancelled due to a client disconnecting. */
|
|
if (_sl.error_str != STR_NETWORK_ERROR_LOSTCONNECTION) {
|
|
/* Skip the "colour" character */
|
|
DEBUG(sl, 0, "%s", GetSaveLoadErrorString() + 3);
|
|
asfp = SaveFileError;
|
|
}
|
|
|
|
if (threaded) {
|
|
SetAsyncSaveFinish(asfp);
|
|
} else {
|
|
asfp();
|
|
}
|
|
return SL_ERROR;
|
|
}
|
|
}
|
|
|
|
void WaitTillSaved()
|
|
{
|
|
if (!_save_thread.joinable()) return;
|
|
|
|
_save_thread.join();
|
|
|
|
/* Make sure every other state is handled properly as well. */
|
|
ProcessAsyncSaveFinish();
|
|
}
|
|
|
|
/**
|
|
* Actually perform the saving of the savegame.
|
|
* General tactics is to first save the game to memory, then write it to file
|
|
* using the writer, either in threaded mode if possible, or single-threaded.
|
|
* @param writer The filter to write the savegame to.
|
|
* @param threaded Whether to try to perform the saving asynchronously.
|
|
* @return Return the result of the action. #SL_OK or #SL_ERROR
|
|
*/
|
|
static SaveOrLoadResult DoSave(SaveFilter *writer, bool threaded)
|
|
{
|
|
assert(!_sl.saveinprogress);
|
|
|
|
_sl.dumper = new MemoryDumper();
|
|
_sl.sf = writer;
|
|
|
|
_sl_version = SAVEGAME_VERSION;
|
|
SlXvSetCurrentState();
|
|
|
|
SaveViewportBeforeSaveGame();
|
|
SlSaveChunks();
|
|
|
|
SaveFileStart();
|
|
|
|
if (!threaded || !StartNewThread(&_save_thread, "ottd:savegame", &SaveFileToDisk, true)) {
|
|
if (threaded) DEBUG(sl, 1, "Cannot create savegame thread, reverting to single-threaded mode...");
|
|
|
|
SaveOrLoadResult result = SaveFileToDisk(false);
|
|
SaveFileDone();
|
|
|
|
return result;
|
|
}
|
|
|
|
return SL_OK;
|
|
}
|
|
|
|
/**
|
|
* Save the game using a (writer) filter.
|
|
* @param writer The filter to write the savegame to.
|
|
* @param threaded Whether to try to perform the saving asynchronously.
|
|
* @param flags Save mode flags.
|
|
* @return Return the result of the action. #SL_OK or #SL_ERROR
|
|
*/
|
|
SaveOrLoadResult SaveWithFilter(SaveFilter *writer, bool threaded, SaveModeFlags flags)
|
|
{
|
|
try {
|
|
_sl.action = SLA_SAVE;
|
|
_sl.save_flags = flags;
|
|
return DoSave(writer, threaded);
|
|
} catch (...) {
|
|
ClearSaveLoadState();
|
|
return SL_ERROR;
|
|
}
|
|
}
|
|
|
|
bool IsNetworkServerSave()
|
|
{
|
|
return _sl.save_flags & SMF_NET_SERVER;
|
|
}
|
|
|
|
struct ThreadedLoadFilter : LoadFilter {
|
|
static const size_t BUFFER_COUNT = 4;
|
|
|
|
std::mutex mutex;
|
|
std::condition_variable full_cv;
|
|
std::condition_variable empty_cv;
|
|
uint first_ready = 0;
|
|
uint count_ready = 0;
|
|
size_t read_offsets[BUFFER_COUNT];
|
|
size_t read_counts[BUFFER_COUNT];
|
|
byte read_buf[MEMORY_CHUNK_SIZE * BUFFER_COUNT]; ///< Buffers for reading from source.
|
|
bool no_thread = false;
|
|
|
|
bool have_exception = false;
|
|
ThreadSlErrorException caught_exception;
|
|
|
|
std::thread read_thread;
|
|
|
|
/**
|
|
* Initialise this filter.
|
|
* @param chain The next filter in this chain.
|
|
*/
|
|
ThreadedLoadFilter(LoadFilter *chain) : LoadFilter(chain)
|
|
{
|
|
std::unique_lock<std::mutex> lk(this->mutex);
|
|
if (!StartNewThread(&this->read_thread, "ottd:loadgame", &ThreadedLoadFilter::RunThread, this)) {
|
|
DEBUG(sl, 1, "Failed to start load read thread, reading non-threaded");
|
|
this->no_thread = true;
|
|
} else {
|
|
DEBUG(sl, 2, "Started load read thread");
|
|
}
|
|
}
|
|
|
|
/** Clean everything up. */
|
|
~ThreadedLoadFilter()
|
|
{
|
|
std::unique_lock<std::mutex> lk(this->mutex);
|
|
this->no_thread = true;
|
|
lk.unlock();
|
|
this->empty_cv.notify_all();
|
|
this->full_cv.notify_all();
|
|
if (this->read_thread.joinable()) {
|
|
this->read_thread.join();
|
|
DEBUG(sl, 2, "Joined load read thread");
|
|
}
|
|
}
|
|
|
|
static void RunThread(ThreadedLoadFilter *self)
|
|
{
|
|
try {
|
|
std::unique_lock<std::mutex> lk(self->mutex);
|
|
while (!self->no_thread) {
|
|
if (self->count_ready == BUFFER_COUNT) {
|
|
self->full_cv.wait(lk);
|
|
continue;
|
|
}
|
|
|
|
uint buf = (self->first_ready + self->count_ready) % BUFFER_COUNT;
|
|
lk.unlock();
|
|
size_t read = self->chain->Read(self->read_buf + (buf * MEMORY_CHUNK_SIZE), MEMORY_CHUNK_SIZE);
|
|
lk.lock();
|
|
self->read_offsets[buf] = 0;
|
|
self->read_counts[buf] = read;
|
|
self->count_ready++;
|
|
if (self->count_ready == 1) self->empty_cv.notify_one();
|
|
}
|
|
} catch (const ThreadSlErrorException &ex) {
|
|
std::unique_lock<std::mutex> lk(self->mutex);
|
|
self->caught_exception = ex;
|
|
self->have_exception = true;
|
|
self->empty_cv.notify_one();
|
|
}
|
|
}
|
|
|
|
size_t Read(byte *buf, size_t size) override
|
|
{
|
|
if (this->no_thread) return this->chain->Read(buf, size);
|
|
|
|
size_t read = 0;
|
|
std::unique_lock<std::mutex> lk(this->mutex);
|
|
while (read < size || this->have_exception) {
|
|
if (this->have_exception) {
|
|
this->have_exception = false;
|
|
SlError(this->caught_exception.string, this->caught_exception.extra_msg);
|
|
}
|
|
if (this->count_ready == 0) {
|
|
this->empty_cv.wait(lk);
|
|
continue;
|
|
}
|
|
|
|
size_t to_read = std::min<size_t>(size - read, read_counts[this->first_ready]);
|
|
if (to_read == 0) break;
|
|
memcpy(buf + read, this->read_buf + (this->first_ready * MEMORY_CHUNK_SIZE) + read_offsets[this->first_ready], to_read);
|
|
read += to_read;
|
|
read_offsets[this->first_ready] += to_read;
|
|
read_counts[this->first_ready] -= to_read;
|
|
if (read_counts[this->first_ready] == 0) {
|
|
this->first_ready = (this->first_ready + 1) % BUFFER_COUNT;
|
|
this->count_ready--;
|
|
if (this->count_ready == BUFFER_COUNT - 1) this->full_cv.notify_one();
|
|
}
|
|
}
|
|
return read;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Actually perform the loading of a "non-old" savegame.
|
|
* @param reader The filter to read the savegame from.
|
|
* @param load_check Whether to perform the checking ("preview") or actually load the game.
|
|
* @return Return the result of the action. #SL_OK or #SL_REINIT ("unload" the game)
|
|
*/
|
|
static SaveOrLoadResult DoLoad(LoadFilter *reader, bool load_check)
|
|
{
|
|
_sl.lf = reader;
|
|
|
|
if (load_check) {
|
|
/* Clear previous check data */
|
|
_load_check_data.Clear();
|
|
/* Mark SL_LOAD_CHECK as supported for this savegame. */
|
|
_load_check_data.checkable = true;
|
|
}
|
|
|
|
SlXvResetState();
|
|
SlResetVENC();
|
|
auto guard = scope_guard([&]() {
|
|
SlResetVENC();
|
|
});
|
|
|
|
uint32 hdr[2];
|
|
if (_sl.lf->Read((byte*)hdr, sizeof(hdr)) != sizeof(hdr)) SlError(STR_GAME_SAVELOAD_ERROR_FILE_NOT_READABLE);
|
|
|
|
/* see if we have any loader for this type. */
|
|
const SaveLoadFormat *fmt = _saveload_formats;
|
|
for (;;) {
|
|
/* No loader found, treat as version 0 and use LZO format */
|
|
if (fmt == endof(_saveload_formats)) {
|
|
DEBUG(sl, 0, "Unknown savegame type, trying to load it as the buggy format");
|
|
_sl.lf->Reset();
|
|
_sl_version = SL_MIN_VERSION;
|
|
_sl_minor_version = 0;
|
|
SlXvResetState();
|
|
|
|
/* Try to find the LZO savegame format; it uses 'OTTD' as tag. */
|
|
fmt = _saveload_formats;
|
|
for (;;) {
|
|
if (fmt == endof(_saveload_formats)) {
|
|
/* Who removed LZO support? */
|
|
NOT_REACHED();
|
|
}
|
|
if (fmt->tag == TO_BE32X('OTTD')) break;
|
|
fmt++;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (fmt->tag == hdr[0]) {
|
|
/* check version number */
|
|
_sl_version = (SaveLoadVersion)(TO_BE32(hdr[1]) >> 16);
|
|
/* Minor is not used anymore from version 18.0, but it is still needed
|
|
* in versions before that (4 cases) which can't be removed easy.
|
|
* Therefore it is loaded, but never saved (or, it saves a 0 in any scenario). */
|
|
_sl_minor_version = (TO_BE32(hdr[1]) >> 8) & 0xFF;
|
|
|
|
bool special_version = false;
|
|
if (_sl_version & SAVEGAME_VERSION_EXT) {
|
|
_sl_version = (SaveLoadVersion)(_sl_version & ~SAVEGAME_VERSION_EXT);
|
|
_sl_is_ext_version = true;
|
|
} else {
|
|
special_version = SlXvCheckSpecialSavegameVersions();
|
|
}
|
|
|
|
if (_sl_version >= SLV_SAVELOAD_LIST_LENGTH) {
|
|
if (_sl_is_ext_version) {
|
|
DEBUG(sl, 0, "Got an extended savegame version with a base version in the upstream mode range, giving up");
|
|
SlError(STR_GAME_SAVELOAD_ERROR_TOO_NEW_SAVEGAME);
|
|
} else {
|
|
_sl_upstream_mode = true;
|
|
}
|
|
}
|
|
|
|
DEBUG(sl, 1, "Loading savegame version %d%s%s%s%s", _sl_version, _sl_is_ext_version ? " (extended)" : "",
|
|
_sl_maybe_springpp ? " which might be SpringPP" : "", _sl_maybe_chillpp ? " which might be ChillPP" : "", _sl_upstream_mode ? " (upstream mode)" : "");
|
|
|
|
/* Is the version higher than the current? */
|
|
if (_sl_version > MAX_LOAD_SAVEGAME_VERSION && !special_version) SlError(STR_GAME_SAVELOAD_ERROR_TOO_NEW_SAVEGAME);
|
|
if (_sl_version >= SLV_START_PATCHPACKS && _sl_version <= SLV_END_PATCHPACKS && !special_version) SlError(STR_GAME_SAVELOAD_ERROR_PATCHPACK);
|
|
break;
|
|
}
|
|
|
|
fmt++;
|
|
}
|
|
|
|
/* loader for this savegame type is not implemented? */
|
|
if (fmt->init_load == nullptr) {
|
|
char err_str[64];
|
|
seprintf(err_str, lastof(err_str), "Loader for '%s' is not available.", fmt->name);
|
|
SlError(STR_GAME_SAVELOAD_ERROR_BROKEN_INTERNAL_ERROR, err_str);
|
|
}
|
|
|
|
_sl.lf = fmt->init_load(_sl.lf);
|
|
if (!(fmt->flags & SLF_NO_THREADED_LOAD)) {
|
|
_sl.lf = new ThreadedLoadFilter(_sl.lf);
|
|
}
|
|
_sl.reader = new ReadBuffer(_sl.lf);
|
|
_next_offs = 0;
|
|
|
|
if (!load_check) {
|
|
ResetSaveloadData();
|
|
|
|
/* Old maps were hardcoded to 256x256 and thus did not contain
|
|
* any mapsize information. Pre-initialize to 256x256 to not to
|
|
* confuse old games */
|
|
InitializeGame(256, 256, true, true);
|
|
|
|
GamelogReset();
|
|
|
|
if (IsSavegameVersionBefore(SLV_4)) {
|
|
/*
|
|
* NewGRFs were introduced between 0.3,4 and 0.3.5, which both
|
|
* shared savegame version 4. Anything before that 'obviously'
|
|
* does not have any NewGRFs. Between the introduction and
|
|
* savegame version 41 (just before 0.5) the NewGRF settings
|
|
* were not stored in the savegame and they were loaded by
|
|
* using the settings from the main menu.
|
|
* So, to recap:
|
|
* - savegame version < 4: do not load any NewGRFs.
|
|
* - savegame version >= 41: load NewGRFs from savegame, which is
|
|
* already done at this stage by
|
|
* overwriting the main menu settings.
|
|
* - other savegame versions: use main menu settings.
|
|
*
|
|
* This means that users *can* crash savegame version 4..40
|
|
* savegames if they set incompatible NewGRFs in the main menu,
|
|
* but can't crash anymore for savegame version < 4 savegames.
|
|
*
|
|
* Note: this is done here because AfterLoadGame is also called
|
|
* for TTO/TTD/TTDP savegames which have their own NewGRF logic.
|
|
*/
|
|
ClearGRFConfigList(&_grfconfig);
|
|
}
|
|
}
|
|
|
|
if (load_check) {
|
|
/* Load chunks into _load_check_data.
|
|
* No pools are loaded. References are not possible, and thus do not need resolving. */
|
|
SlLoadCheckChunks();
|
|
} else {
|
|
/* Load chunks and resolve references */
|
|
SlLoadChunks();
|
|
SlFixPointers();
|
|
}
|
|
|
|
ClearSaveLoadState();
|
|
|
|
_savegame_type = SGT_OTTD;
|
|
|
|
if (load_check) {
|
|
/* The only part from AfterLoadGame() we need */
|
|
if (_load_check_data.want_grf_compatibility) _load_check_data.grf_compatibility = IsGoodGRFConfigList(_load_check_data.grfconfig);
|
|
} else {
|
|
GamelogStartAction(GLAT_LOAD);
|
|
|
|
/* After loading fix up savegame for any internal changes that
|
|
* might have occurred since then. If it fails, load back the old game. */
|
|
if (!AfterLoadGame()) {
|
|
GamelogStopAction();
|
|
return SL_REINIT;
|
|
}
|
|
|
|
GamelogStopAction();
|
|
SlXvSetCurrentState();
|
|
}
|
|
|
|
return SL_OK;
|
|
}
|
|
|
|
/**
|
|
* Load the game using a (reader) filter.
|
|
* @param reader The filter to read the savegame from.
|
|
* @return Return the result of the action. #SL_OK or #SL_REINIT ("unload" the game)
|
|
*/
|
|
SaveOrLoadResult LoadWithFilter(LoadFilter *reader)
|
|
{
|
|
try {
|
|
_sl.action = SLA_LOAD;
|
|
return DoLoad(reader, false);
|
|
} catch (...) {
|
|
ClearSaveLoadState();
|
|
return SL_REINIT;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main Save or Load function where the high-level saveload functions are
|
|
* handled. It opens the savegame, selects format and checks versions
|
|
* @param filename The name of the savegame being created/loaded
|
|
* @param fop Save or load mode. Load can also be a TTD(Patch) game.
|
|
* @param sb The sub directory to save the savegame in
|
|
* @param threaded True when threaded saving is allowed
|
|
* @return Return the result of the action. #SL_OK, #SL_ERROR, or #SL_REINIT ("unload" the game)
|
|
*/
|
|
SaveOrLoadResult SaveOrLoad(const std::string &filename, SaveLoadOperation fop, DetailedFileType dft, Subdirectory sb, bool threaded, SaveModeFlags save_flags)
|
|
{
|
|
/* An instance of saving is already active, so don't go saving again */
|
|
if (_sl.saveinprogress && fop == SLO_SAVE && dft == DFT_GAME_FILE && threaded) {
|
|
/* if not an autosave, but a user action, show error message */
|
|
if (!_do_autosave) ShowErrorMessage(STR_ERROR_SAVE_STILL_IN_PROGRESS, INVALID_STRING_ID, WL_ERROR);
|
|
return SL_OK;
|
|
}
|
|
WaitTillSaved();
|
|
|
|
try {
|
|
/* Load a TTDLX or TTDPatch game */
|
|
if (fop == SLO_LOAD && dft == DFT_OLD_GAME_FILE) {
|
|
ResetSaveloadData();
|
|
|
|
InitializeGame(256, 256, true, true); // set a mapsize of 256x256 for TTDPatch games or it might get confused
|
|
|
|
/* TTD/TTO savegames have no NewGRFs, TTDP savegame have them
|
|
* and if so a new NewGRF list will be made in LoadOldSaveGame.
|
|
* Note: this is done here because AfterLoadGame is also called
|
|
* for OTTD savegames which have their own NewGRF logic. */
|
|
ClearGRFConfigList(&_grfconfig);
|
|
GamelogReset();
|
|
if (!LoadOldSaveGame(filename)) return SL_REINIT;
|
|
_sl_version = SL_MIN_VERSION;
|
|
_sl_minor_version = 0;
|
|
SlXvResetState();
|
|
GamelogStartAction(GLAT_LOAD);
|
|
if (!AfterLoadGame()) {
|
|
GamelogStopAction();
|
|
return SL_REINIT;
|
|
}
|
|
GamelogStopAction();
|
|
SlXvSetCurrentState();
|
|
return SL_OK;
|
|
}
|
|
|
|
assert(dft == DFT_GAME_FILE);
|
|
switch (fop) {
|
|
case SLO_CHECK:
|
|
_sl.action = SLA_LOAD_CHECK;
|
|
break;
|
|
|
|
case SLO_LOAD:
|
|
_sl.action = SLA_LOAD;
|
|
break;
|
|
|
|
case SLO_SAVE:
|
|
_sl.action = SLA_SAVE;
|
|
break;
|
|
|
|
default: NOT_REACHED();
|
|
}
|
|
_sl.save_flags = save_flags;
|
|
|
|
FILE *fh = (fop == SLO_SAVE) ? FioFOpenFile(filename, "wb", sb) : FioFOpenFile(filename, "rb", sb);
|
|
|
|
/* Make it a little easier to load savegames from the console */
|
|
if (fh == nullptr && fop != SLO_SAVE) fh = FioFOpenFile(filename, "rb", SAVE_DIR);
|
|
if (fh == nullptr && fop != SLO_SAVE) fh = FioFOpenFile(filename, "rb", BASE_DIR);
|
|
if (fh == nullptr && fop != SLO_SAVE) fh = FioFOpenFile(filename, "rb", SCENARIO_DIR);
|
|
|
|
if (fh == nullptr) {
|
|
SlError(fop == SLO_SAVE ? STR_GAME_SAVELOAD_ERROR_FILE_NOT_WRITEABLE : STR_GAME_SAVELOAD_ERROR_FILE_NOT_READABLE);
|
|
}
|
|
|
|
if (fop == SLO_SAVE) { // SAVE game
|
|
DEBUG(desync, 1, "save: date{%08x; %02x; %02x}; %s", _date, _date_fract, _tick_skip_counter, filename.c_str());
|
|
if (!_settings_client.gui.threaded_saves) threaded = false;
|
|
|
|
return DoSave(new FileWriter(fh), threaded);
|
|
}
|
|
|
|
/* LOAD game */
|
|
assert(fop == SLO_LOAD || fop == SLO_CHECK);
|
|
DEBUG(desync, 1, "load: %s", filename.c_str());
|
|
return DoLoad(new FileReader(fh), fop == SLO_CHECK);
|
|
} catch (...) {
|
|
/* This code may be executed both for old and new save games. */
|
|
ClearSaveLoadState();
|
|
|
|
/* Skip the "colour" character */
|
|
if (fop != SLO_CHECK) DEBUG(sl, 0, "%s", GetSaveLoadErrorString() + 3);
|
|
|
|
/* A saver/loader exception!! reinitialize all variables to prevent crash! */
|
|
return (fop == SLO_LOAD) ? SL_REINIT : SL_ERROR;
|
|
}
|
|
}
|
|
|
|
/** Do a save when exiting the game (_settings_client.gui.autosave_on_exit) */
|
|
void DoExitSave()
|
|
{
|
|
SaveOrLoad("exit.sav", SLO_SAVE, DFT_GAME_FILE, AUTOSAVE_DIR, true, SMF_ZSTD_OK);
|
|
}
|
|
|
|
/**
|
|
* Fill the buffer with the default name for a savegame *or* screenshot.
|
|
* @param buf the buffer to write to.
|
|
* @param last the last element in the buffer.
|
|
*/
|
|
void GenerateDefaultSaveName(char *buf, const char *last)
|
|
{
|
|
/* Check if we have a name for this map, which is the name of the first
|
|
* available company. When there's no company available we'll use
|
|
* 'Spectator' as "company" name. */
|
|
CompanyID cid = _local_company;
|
|
if (!Company::IsValidID(cid)) {
|
|
for (const Company *c : Company::Iterate()) {
|
|
cid = c->index;
|
|
break;
|
|
}
|
|
}
|
|
|
|
SetDParam(0, cid);
|
|
|
|
/* Insert current date */
|
|
switch (_settings_client.gui.date_format_in_default_names) {
|
|
case 0: SetDParam(1, STR_JUST_DATE_LONG); break;
|
|
case 1: SetDParam(1, STR_JUST_DATE_TINY); break;
|
|
case 2: SetDParam(1, STR_JUST_DATE_ISO); break;
|
|
default: NOT_REACHED();
|
|
}
|
|
SetDParam(2, _date);
|
|
|
|
/* Get the correct string (special string for when there's not company) */
|
|
GetString(buf, !Company::IsValidID(cid) ? STR_SAVEGAME_NAME_SPECTATOR : STR_SAVEGAME_NAME_DEFAULT, last);
|
|
SanitizeFilename(buf);
|
|
}
|
|
|
|
/**
|
|
* Set the mode and file type of the file to save or load based on the type of file entry at the file system.
|
|
* @param ft Type of file entry of the file system.
|
|
*/
|
|
void FileToSaveLoad::SetMode(FiosType ft)
|
|
{
|
|
this->SetMode(SLO_LOAD, GetAbstractFileType(ft), GetDetailedFileType(ft));
|
|
}
|
|
|
|
/**
|
|
* Set the mode and file type of the file to save or load.
|
|
* @param fop File operation being performed.
|
|
* @param aft Abstract file type.
|
|
* @param dft Detailed file type.
|
|
*/
|
|
void FileToSaveLoad::SetMode(SaveLoadOperation fop, AbstractFileType aft, DetailedFileType dft)
|
|
{
|
|
if (aft == FT_INVALID || aft == FT_NONE) {
|
|
this->file_op = SLO_INVALID;
|
|
this->detail_ftype = DFT_INVALID;
|
|
this->abstract_ftype = FT_INVALID;
|
|
return;
|
|
}
|
|
|
|
this->file_op = fop;
|
|
this->detail_ftype = dft;
|
|
this->abstract_ftype = aft;
|
|
}
|
|
|
|
/**
|
|
* Set the name of the file.
|
|
* @param name Name of the file.
|
|
*/
|
|
void FileToSaveLoad::SetName(const char *name)
|
|
{
|
|
this->name = name;
|
|
}
|
|
|
|
/**
|
|
* Set the title of the file.
|
|
* @param title Title of the file.
|
|
*/
|
|
void FileToSaveLoad::SetTitle(const char *title)
|
|
{
|
|
strecpy(this->title, title, lastof(this->title));
|
|
}
|
|
|
|
bool SaveLoadFileTypeIsScenario()
|
|
{
|
|
return _file_to_saveload.abstract_ftype == FT_SCENARIO;
|
|
}
|