diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3c66172c49..4146084b3b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -489,7 +489,7 @@ jobs:
name: Ubuntu Linux (AppImage, x86_64)
runs-on: ubuntu-latest
needs: [check-code-formatting, build_variables]
- container: openrct2/openrct2-build:14-jammy
+ container: openrct2/openrct2-build:21-jammy
steps:
- name: Checkout
uses: actions/checkout@v4
diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml
index 33b25d4d85..2bd6ea1a11 100644
--- a/.github/workflows/clang-tidy.yml
+++ b/.github/workflows/clang-tidy.yml
@@ -12,7 +12,7 @@ on:
jobs:
clang-tidy-check:
runs-on: ubuntu-latest
- container: openrct2/openrct2-build:19-noble
+ container: openrct2/openrct2-build:20-noble
steps:
- uses: actions/checkout@v4
- uses: ZehMatt/clang-tidy-annotations@v1
diff --git a/distribution/changelog.txt b/distribution/changelog.txt
index df04209894..f2328f1056 100644
--- a/distribution/changelog.txt
+++ b/distribution/changelog.txt
@@ -1,5 +1,6 @@
0.4.26 (in development)
------------------------------------------------------------------------
+- Improved: [#24734] Save files now use Zstd compression for faster saving and smaller files.
- Improved: [#24893] The ride list now has headers, and can be sorted in both directions.
- Fix: [#16988] AppImage version does not show changelog.
diff --git a/distribution/readme.txt b/distribution/readme.txt
index c423d4be13..15ac273c03 100644
--- a/distribution/readme.txt
+++ b/distribution/readme.txt
@@ -1,4 +1,4 @@
-Last updated: 2024-11-19
+Last updated: 2025-08-04
------------------------------------------------------------------------
@@ -155,6 +155,7 @@ zlib | zlib licence.
Google Test | BSD 3 clause licence.
Google Benchmark | Apache 2.0 licence.
sfl | zlib licence.
+zstd | BSD 3 clause license.
Licences for sub-libraries used by the above may vary. For more information, visit the libraries' respective official websites.
diff --git a/openrct2.common.props b/openrct2.common.props
index 89776e4560..9028146f6d 100644
--- a/openrct2.common.props
+++ b/openrct2.common.props
@@ -88,7 +88,7 @@
DebugFull
brotlicommon.lib;brotlidec.lib;brotlienc.lib;%(AdditionalDependencies)
libbreakpadd.lib;libbreakpad_clientd.lib;%(AdditionalDependencies)
- bz2d.lib;discord-rpc.lib;flac.lib;freetyped.lib;libpng16d.lib;ogg.lib;speexdsp.lib;SDL2-staticd.lib;vorbis.lib;vorbisfile.lib;zip.lib;zlibd.lib;%(AdditionalDependencies)
+ bz2d.lib;discord-rpc.lib;flac.lib;freetyped.lib;libpng16d.lib;ogg.lib;speexdsp.lib;SDL2-staticd.lib;vorbis.lib;vorbisfile.lib;zip.lib;zlibd.lib;zstd.lib;%(AdditionalDependencies)
@@ -109,7 +109,7 @@
true
brotlicommon.lib;brotlidec.lib;brotlienc.lib;%(AdditionalDependencies)
libbreakpad.lib;libbreakpad_client.lib;%(AdditionalDependencies)
- bz2.lib;discord-rpc.lib;flac.lib;freetype.lib;libpng16.lib;ogg.lib;speexdsp.lib;SDL2-static.lib;vorbis.lib;vorbisfile.lib;zip.lib;zlib.lib;%(AdditionalDependencies)
+ bz2.lib;discord-rpc.lib;flac.lib;freetype.lib;libpng16.lib;ogg.lib;speexdsp.lib;SDL2-static.lib;vorbis.lib;vorbisfile.lib;zip.lib;zlib.lib;zstd.lib;%(AdditionalDependencies)
diff --git a/scripts/build-emscripten b/scripts/build-emscripten
index af9065ec36..f6f516205d 100755
--- a/scripts/build-emscripten
+++ b/scripts/build-emscripten
@@ -7,6 +7,7 @@ START_DIR=$(pwd)
SPEEXDSP_ROOT=/ext/speexdsp
ICU_ROOT=/ext/icu/icu4c/source
LIBZIP_ROOT=/ext/libzip
+ZSTD_ROOT=/ext/zstd
JSON_DIR=/usr/include/nlohmann/
emcmake cmake ../ \
@@ -25,7 +26,8 @@ emcmake cmake ../ \
-DICU_DATA_LIBRARIES=$ICU_ROOT/lib/libicuuc.so \
-DICU_DT_LIBRARY_RELEASE="$ICU_ROOT/stubdata/libicudata.so" \
-DLIBZIP_LIBRARIES="$LIBZIP_ROOT/build/lib/libzip.a" \
- -DEMSCRIPTEN_FLAGS="-s USE_SDL=2 -s USE_BZIP2=1 -s USE_LIBPNG=1 -pthread -O3" \
+ -DZSTD_LIBRARIES="$ZSTD_ROOT/build/build/lib/libzstd.a" \
+ -DEMSCRIPTEN_FLAGS="-s USE_SDL=2 -s USE_ZLIB=1 -s USE_BZIP2=1 -s USE_LIBPNG=1 -pthread -O3" \
-DEMSCRIPTEN_LDFLAGS="-Wno-pthreads-mem-growth -s SAFE_HEAP=0 -s ALLOW_MEMORY_GROWTH=1 -s MAXIMUM_MEMORY=4GB -s INITIAL_MEMORY=2GB -s STACK_SIZE=8388608 -s MIN_WEBGL_VERSION=2 -s MAX_WEBGL_VERSION=2 -s PTHREAD_POOL_SIZE=120 -pthread -s EXPORTED_RUNTIME_METHODS=ccall,FS,callMain,UTF8ToString,stringToNewUTF8 -lidbfs.js --use-preload-plugins -s MODULARIZE=1 -s 'EXPORT_NAME=\"OPENRCT2_WEB\"'"
emmake ninja
diff --git a/src/openrct2-android/app/src/main/CMakeLists.txt b/src/openrct2-android/app/src/main/CMakeLists.txt
index a016b3a567..ef63285416 100644
--- a/src/openrct2-android/app/src/main/CMakeLists.txt
+++ b/src/openrct2-android/app/src/main/CMakeLists.txt
@@ -46,6 +46,7 @@ ExternalProject_Add(libs
${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}icui18n${CMAKE_SHARED_LIBRARY_SUFFIX}
${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}icuuc${CMAKE_SHARED_LIBRARY_SUFFIX}
${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}z${CMAKE_SHARED_LIBRARY_SUFFIX}
+ ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}zstd${CMAKE_SHARED_LIBRARY_SUFFIX}
${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_STATIC_LIBRARY_PREFIX}SDL2main${CMAKE_STATIC_LIBRARY_SUFFIX}
LOG_DOWNLOAD 1
@@ -78,6 +79,7 @@ add_custom_command(TARGET libs POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_BINARY_DIR}/libs/lib/libspeexdsp.so" ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_BINARY_DIR}/libs/lib/libssl.so" ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_BINARY_DIR}/libs/lib/libz.so" ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
+ COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_BINARY_DIR}/libs/lib/libzstd.so" ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
)
add_library(freetype SHARED IMPORTED)
@@ -98,6 +100,12 @@ set_target_properties(z PROPERTIES IMPORTED_LOCATION
)
add_dependencies(z libs)
+add_library(zstd SHARED IMPORTED)
+set_target_properties(zstd PROPERTIES IMPORTED_LOCATION
+ ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}zstd${CMAKE_SHARED_LIBRARY_SUFFIX}
+)
+add_dependencies(zstd libs)
+
add_library(SDL2 SHARED IMPORTED)
set_target_properties(SDL2 PROPERTIES IMPORTED_LOCATION
${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}SDL2${CMAKE_SHARED_LIBRARY_SUFFIX}
@@ -250,7 +258,7 @@ file(GLOB_RECURSE OPENRCT2_CLI_SOURCES
"${ORCT2_ROOT}/src/openrct2-cli/*.hpp")
add_library(openrct2 SHARED ${LIBOPENRCT2_SOURCES})
-target_link_libraries(openrct2 android stdc++ log dl SDL2 png z icu icuuc icudata crypto ssl freetype)
+target_link_libraries(openrct2 android stdc++ log dl SDL2 png z zstd icu icuuc icudata crypto ssl freetype)
add_library(openrct2-ui SHARED ${OPENRCT2_GUI_SOURCES})
target_link_libraries(openrct2-ui openrct2 android stdc++ GLESv1_CM GLESv2 SDL2main speexdsp brotlicommon brotlidec bz2 freetype ogg vorbis vorbisfile FLAC)
diff --git a/src/openrct2/CMakeLists.txt b/src/openrct2/CMakeLists.txt
index 137b5925b5..6d4de005ec 100644
--- a/src/openrct2/CMakeLists.txt
+++ b/src/openrct2/CMakeLists.txt
@@ -121,12 +121,14 @@ if (EMSCRIPTEN)
elseif (MSVC)
find_package(png 1.6 REQUIRED)
find_package(zlib REQUIRED)
+ find_package(zstd REQUIRED)
find_path(LIBZIP_INCLUDE_DIRS zip.h)
find_library(LIBZIP_LIBRARIES zip)
else ()
PKG_CHECK_MODULES(LIBZIP REQUIRED IMPORTED_TARGET libzip>=1.0)
PKG_CHECK_MODULES(ZLIB REQUIRED IMPORTED_TARGET zlib)
+ PKG_CHECK_MODULES(ZSTD REQUIRED IMPORTED_TARGET libzstd)
PKG_CHECK_MODULES(PNG IMPORTED_TARGET libpng>=1.6)
if (NOT PNG_FOUND)
@@ -144,18 +146,21 @@ if (STATIC)
target_link_libraries(libopenrct2
${PNG_STATIC_LIBRARIES}
${ZLIB_STATIC_LIBRARIES}
- ${LIBZIP_STATIC_LIBRARIES})
+ ${LIBZIP_STATIC_LIBRARIES}
+ ${ZSTD_STATIC_LIBRARIES})
else ()
if (NOT MSVC AND NOT EMSCRIPTEN)
target_link_libraries(libopenrct2
PkgConfig::PNG
PkgConfig::ZLIB
- PkgConfig::LIBZIP)
+ PkgConfig::LIBZIP
+ PkgConfig::ZSTD)
else ()
target_link_libraries(libopenrct2
${PNG_LIBRARIES}
${ZLIB_LIBRARIES}
- ${LIBZIP_LIBRARIES})
+ ${LIBZIP_LIBRARIES}
+ ${ZSTD_LIBRARIES})
endif ()
endif ()
@@ -235,6 +240,7 @@ endif()
target_include_directories(libopenrct2 SYSTEM PRIVATE ${LIBZIP_INCLUDE_DIRS})
target_include_directories(libopenrct2 SYSTEM PRIVATE ${PNG_INCLUDE_DIRS}
${ZLIB_INCLUDE_DIRS})
+target_include_directories(libopenrct2 SYSTEM PRIVATE ${ZSTD_INCLUDE_DIRS})
include_directories(libopenrct2 SYSTEM ${CMAKE_CURRENT_LIST_DIR}/../thirdparty)
# To avoid unnecessary rebuilds set the current branch and
diff --git a/src/openrct2/ReplayManager.cpp b/src/openrct2/ReplayManager.cpp
index 03cbc01a99..b7fae2173c 100644
--- a/src/openrct2/ReplayManager.cpp
+++ b/src/openrct2/ReplayManager.cpp
@@ -103,9 +103,10 @@ namespace OpenRCT2
class ReplayManager final : public IReplayManager
{
- static constexpr uint16_t kReplayVersion = 10;
+ static constexpr uint16_t kReplayVersion = 11;
+ static constexpr uint16_t kReplayMinCompatVersion = 10;
static constexpr uint32_t kReplayMagic = 0x5243524F; // ORCR.
- static constexpr int kReplayCompressionLevel = Compression::kZlibMaxCompressionLevel;
+ static constexpr int kReplayCompressionLevel = 18;
static constexpr int kNormalRecordingChecksumTicks = 1;
static constexpr int kSilentRecordingChecksumTicks = 40; // Same as network server
@@ -313,8 +314,9 @@ namespace OpenRCT2
MemoryStream compressed;
stream.SetPosition(0);
- bool compressStatus = Compression::zlibCompress(
- stream, stream.GetLength(), compressed, Compression::ZlibHeaderType::zlib, kReplayCompressionLevel);
+ // header already has decompressed length, but no checksum, so use the ZStandard checksum
+ bool compressStatus = Compression::zstdCompress(
+ stream, stream.GetLength(), compressed, Compression::ZstdMetadata::checksum, kReplayCompressionLevel);
if (!compressStatus)
throw IOException("Compression Error");
@@ -563,12 +565,18 @@ namespace OpenRCT2
MemoryStream decompressed;
bool decompressStatus = true;
-
recFile.data.SetPosition(0);
- decompressStatus = Compression::zlibDecompress(
- recFile.data, recFile.data.GetLength(), decompressed, recFile.uncompressedSize,
- Compression::ZlibHeaderType::zlib);
-
+ if (recFile.version <= 10)
+ {
+ decompressStatus = Compression::zlibDecompress(
+ recFile.data, recFile.data.GetLength(), decompressed, recFile.uncompressedSize,
+ Compression::ZlibHeaderType::zlib);
+ }
+ else
+ {
+ decompressStatus = Compression::zstdDecompress(
+ recFile.data, recFile.data.GetLength(), decompressed, recFile.uncompressedSize);
+ }
if (!decompressStatus)
throw IOException("Decompression Error");
@@ -683,7 +691,7 @@ namespace OpenRCT2
bool Compatible(ReplayRecordData& data)
{
- return data.version == kReplayVersion;
+ return data.version >= kReplayMinCompatVersion;
}
bool Serialise(DataSerialiser& serialiser, ReplayRecordData& data)
diff --git a/src/openrct2/command_line/CommandLine.hpp b/src/openrct2/command_line/CommandLine.hpp
index 401bfb7921..03400271e0 100644
--- a/src/openrct2/command_line/CommandLine.hpp
+++ b/src/openrct2/command_line/CommandLine.hpp
@@ -108,6 +108,7 @@ consteval CommandLineCommand DefineSubCommand(const char* name, const CommandLin
namespace OpenRCT2::CommandLine
{
extern const CommandLineCommand kRootCommands[];
+ extern const CommandLineCommand kConvertCommands[];
extern const CommandLineCommand kScreenshotCommands[];
extern const CommandLineCommand kSpriteCommands[];
extern const CommandLineCommand kSimulateCommands[];
@@ -118,6 +119,5 @@ namespace OpenRCT2::CommandLine
void PrintHelp(bool allCommands = false);
exitcode_t HandleCommandDefault();
- exitcode_t HandleCommandConvert(CommandLineArgEnumerator* enumerator);
exitcode_t HandleCommandUri(CommandLineArgEnumerator* enumerator);
} // namespace OpenRCT2::CommandLine
diff --git a/src/openrct2/command_line/ConvertCommand.cpp b/src/openrct2/command_line/ConvertCommand.cpp
index 35d6289be2..b690973a66 100644
--- a/src/openrct2/command_line/ConvertCommand.cpp
+++ b/src/openrct2/command_line/ConvertCommand.cpp
@@ -12,6 +12,8 @@
#include "../GameState.h"
#include "../OpenRCT2.h"
#include "../ParkImporter.h"
+#include "../PlatformEnvironment.h"
+#include "../config/Config.h"
#include "../core/Console.hpp"
#include "../core/Path.hpp"
#include "../object/ObjectManager.h"
@@ -20,14 +22,33 @@
#include "CommandLine.hpp"
#include
+#include
#include
using namespace OpenRCT2;
+static int32_t _compressLevel = kParkFileSaveCompressionLevel;
+
+// clang-format off
+static constexpr CommandLineOptionDefinition kConvertOptions[]
+{
+ { CMDLINE_TYPE_INTEGER, &_compressLevel, 'l', "compress-level", "The compression level to use when writing the converted file" },
+ kOptionTableEnd
+};
+
+static exitcode_t HandleCommandConvert(CommandLineArgEnumerator* argEnumerator);
+
+const CommandLineCommand CommandLine::kConvertCommands[]{
+ // Main commands
+ DefineCommand("", " [destination]", kConvertOptions, HandleCommandConvert),
+ kCommandTableEnd
+};
+// clang-format on
+
static void WriteConvertFromAndToMessage(FileExtension sourceFileType, FileExtension destinationFileType);
static u8string GetFileTypeFriendlyName(FileExtension fileType);
-exitcode_t CommandLine::HandleCommandConvert(CommandLineArgEnumerator* enumerator)
+static exitcode_t HandleCommandConvert(CommandLineArgEnumerator* enumerator)
{
exitcode_t result = CommandLine::HandleCommandDefault();
if (result != EXITCODE_CONTINUE)
@@ -48,10 +69,10 @@ exitcode_t CommandLine::HandleCommandConvert(CommandLineArgEnumerator* enumerato
// Get the destination path
const utf8* rawDestinationPath;
- if (!enumerator->TryPopString(&rawDestinationPath))
+ if (!enumerator->TryPopString(&rawDestinationPath) || String::startsWith(rawDestinationPath, "-"))
{
- Console::Error::WriteLine("Expected a destination path.");
- return EXITCODE_FAIL;
+ // if no destination path is provided, convert the park file in-place
+ rawDestinationPath = rawSourcePath;
}
const auto destinationPath = Path::GetAbsolute(rawDestinationPath);
@@ -75,12 +96,12 @@ exitcode_t CommandLine::HandleCommandConvert(CommandLineArgEnumerator* enumerato
case FileExtension::PARK:
if (destinationFileType == FileExtension::PARK)
{
- Console::Error::WriteLine("File is already an OpenRCT2 saved game or scenario.");
- return EXITCODE_FAIL;
+ Console::Error::WriteLine(
+ "File is already an OpenRCT2 saved game or scenario. Updating file version and recompressing.");
}
break;
default:
- Console::Error::WriteLine("Only conversion from .SC4, .SV4, .SC6 or .SV6 is supported.");
+ Console::Error::WriteLine("Only conversion from .SC4, .SV4, .SC6, .SV6, or .PARK is supported.");
return EXITCODE_FAIL;
}
@@ -125,7 +146,7 @@ exitcode_t CommandLine::HandleCommandConvert(CommandLineArgEnumerator* enumerato
auto* windowMgr = Ui::GetWindowManager();
windowMgr->CloseByClass(WindowClass::MainWindow);
- exporter->Export(gameState, destinationPath);
+ exporter->Export(gameState, destinationPath, static_cast(_compressLevel));
}
catch (const std::exception& ex)
{
diff --git a/src/openrct2/command_line/RootCommands.cpp b/src/openrct2/command_line/RootCommands.cpp
index ac18b1badc..36dfe84f5b 100644
--- a/src/openrct2/command_line/RootCommands.cpp
+++ b/src/openrct2/command_line/RootCommands.cpp
@@ -133,7 +133,6 @@ const CommandLineCommand CommandLine::kRootCommands[]
DefineCommand("join", "", kStandardOptions, HandleCommandJoin ),
#endif
DefineCommand("set-rct2", "", kStandardOptions, HandleCommandSetRCT2),
- DefineCommand("convert", " ", kStandardOptions, CommandLine::HandleCommandConvert),
DefineCommand("scan-objects", "", kStandardOptions, HandleCommandScanObjects),
DefineCommand("handle-uri", "openrct2://.../", kStandardOptions, CommandLine::HandleCommandUri),
@@ -142,6 +141,7 @@ const CommandLineCommand CommandLine::kRootCommands[]
#endif
// Sub-commands
+ DefineSubCommand("convert", CommandLine::kConvertCommands ),
DefineSubCommand("screenshot", CommandLine::kScreenshotCommands ),
DefineSubCommand("sprite", CommandLine::kSpriteCommands ),
DefineSubCommand("simulate", CommandLine::kSimulateCommands ),
diff --git a/src/openrct2/command_line/SimulateCommands.cpp b/src/openrct2/command_line/SimulateCommands.cpp
index cef7abef0c..b8b5dff716 100644
--- a/src/openrct2/command_line/SimulateCommands.cpp
+++ b/src/openrct2/command_line/SimulateCommands.cpp
@@ -23,26 +23,36 @@
using namespace OpenRCT2;
+// clang-format off
+static constexpr CommandLineOptionDefinition kNoOptions[]
+{
+ kOptionTableEnd
+};
+
static exitcode_t HandleSimulate(CommandLineArgEnumerator* argEnumerator);
-const CommandLineCommand CommandLine::kSimulateCommands[]{ // Main commands
- DefineCommand("", "", nullptr, HandleSimulate),
- kCommandTableEnd
+const CommandLineCommand CommandLine::kSimulateCommands[]{
+ // Main commands
+ DefineCommand("", " ", kNoOptions, HandleSimulate),
+ kCommandTableEnd
};
+// clang-format on
static exitcode_t HandleSimulate(CommandLineArgEnumerator* argEnumerator)
{
- const char** argv = const_cast(argEnumerator->GetArguments()) + argEnumerator->GetIndex();
- int32_t argc = argEnumerator->GetCount() - argEnumerator->GetIndex();
-
- if (argc < 2)
+ const utf8* inputPath;
+ if (!argEnumerator->TryPopString(&inputPath))
{
- Console::Error::WriteLine("Missing arguments .");
+ Console::Error::WriteLine("Expected a save file path");
return EXITCODE_FAIL;
}
- const char* inputPath = argv[0];
- uint32_t ticks = atol(argv[1]);
+ int32_t ticks;
+ if (!argEnumerator->TryPopInteger(&ticks))
+ {
+ Console::Error::WriteLine("Expected a number of ticks to simulate");
+ return EXITCODE_FAIL;
+ }
gOpenRCT2Headless = true;
@@ -59,7 +69,7 @@ static exitcode_t HandleSimulate(CommandLineArgEnumerator* argEnumerator)
}
Console::WriteLine("Running %d ticks...", ticks);
- for (uint32_t i = 0; i < ticks; i++)
+ for (int32_t i = 0; i < ticks; i++)
{
gameStateUpdateLogic();
}
diff --git a/src/openrct2/core/Compression.cpp b/src/openrct2/core/Compression.cpp
index c0e0990850..340b225f86 100644
--- a/src/openrct2/core/Compression.cpp
+++ b/src/openrct2/core/Compression.cpp
@@ -19,12 +19,13 @@
#include
#include
+#include
namespace OpenRCT2::Compression
{
- constexpr size_t kZlibChunkSize = 128 * 1024;
- constexpr size_t kZlibMaxChunkSize = static_cast(std::numeric_limits::max());
- constexpr int kZlibWindowBits[] = { -15, 15, 15 + 16 };
+ static constexpr size_t kZlibChunkSize = 128 * 1024;
+ static constexpr size_t kZlibMaxChunkSize = static_cast(std::numeric_limits::max());
+ static constexpr int kZlibWindowBits[] = { -15, 15, 15 + 16 };
/*
* Modified copy of compressBound() from zlib 1.3.1, with the argument type changed from ULong
@@ -32,7 +33,7 @@ namespace OpenRCT2::Compression
*/
static uint64_t zlibCompressBound(uint64_t length)
{
- return length + (length >> 12) + (length >> 14) + (length >> 25) + 13 + (18 - 6);
+ return length + (length >> 12) + (length >> 14) + (length >> 25) + 13uLL + (18uLL - 6uLL);
}
bool zlibCompress(IStream& source, uint64_t sourceLength, IStream& dest, ZlibHeaderType header, int16_t level)
@@ -138,4 +139,137 @@ namespace OpenRCT2::Compression
inflateEnd(&strm);
return true;
}
+
+ /*
+ * Modified copy of ZSTD_COMPRESSBOUND / ZSTD_compressBound() from zstd 1.5.7, with the argument
+ * type changed from size_t (which may be only 32 bits) to uint64_t, and removes the error handling.
+ */
+ static uint64_t zstdCompressBound(uint64_t length)
+ {
+ return length + (length >> 8)
+ + ((length < (128uLL << 10)) ? (((128uLL << 10) - length) >> 11) /* margin, from 64 to 0 */ : 0uLL);
+ }
+
+ bool zstdCompress(IStream& source, uint64_t sourceLength, IStream& dest, ZstdMetadata metadata, int16_t level)
+ {
+ Guard::Assert(sourceLength <= source.GetLength() - source.GetPosition());
+
+ size_t ret;
+ StreamReadBuffer sourceBuf(source, sourceLength, ZSTD_CStreamInSize());
+ StreamWriteBuffer destBuf(dest, zstdCompressBound(sourceLength), ZSTD_CStreamOutSize());
+ unsigned metaFlags = static_cast(metadata);
+
+ const auto deleter = [](ZSTD_CCtx* ptr) { ZSTD_freeCCtx(ptr); };
+ std::unique_ptr ctx(ZSTD_createCCtx(), deleter);
+ if (ctx == nullptr)
+ {
+ LOG_ERROR("Failed to create zstd context");
+ return false;
+ }
+
+ ret = ZSTD_CCtx_setParameter(ctx.get(), ZSTD_c_compressionLevel, level);
+ if (ZSTD_isError(ret))
+ {
+ LOG_ERROR("Failed to set compression level with error: %s", ZSTD_getErrorName(ret));
+ return false;
+ }
+ // set options for content size (default on) and checksum (default off)
+ ret = ZSTD_CCtx_setParameter(ctx.get(), ZSTD_c_contentSizeFlag, (metaFlags & 1) != 0);
+ if (ZSTD_isError(ret))
+ {
+ LOG_ERROR("Failed to set content size flag with error: %s", ZSTD_getErrorName(ret));
+ return false;
+ }
+ ret = ZSTD_CCtx_setParameter(ctx.get(), ZSTD_c_checksumFlag, (metaFlags & 2) != 0);
+ if (ZSTD_isError(ret))
+ {
+ LOG_ERROR("Failed to set checksum flag with error: %s", ZSTD_getErrorName(ret));
+ return false;
+ }
+ // unlike gzip, zstd puts the decompressed content size at the start of the file,
+ // so we need to tell zstd how big the input is before we start compressing.
+ ret = ZSTD_CCtx_setPledgedSrcSize(ctx.get(), sourceLength);
+ if (ZSTD_isError(ret))
+ {
+ LOG_ERROR("Failed to set file length with error: %s", ZSTD_getErrorName(ret));
+ return false;
+ }
+
+ do
+ {
+ auto readBlock = sourceBuf.ReadBlock(source);
+ ZSTD_inBuffer input = { readBlock.first, readBlock.second, 0 };
+
+ do
+ {
+ Guard::Assert(destBuf, "Compression Overruns Ouput Size");
+
+ auto writeBlock = destBuf.WriteBlockStart();
+ ZSTD_outBuffer output = { writeBlock.first, writeBlock.second, 0 };
+
+ ret = ZSTD_compressStream2(ctx.get(), &output, &input, sourceBuf ? ZSTD_e_continue : ZSTD_e_end);
+ if (ZSTD_isError(ret))
+ {
+ LOG_ERROR("Failed to compress data with error: %s", ZSTD_getErrorName(ret));
+ return false;
+ }
+
+ destBuf.WriteBlockCommit(dest, output.pos);
+ } while (input.pos < input.size || (!sourceBuf && ret > 0));
+ } while (sourceBuf);
+
+ return true;
+ }
+
+ bool zstdDecompress(IStream& source, uint64_t sourceLength, IStream& dest, uint64_t decompressLength)
+ {
+ Guard::Assert(sourceLength <= source.GetLength() - source.GetPosition());
+
+ size_t ret;
+ StreamReadBuffer sourceBuf(source, sourceLength, ZSTD_DStreamInSize());
+ StreamWriteBuffer destBuf(dest, decompressLength, ZSTD_DStreamOutSize());
+
+ const auto deleter = [](ZSTD_DCtx* ptr) { ZSTD_freeDCtx(ptr); };
+ std::unique_ptr ctx(ZSTD_createDCtx(), deleter);
+ if (ctx == nullptr)
+ {
+ LOG_ERROR("Failed to create zstd context");
+ return false;
+ }
+
+ do
+ {
+ auto readBlock = sourceBuf.ReadBlock(source);
+ ZSTD_inBuffer input = { readBlock.first, readBlock.second, 0 };
+
+ do
+ {
+ if (!destBuf)
+ {
+ LOG_ERROR("Decompressed data larger than expected");
+ return false;
+ }
+
+ auto writeBlock = destBuf.WriteBlockStart();
+ ZSTD_outBuffer output = { writeBlock.first, writeBlock.second, 0 };
+
+ ret = ZSTD_decompressStream(ctx.get(), &output, &input);
+ if (ZSTD_isError(ret))
+ {
+ LOG_ERROR("Failed to compress data with error: %s", ZSTD_getErrorName(ret));
+ return false;
+ }
+
+ destBuf.WriteBlockCommit(dest, output.pos);
+ } while (input.pos < input.size || (!sourceBuf && ret > 0));
+ } while (sourceBuf);
+
+ if (destBuf)
+ {
+ LOG_ERROR("Decompressed data smaller than expected");
+ return false;
+ }
+
+ return true;
+ }
} // namespace OpenRCT2::Compression
diff --git a/src/openrct2/core/Compression.h b/src/openrct2/core/Compression.h
index 8ab7b2eea5..ecbf5cf054 100644
--- a/src/openrct2/core/Compression.h
+++ b/src/openrct2/core/Compression.h
@@ -17,7 +17,7 @@
namespace OpenRCT2::Compression
{
- // zlib doesn't use 0 as a real compression level, so use it to mean no compression
+ // both zlib and zstd don't use 0 as a real compression level, so use it to mean no compression
constexpr int16_t kNoCompressionLevel = 0;
// Zlib methods, using the DEFLATE compression algorithm
@@ -36,4 +36,22 @@ namespace OpenRCT2::Compression
int16_t level = kZlibDefaultCompressionLevel);
bool zlibDecompress(
IStream& source, uint64_t sourceLength, IStream& dest, uint64_t decompressLength, ZlibHeaderType header);
+
+ // Zstd methods, using the ZStandard compression algorithm
+ constexpr int16_t kZstdDefaultCompressionLevel = 3;
+
+ // Options for optional metadata to attach to a ZStandard frame. Zstd default is length-only,
+ // but callers to zstdCompress should use whatever is not already duplicated by other headers.
+ enum class ZstdMetadata
+ {
+ none = 0,
+ length = 1,
+ checksum = 2,
+ both = 3,
+ };
+
+ bool zstdCompress(
+ IStream& source, uint64_t sourceLength, IStream& dest, ZstdMetadata metadata,
+ int16_t level = kZstdDefaultCompressionLevel);
+ bool zstdDecompress(IStream& source, uint64_t sourceLength, IStream& dest, uint64_t decompressLength);
} // namespace OpenRCT2::Compression
diff --git a/src/openrct2/core/OrcaStream.hpp b/src/openrct2/core/OrcaStream.hpp
index 86b98d722a..997ccfaa09 100644
--- a/src/openrct2/core/OrcaStream.hpp
+++ b/src/openrct2/core/OrcaStream.hpp
@@ -41,6 +41,7 @@ namespace OpenRCT2
{
none,
gzip,
+ zstd,
};
private:
@@ -76,7 +77,7 @@ namespace OpenRCT2
int16_t _compressionLevel;
public:
- OrcaStream(IStream& stream, const Mode mode, int16_t compressionLevel = Compression::kZlibDefaultCompressionLevel)
+ OrcaStream(IStream& stream, const Mode mode, int16_t compressionLevel = Compression::kNoCompressionLevel)
{
_stream = &stream;
_mode = mode;
@@ -104,6 +105,10 @@ namespace OpenRCT2
*_stream, _header.CompressedSize, _buffer, _header.UncompressedSize,
Compression::ZlibHeaderType::gzip);
break;
+ case CompressionType::zstd:
+ decompressStatus = Compression::zstdDecompress(
+ *_stream, _header.CompressedSize, _buffer, _header.UncompressedSize);
+ break;
default:
throw IOException("Unknown Park Compression Type");
}
@@ -131,7 +136,7 @@ namespace OpenRCT2
{
_header = {};
_header.Compression = _compressionLevel == Compression::kNoCompressionLevel ? CompressionType::none
- : CompressionType::gzip;
+ : CompressionType::zstd;
_buffer = MemoryStream{};
}
}
@@ -163,6 +168,11 @@ namespace OpenRCT2
compressStatus = Compression::zlibCompress(
_buffer, _buffer.GetLength(), compressed, Compression::ZlibHeaderType::gzip, _compressionLevel);
break;
+ case CompressionType::zstd:
+ // PARK header already has length and checksum, so exclude them in the compression frame
+ compressStatus = Compression::zstdCompress(
+ _buffer, _buffer.GetLength(), compressed, Compression::ZstdMetadata::none, _compressionLevel);
+ break;
default:
break;
}
diff --git a/src/openrct2/network/NetworkBase.cpp b/src/openrct2/network/NetworkBase.cpp
index bf50997b2a..3564ad7681 100644
--- a/src/openrct2/network/NetworkBase.cpp
+++ b/src/openrct2/network/NetworkBase.cpp
@@ -49,7 +49,7 @@ using namespace OpenRCT2;
// It is used for making sure only compatible builds get connected, even within
// single OpenRCT2 version.
-constexpr uint8_t kNetworkStreamVersion = 0;
+constexpr uint8_t kNetworkStreamVersion = 1;
const std::string kNetworkStreamID = std::string(kOpenRCT2Version) + "-" + std::to_string(kNetworkStreamVersion);
@@ -2876,7 +2876,7 @@ bool NetworkBase::SaveMap(IStream* stream, const std::vectorExportObjectsList = objects;
auto& gameState = getGameState();
- exporter->Export(gameState, *stream);
+ exporter->Export(gameState, *stream, kParkFileNetCompressionLevel);
result = true;
}
catch (const std::exception& e)
diff --git a/src/openrct2/park/ParkFile.cpp b/src/openrct2/park/ParkFile.cpp
index 306f797cba..d650d42f2b 100644
--- a/src/openrct2/park/ParkFile.cpp
+++ b/src/openrct2/park/ParkFile.cpp
@@ -17,6 +17,7 @@
#include "../OpenRCT2.h"
#include "../ParkImporter.h"
#include "../Version.h"
+#include "../config/Config.h"
#include "../core/Console.hpp"
#include "../core/Crypt.h"
#include "../core/DataSerialiser.h"
@@ -2766,7 +2767,7 @@ int32_t ScenarioSave(GameState_t& gameState, u8string_view path, int32_t flags)
{
// s6exporter->SaveGame(path);
}
- parkFile->Save(gameState, path, Compression::kZlibDefaultCompressionLevel);
+ parkFile->Save(gameState, path, gIsAutosave ? kParkFileAutoCompressionLevel : kParkFileSaveCompressionLevel);
result = true;
}
catch (const std::exception& e)
diff --git a/src/openrct2/park/ParkFile.h b/src/openrct2/park/ParkFile.h
index e6475ce67f..e8f3b0f586 100644
--- a/src/openrct2/park/ParkFile.h
+++ b/src/openrct2/park/ParkFile.h
@@ -22,10 +22,10 @@ namespace OpenRCT2
struct GameState_t;
// Current version that is saved.
- constexpr uint32_t kParkFileCurrentVersion = 56;
+ constexpr uint32_t kParkFileCurrentVersion = 57;
// The minimum version that is forwards compatible with the current version.
- constexpr uint32_t kParkFileMinVersion = 55;
+ constexpr uint32_t kParkFileMinVersion = 57;
// The minimum version that is backwards compatible with the current version.
// If this is increased beyond 0, uncomment the checks in ParkFile.cpp and Context.cpp!
@@ -33,6 +33,11 @@ namespace OpenRCT2
constexpr uint32_t kParkFileMagic = 0x4B524150; // PARK
+ // ZStd compression levels to use for various types of saves
+ constexpr int16_t kParkFileSaveCompressionLevel = 7;
+ constexpr int16_t kParkFileAutoCompressionLevel = 4;
+ constexpr int16_t kParkFileNetCompressionLevel = 4;
+
struct IStream;
// As uint16_t, in order to allow comparison with int32_t
@@ -62,11 +67,7 @@ namespace OpenRCT2
public:
std::vector ExportObjectsList;
- void Export(
- OpenRCT2::GameState_t& gameState, std::string_view path,
- int16_t compressionLevel = Compression::kZlibDefaultCompressionLevel);
- void Export(
- OpenRCT2::GameState_t& gameState, OpenRCT2::IStream& stream,
- int16_t compressionLevel = Compression::kZlibDefaultCompressionLevel);
+ void Export(OpenRCT2::GameState_t& gameState, std::string_view path, int16_t compressionLevel);
+ void Export(OpenRCT2::GameState_t& gameState, OpenRCT2::IStream& stream, int16_t compressionLevel);
};
} // namespace OpenRCT2
diff --git a/src/openrct2/platform/Crash.cpp b/src/openrct2/platform/Crash.cpp
index 5264414ca3..be3345cb10 100644
--- a/src/openrct2/platform/Crash.cpp
+++ b/src/openrct2/platform/Crash.cpp
@@ -139,6 +139,8 @@ static bool OnCrash(
FileStream source(dumpFilePath, FileMode::open);
FileStream dest(dumpFilePathGZIP, FileMode::write);
+ // We could switch this to zstdCompress() if supported by backtrace.io. If you switch it,
+ // use the extension .zst and ZstdMetadataType::both to use the appropriate metadata.
if (Compression::zlibCompress(source, source.GetLength(), dest, Compression::ZlibHeaderType::gzip))
{
// TODO: enable upload of gzip-compressed dumps once supported on
@@ -183,7 +185,7 @@ static bool OnCrash(
exporter->ExportObjectsList = objManager.GetPackableObjects();
auto& gameState = getGameState();
- exporter->Export(gameState, saveFilePathUTF8.c_str());
+ exporter->Export(gameState, saveFilePathUTF8.c_str(), kParkFileSaveCompressionLevel);
savedGameDumped = true;
}
catch (const std::exception& e)
diff --git a/test/tests/S6ImportExportTests.cpp b/test/tests/S6ImportExportTests.cpp
index 5fdefda7fc..cccee12051 100644
--- a/test/tests/S6ImportExportTests.cpp
+++ b/test/tests/S6ImportExportTests.cpp
@@ -120,7 +120,7 @@ static bool ExportSave(MemoryStream& stream, std::unique_ptr& context)
exporter->ExportObjectsList = objManager.GetPackableObjects();
auto& gameState = getGameState();
- exporter->Export(gameState, stream);
+ exporter->Export(gameState, stream, OpenRCT2::kParkFileSaveCompressionLevel);
return true;
}