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; }