1
0
mirror of https://github.com/OpenRCT2/OpenRCT2 synced 2025-12-10 17:42:29 +01:00

Use ZStandard for Park and Replay Files (#24734)

This commit is contained in:
LRFLEW
2025-08-06 14:50:18 -05:00
committed by GitHub
parent 8ef4b207b9
commit 52e3c774bc
21 changed files with 284 additions and 61 deletions

View File

@@ -489,7 +489,7 @@ jobs:
name: Ubuntu Linux (AppImage, x86_64) name: Ubuntu Linux (AppImage, x86_64)
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [check-code-formatting, build_variables] needs: [check-code-formatting, build_variables]
container: openrct2/openrct2-build:14-jammy container: openrct2/openrct2-build:21-jammy
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@@ -12,7 +12,7 @@ on:
jobs: jobs:
clang-tidy-check: clang-tidy-check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: openrct2/openrct2-build:19-noble container: openrct2/openrct2-build:20-noble
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: ZehMatt/clang-tidy-annotations@v1 - uses: ZehMatt/clang-tidy-annotations@v1

View File

@@ -1,5 +1,6 @@
0.4.26 (in development) 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. - Improved: [#24893] The ride list now has headers, and can be sorted in both directions.
- Fix: [#16988] AppImage version does not show changelog. - Fix: [#16988] AppImage version does not show changelog.

View File

@@ -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 Test | BSD 3 clause licence.
Google Benchmark | Apache 2.0 licence. Google Benchmark | Apache 2.0 licence.
sfl | zlib 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. Licences for sub-libraries used by the above may vary. For more information, visit the libraries' respective official websites.

View File

@@ -88,7 +88,7 @@
<GenerateDebugInformation>DebugFull</GenerateDebugInformation> <GenerateDebugInformation>DebugFull</GenerateDebugInformation>
<AdditionalDependencies>brotlicommon.lib;brotlidec.lib;brotlienc.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies>brotlicommon.lib;brotlidec.lib;brotlienc.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies Condition="'$(Breakpad)'=='true' and ('$(Platform)'=='Win32' or '$(Platform)'=='x64')">libbreakpadd.lib;libbreakpad_clientd.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies Condition="'$(Breakpad)'=='true' and ('$(Platform)'=='Win32' or '$(Platform)'=='x64')">libbreakpadd.lib;libbreakpad_clientd.lib;%(AdditionalDependencies)</AdditionalDependencies>
<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)</AdditionalDependencies> <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)</AdditionalDependencies>
</Link> </Link>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release' or '$(Configuration)'=='ReleaseLTCG'"> <ItemDefinitionGroup Condition="'$(Configuration)'=='Release' or '$(Configuration)'=='ReleaseLTCG'">
@@ -109,7 +109,7 @@
<OptimizeReferences>true</OptimizeReferences> <OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>brotlicommon.lib;brotlidec.lib;brotlienc.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies>brotlicommon.lib;brotlidec.lib;brotlienc.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies Condition="'$(Breakpad)'=='true' and ('$(Platform)'=='Win32' or '$(Platform)'=='x64')">libbreakpad.lib;libbreakpad_client.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies Condition="'$(Breakpad)'=='true' and ('$(Platform)'=='Win32' or '$(Platform)'=='x64')">libbreakpad.lib;libbreakpad_client.lib;%(AdditionalDependencies)</AdditionalDependencies>
<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)</AdditionalDependencies> <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)</AdditionalDependencies>
</Link> </Link>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />

View File

@@ -7,6 +7,7 @@ START_DIR=$(pwd)
SPEEXDSP_ROOT=/ext/speexdsp SPEEXDSP_ROOT=/ext/speexdsp
ICU_ROOT=/ext/icu/icu4c/source ICU_ROOT=/ext/icu/icu4c/source
LIBZIP_ROOT=/ext/libzip LIBZIP_ROOT=/ext/libzip
ZSTD_ROOT=/ext/zstd
JSON_DIR=/usr/include/nlohmann/ JSON_DIR=/usr/include/nlohmann/
emcmake cmake ../ \ emcmake cmake ../ \
@@ -25,7 +26,8 @@ emcmake cmake ../ \
-DICU_DATA_LIBRARIES=$ICU_ROOT/lib/libicuuc.so \ -DICU_DATA_LIBRARIES=$ICU_ROOT/lib/libicuuc.so \
-DICU_DT_LIBRARY_RELEASE="$ICU_ROOT/stubdata/libicudata.so" \ -DICU_DT_LIBRARY_RELEASE="$ICU_ROOT/stubdata/libicudata.so" \
-DLIBZIP_LIBRARIES="$LIBZIP_ROOT/build/lib/libzip.a" \ -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\"'" -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 emmake ninja

View File

@@ -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}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}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}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} ${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_STATIC_LIBRARY_PREFIX}SDL2main${CMAKE_STATIC_LIBRARY_SUFFIX}
LOG_DOWNLOAD 1 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/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/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/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) add_library(freetype SHARED IMPORTED)
@@ -98,6 +100,12 @@ set_target_properties(z PROPERTIES IMPORTED_LOCATION
) )
add_dependencies(z libs) 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) add_library(SDL2 SHARED IMPORTED)
set_target_properties(SDL2 PROPERTIES IMPORTED_LOCATION set_target_properties(SDL2 PROPERTIES IMPORTED_LOCATION
${CMAKE_BINARY_DIR}/libs/lib/${CMAKE_SHARED_LIBRARY_PREFIX}SDL2${CMAKE_SHARED_LIBRARY_SUFFIX} ${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") "${ORCT2_ROOT}/src/openrct2-cli/*.hpp")
add_library(openrct2 SHARED ${LIBOPENRCT2_SOURCES}) 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}) 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) target_link_libraries(openrct2-ui openrct2 android stdc++ GLESv1_CM GLESv2 SDL2main speexdsp brotlicommon brotlidec bz2 freetype ogg vorbis vorbisfile FLAC)

View File

@@ -121,12 +121,14 @@ if (EMSCRIPTEN)
elseif (MSVC) elseif (MSVC)
find_package(png 1.6 REQUIRED) find_package(png 1.6 REQUIRED)
find_package(zlib REQUIRED) find_package(zlib REQUIRED)
find_package(zstd REQUIRED)
find_path(LIBZIP_INCLUDE_DIRS zip.h) find_path(LIBZIP_INCLUDE_DIRS zip.h)
find_library(LIBZIP_LIBRARIES zip) find_library(LIBZIP_LIBRARIES zip)
else () else ()
PKG_CHECK_MODULES(LIBZIP REQUIRED IMPORTED_TARGET libzip>=1.0) PKG_CHECK_MODULES(LIBZIP REQUIRED IMPORTED_TARGET libzip>=1.0)
PKG_CHECK_MODULES(ZLIB REQUIRED IMPORTED_TARGET zlib) 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) PKG_CHECK_MODULES(PNG IMPORTED_TARGET libpng>=1.6)
if (NOT PNG_FOUND) if (NOT PNG_FOUND)
@@ -144,18 +146,21 @@ if (STATIC)
target_link_libraries(libopenrct2 target_link_libraries(libopenrct2
${PNG_STATIC_LIBRARIES} ${PNG_STATIC_LIBRARIES}
${ZLIB_STATIC_LIBRARIES} ${ZLIB_STATIC_LIBRARIES}
${LIBZIP_STATIC_LIBRARIES}) ${LIBZIP_STATIC_LIBRARIES}
${ZSTD_STATIC_LIBRARIES})
else () else ()
if (NOT MSVC AND NOT EMSCRIPTEN) if (NOT MSVC AND NOT EMSCRIPTEN)
target_link_libraries(libopenrct2 target_link_libraries(libopenrct2
PkgConfig::PNG PkgConfig::PNG
PkgConfig::ZLIB PkgConfig::ZLIB
PkgConfig::LIBZIP) PkgConfig::LIBZIP
PkgConfig::ZSTD)
else () else ()
target_link_libraries(libopenrct2 target_link_libraries(libopenrct2
${PNG_LIBRARIES} ${PNG_LIBRARIES}
${ZLIB_LIBRARIES} ${ZLIB_LIBRARIES}
${LIBZIP_LIBRARIES}) ${LIBZIP_LIBRARIES}
${ZSTD_LIBRARIES})
endif () endif ()
endif () endif ()
@@ -235,6 +240,7 @@ endif()
target_include_directories(libopenrct2 SYSTEM PRIVATE ${LIBZIP_INCLUDE_DIRS}) target_include_directories(libopenrct2 SYSTEM PRIVATE ${LIBZIP_INCLUDE_DIRS})
target_include_directories(libopenrct2 SYSTEM PRIVATE ${PNG_INCLUDE_DIRS} target_include_directories(libopenrct2 SYSTEM PRIVATE ${PNG_INCLUDE_DIRS}
${ZLIB_INCLUDE_DIRS}) ${ZLIB_INCLUDE_DIRS})
target_include_directories(libopenrct2 SYSTEM PRIVATE ${ZSTD_INCLUDE_DIRS})
include_directories(libopenrct2 SYSTEM ${CMAKE_CURRENT_LIST_DIR}/../thirdparty) include_directories(libopenrct2 SYSTEM ${CMAKE_CURRENT_LIST_DIR}/../thirdparty)
# To avoid unnecessary rebuilds set the current branch and # To avoid unnecessary rebuilds set the current branch and

View File

@@ -103,9 +103,10 @@ namespace OpenRCT2
class ReplayManager final : public IReplayManager 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 uint32_t kReplayMagic = 0x5243524F; // ORCR.
static constexpr int kReplayCompressionLevel = Compression::kZlibMaxCompressionLevel; static constexpr int kReplayCompressionLevel = 18;
static constexpr int kNormalRecordingChecksumTicks = 1; static constexpr int kNormalRecordingChecksumTicks = 1;
static constexpr int kSilentRecordingChecksumTicks = 40; // Same as network server static constexpr int kSilentRecordingChecksumTicks = 40; // Same as network server
@@ -313,8 +314,9 @@ namespace OpenRCT2
MemoryStream compressed; MemoryStream compressed;
stream.SetPosition(0); stream.SetPosition(0);
bool compressStatus = Compression::zlibCompress( // header already has decompressed length, but no checksum, so use the ZStandard checksum
stream, stream.GetLength(), compressed, Compression::ZlibHeaderType::zlib, kReplayCompressionLevel); bool compressStatus = Compression::zstdCompress(
stream, stream.GetLength(), compressed, Compression::ZstdMetadata::checksum, kReplayCompressionLevel);
if (!compressStatus) if (!compressStatus)
throw IOException("Compression Error"); throw IOException("Compression Error");
@@ -563,12 +565,18 @@ namespace OpenRCT2
MemoryStream decompressed; MemoryStream decompressed;
bool decompressStatus = true; bool decompressStatus = true;
recFile.data.SetPosition(0); recFile.data.SetPosition(0);
if (recFile.version <= 10)
{
decompressStatus = Compression::zlibDecompress( decompressStatus = Compression::zlibDecompress(
recFile.data, recFile.data.GetLength(), decompressed, recFile.uncompressedSize, recFile.data, recFile.data.GetLength(), decompressed, recFile.uncompressedSize,
Compression::ZlibHeaderType::zlib); Compression::ZlibHeaderType::zlib);
}
else
{
decompressStatus = Compression::zstdDecompress(
recFile.data, recFile.data.GetLength(), decompressed, recFile.uncompressedSize);
}
if (!decompressStatus) if (!decompressStatus)
throw IOException("Decompression Error"); throw IOException("Decompression Error");
@@ -683,7 +691,7 @@ namespace OpenRCT2
bool Compatible(ReplayRecordData& data) bool Compatible(ReplayRecordData& data)
{ {
return data.version == kReplayVersion; return data.version >= kReplayMinCompatVersion;
} }
bool Serialise(DataSerialiser& serialiser, ReplayRecordData& data) bool Serialise(DataSerialiser& serialiser, ReplayRecordData& data)

View File

@@ -108,6 +108,7 @@ consteval CommandLineCommand DefineSubCommand(const char* name, const CommandLin
namespace OpenRCT2::CommandLine namespace OpenRCT2::CommandLine
{ {
extern const CommandLineCommand kRootCommands[]; extern const CommandLineCommand kRootCommands[];
extern const CommandLineCommand kConvertCommands[];
extern const CommandLineCommand kScreenshotCommands[]; extern const CommandLineCommand kScreenshotCommands[];
extern const CommandLineCommand kSpriteCommands[]; extern const CommandLineCommand kSpriteCommands[];
extern const CommandLineCommand kSimulateCommands[]; extern const CommandLineCommand kSimulateCommands[];
@@ -118,6 +119,5 @@ namespace OpenRCT2::CommandLine
void PrintHelp(bool allCommands = false); void PrintHelp(bool allCommands = false);
exitcode_t HandleCommandDefault(); exitcode_t HandleCommandDefault();
exitcode_t HandleCommandConvert(CommandLineArgEnumerator* enumerator);
exitcode_t HandleCommandUri(CommandLineArgEnumerator* enumerator); exitcode_t HandleCommandUri(CommandLineArgEnumerator* enumerator);
} // namespace OpenRCT2::CommandLine } // namespace OpenRCT2::CommandLine

View File

@@ -12,6 +12,8 @@
#include "../GameState.h" #include "../GameState.h"
#include "../OpenRCT2.h" #include "../OpenRCT2.h"
#include "../ParkImporter.h" #include "../ParkImporter.h"
#include "../PlatformEnvironment.h"
#include "../config/Config.h"
#include "../core/Console.hpp" #include "../core/Console.hpp"
#include "../core/Path.hpp" #include "../core/Path.hpp"
#include "../object/ObjectManager.h" #include "../object/ObjectManager.h"
@@ -20,14 +22,33 @@
#include "CommandLine.hpp" #include "CommandLine.hpp"
#include <cassert> #include <cassert>
#include <limits>
#include <memory> #include <memory>
using namespace OpenRCT2; 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("", "<source> [destination]", kConvertOptions, HandleCommandConvert),
kCommandTableEnd
};
// clang-format on
static void WriteConvertFromAndToMessage(FileExtension sourceFileType, FileExtension destinationFileType); static void WriteConvertFromAndToMessage(FileExtension sourceFileType, FileExtension destinationFileType);
static u8string GetFileTypeFriendlyName(FileExtension fileType); static u8string GetFileTypeFriendlyName(FileExtension fileType);
exitcode_t CommandLine::HandleCommandConvert(CommandLineArgEnumerator* enumerator) static exitcode_t HandleCommandConvert(CommandLineArgEnumerator* enumerator)
{ {
exitcode_t result = CommandLine::HandleCommandDefault(); exitcode_t result = CommandLine::HandleCommandDefault();
if (result != EXITCODE_CONTINUE) if (result != EXITCODE_CONTINUE)
@@ -48,10 +69,10 @@ exitcode_t CommandLine::HandleCommandConvert(CommandLineArgEnumerator* enumerato
// Get the destination path // Get the destination path
const utf8* rawDestinationPath; const utf8* rawDestinationPath;
if (!enumerator->TryPopString(&rawDestinationPath)) if (!enumerator->TryPopString(&rawDestinationPath) || String::startsWith(rawDestinationPath, "-"))
{ {
Console::Error::WriteLine("Expected a destination path."); // if no destination path is provided, convert the park file in-place
return EXITCODE_FAIL; rawDestinationPath = rawSourcePath;
} }
const auto destinationPath = Path::GetAbsolute(rawDestinationPath); const auto destinationPath = Path::GetAbsolute(rawDestinationPath);
@@ -75,12 +96,12 @@ exitcode_t CommandLine::HandleCommandConvert(CommandLineArgEnumerator* enumerato
case FileExtension::PARK: case FileExtension::PARK:
if (destinationFileType == FileExtension::PARK) if (destinationFileType == FileExtension::PARK)
{ {
Console::Error::WriteLine("File is already an OpenRCT2 saved game or scenario."); Console::Error::WriteLine(
return EXITCODE_FAIL; "File is already an OpenRCT2 saved game or scenario. Updating file version and recompressing.");
} }
break; break;
default: 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; return EXITCODE_FAIL;
} }
@@ -125,7 +146,7 @@ exitcode_t CommandLine::HandleCommandConvert(CommandLineArgEnumerator* enumerato
auto* windowMgr = Ui::GetWindowManager(); auto* windowMgr = Ui::GetWindowManager();
windowMgr->CloseByClass(WindowClass::MainWindow); windowMgr->CloseByClass(WindowClass::MainWindow);
exporter->Export(gameState, destinationPath); exporter->Export(gameState, destinationPath, static_cast<int16_t>(_compressLevel));
} }
catch (const std::exception& ex) catch (const std::exception& ex)
{ {

View File

@@ -133,7 +133,6 @@ const CommandLineCommand CommandLine::kRootCommands[]
DefineCommand("join", "<hostname>", kStandardOptions, HandleCommandJoin ), DefineCommand("join", "<hostname>", kStandardOptions, HandleCommandJoin ),
#endif #endif
DefineCommand("set-rct2", "<path>", kStandardOptions, HandleCommandSetRCT2), DefineCommand("set-rct2", "<path>", kStandardOptions, HandleCommandSetRCT2),
DefineCommand("convert", "<source> <destination>", kStandardOptions, CommandLine::HandleCommandConvert),
DefineCommand("scan-objects", "<path>", kStandardOptions, HandleCommandScanObjects), DefineCommand("scan-objects", "<path>", kStandardOptions, HandleCommandScanObjects),
DefineCommand("handle-uri", "openrct2://.../", kStandardOptions, CommandLine::HandleCommandUri), DefineCommand("handle-uri", "openrct2://.../", kStandardOptions, CommandLine::HandleCommandUri),
@@ -142,6 +141,7 @@ const CommandLineCommand CommandLine::kRootCommands[]
#endif #endif
// Sub-commands // Sub-commands
DefineSubCommand("convert", CommandLine::kConvertCommands ),
DefineSubCommand("screenshot", CommandLine::kScreenshotCommands ), DefineSubCommand("screenshot", CommandLine::kScreenshotCommands ),
DefineSubCommand("sprite", CommandLine::kSpriteCommands ), DefineSubCommand("sprite", CommandLine::kSpriteCommands ),
DefineSubCommand("simulate", CommandLine::kSimulateCommands ), DefineSubCommand("simulate", CommandLine::kSimulateCommands ),

View File

@@ -23,26 +23,36 @@
using namespace OpenRCT2; using namespace OpenRCT2;
// clang-format off
static constexpr CommandLineOptionDefinition kNoOptions[]
{
kOptionTableEnd
};
static exitcode_t HandleSimulate(CommandLineArgEnumerator* argEnumerator); static exitcode_t HandleSimulate(CommandLineArgEnumerator* argEnumerator);
const CommandLineCommand CommandLine::kSimulateCommands[]{ // Main commands const CommandLineCommand CommandLine::kSimulateCommands[]{
DefineCommand("", "<ticks>", nullptr, HandleSimulate), // Main commands
DefineCommand("", "<park file> <ticks>", kNoOptions, HandleSimulate),
kCommandTableEnd kCommandTableEnd
}; };
// clang-format on
static exitcode_t HandleSimulate(CommandLineArgEnumerator* argEnumerator) static exitcode_t HandleSimulate(CommandLineArgEnumerator* argEnumerator)
{ {
const char** argv = const_cast<const char**>(argEnumerator->GetArguments()) + argEnumerator->GetIndex(); const utf8* inputPath;
int32_t argc = argEnumerator->GetCount() - argEnumerator->GetIndex(); if (!argEnumerator->TryPopString(&inputPath))
if (argc < 2)
{ {
Console::Error::WriteLine("Missing arguments <sv6-file> <ticks>."); Console::Error::WriteLine("Expected a save file path");
return EXITCODE_FAIL; return EXITCODE_FAIL;
} }
const char* inputPath = argv[0]; int32_t ticks;
uint32_t ticks = atol(argv[1]); if (!argEnumerator->TryPopInteger(&ticks))
{
Console::Error::WriteLine("Expected a number of ticks to simulate");
return EXITCODE_FAIL;
}
gOpenRCT2Headless = true; gOpenRCT2Headless = true;
@@ -59,7 +69,7 @@ static exitcode_t HandleSimulate(CommandLineArgEnumerator* argEnumerator)
} }
Console::WriteLine("Running %d ticks...", ticks); Console::WriteLine("Running %d ticks...", ticks);
for (uint32_t i = 0; i < ticks; i++) for (int32_t i = 0; i < ticks; i++)
{ {
gameStateUpdateLogic(); gameStateUpdateLogic();
} }

View File

@@ -19,12 +19,13 @@
#include <limits> #include <limits>
#include <zlib.h> #include <zlib.h>
#include <zstd.h>
namespace OpenRCT2::Compression namespace OpenRCT2::Compression
{ {
constexpr size_t kZlibChunkSize = 128 * 1024; static constexpr size_t kZlibChunkSize = 128 * 1024;
constexpr size_t kZlibMaxChunkSize = static_cast<size_t>(std::numeric_limits<uInt>::max()); static constexpr size_t kZlibMaxChunkSize = static_cast<size_t>(std::numeric_limits<uInt>::max());
constexpr int kZlibWindowBits[] = { -15, 15, 15 + 16 }; static constexpr int kZlibWindowBits[] = { -15, 15, 15 + 16 };
/* /*
* Modified copy of compressBound() from zlib 1.3.1, with the argument type changed from ULong * 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) 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) bool zlibCompress(IStream& source, uint64_t sourceLength, IStream& dest, ZlibHeaderType header, int16_t level)
@@ -138,4 +139,137 @@ namespace OpenRCT2::Compression
inflateEnd(&strm); inflateEnd(&strm);
return true; 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<unsigned>(metadata);
const auto deleter = [](ZSTD_CCtx* ptr) { ZSTD_freeCCtx(ptr); };
std::unique_ptr<ZSTD_CCtx, decltype(deleter)> 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<ZSTD_DCtx, decltype(deleter)> 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 } // namespace OpenRCT2::Compression

View File

@@ -17,7 +17,7 @@
namespace OpenRCT2::Compression 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; constexpr int16_t kNoCompressionLevel = 0;
// Zlib methods, using the DEFLATE compression algorithm // Zlib methods, using the DEFLATE compression algorithm
@@ -36,4 +36,22 @@ namespace OpenRCT2::Compression
int16_t level = kZlibDefaultCompressionLevel); int16_t level = kZlibDefaultCompressionLevel);
bool zlibDecompress( bool zlibDecompress(
IStream& source, uint64_t sourceLength, IStream& dest, uint64_t decompressLength, ZlibHeaderType header); 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 } // namespace OpenRCT2::Compression

View File

@@ -41,6 +41,7 @@ namespace OpenRCT2
{ {
none, none,
gzip, gzip,
zstd,
}; };
private: private:
@@ -76,7 +77,7 @@ namespace OpenRCT2
int16_t _compressionLevel; int16_t _compressionLevel;
public: 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; _stream = &stream;
_mode = mode; _mode = mode;
@@ -104,6 +105,10 @@ namespace OpenRCT2
*_stream, _header.CompressedSize, _buffer, _header.UncompressedSize, *_stream, _header.CompressedSize, _buffer, _header.UncompressedSize,
Compression::ZlibHeaderType::gzip); Compression::ZlibHeaderType::gzip);
break; break;
case CompressionType::zstd:
decompressStatus = Compression::zstdDecompress(
*_stream, _header.CompressedSize, _buffer, _header.UncompressedSize);
break;
default: default:
throw IOException("Unknown Park Compression Type"); throw IOException("Unknown Park Compression Type");
} }
@@ -131,7 +136,7 @@ namespace OpenRCT2
{ {
_header = {}; _header = {};
_header.Compression = _compressionLevel == Compression::kNoCompressionLevel ? CompressionType::none _header.Compression = _compressionLevel == Compression::kNoCompressionLevel ? CompressionType::none
: CompressionType::gzip; : CompressionType::zstd;
_buffer = MemoryStream{}; _buffer = MemoryStream{};
} }
} }
@@ -163,6 +168,11 @@ namespace OpenRCT2
compressStatus = Compression::zlibCompress( compressStatus = Compression::zlibCompress(
_buffer, _buffer.GetLength(), compressed, Compression::ZlibHeaderType::gzip, _compressionLevel); _buffer, _buffer.GetLength(), compressed, Compression::ZlibHeaderType::gzip, _compressionLevel);
break; 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: default:
break; break;
} }

View File

@@ -49,7 +49,7 @@ using namespace OpenRCT2;
// It is used for making sure only compatible builds get connected, even within // It is used for making sure only compatible builds get connected, even within
// single OpenRCT2 version. // single OpenRCT2 version.
constexpr uint8_t kNetworkStreamVersion = 0; constexpr uint8_t kNetworkStreamVersion = 1;
const std::string kNetworkStreamID = std::string(kOpenRCT2Version) + "-" + std::to_string(kNetworkStreamVersion); const std::string kNetworkStreamID = std::string(kOpenRCT2Version) + "-" + std::to_string(kNetworkStreamVersion);
@@ -2876,7 +2876,7 @@ bool NetworkBase::SaveMap(IStream* stream, const std::vector<const ObjectReposit
exporter->ExportObjectsList = objects; exporter->ExportObjectsList = objects;
auto& gameState = getGameState(); auto& gameState = getGameState();
exporter->Export(gameState, *stream); exporter->Export(gameState, *stream, kParkFileNetCompressionLevel);
result = true; result = true;
} }
catch (const std::exception& e) catch (const std::exception& e)

View File

@@ -17,6 +17,7 @@
#include "../OpenRCT2.h" #include "../OpenRCT2.h"
#include "../ParkImporter.h" #include "../ParkImporter.h"
#include "../Version.h" #include "../Version.h"
#include "../config/Config.h"
#include "../core/Console.hpp" #include "../core/Console.hpp"
#include "../core/Crypt.h" #include "../core/Crypt.h"
#include "../core/DataSerialiser.h" #include "../core/DataSerialiser.h"
@@ -2766,7 +2767,7 @@ int32_t ScenarioSave(GameState_t& gameState, u8string_view path, int32_t flags)
{ {
// s6exporter->SaveGame(path); // s6exporter->SaveGame(path);
} }
parkFile->Save(gameState, path, Compression::kZlibDefaultCompressionLevel); parkFile->Save(gameState, path, gIsAutosave ? kParkFileAutoCompressionLevel : kParkFileSaveCompressionLevel);
result = true; result = true;
} }
catch (const std::exception& e) catch (const std::exception& e)

View File

@@ -22,10 +22,10 @@ namespace OpenRCT2
struct GameState_t; struct GameState_t;
// Current version that is saved. // 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. // 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. // 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! // 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 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; struct IStream;
// As uint16_t, in order to allow comparison with int32_t // As uint16_t, in order to allow comparison with int32_t
@@ -62,11 +67,7 @@ namespace OpenRCT2
public: public:
std::vector<const ObjectRepositoryItem*> ExportObjectsList; std::vector<const ObjectRepositoryItem*> ExportObjectsList;
void Export( void Export(OpenRCT2::GameState_t& gameState, std::string_view path, int16_t compressionLevel);
OpenRCT2::GameState_t& gameState, std::string_view path, void Export(OpenRCT2::GameState_t& gameState, OpenRCT2::IStream& stream, int16_t compressionLevel);
int16_t compressionLevel = Compression::kZlibDefaultCompressionLevel);
void Export(
OpenRCT2::GameState_t& gameState, OpenRCT2::IStream& stream,
int16_t compressionLevel = Compression::kZlibDefaultCompressionLevel);
}; };
} // namespace OpenRCT2 } // namespace OpenRCT2

View File

@@ -139,6 +139,8 @@ static bool OnCrash(
FileStream source(dumpFilePath, FileMode::open); FileStream source(dumpFilePath, FileMode::open);
FileStream dest(dumpFilePathGZIP, FileMode::write); 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)) if (Compression::zlibCompress(source, source.GetLength(), dest, Compression::ZlibHeaderType::gzip))
{ {
// TODO: enable upload of gzip-compressed dumps once supported on // TODO: enable upload of gzip-compressed dumps once supported on
@@ -183,7 +185,7 @@ static bool OnCrash(
exporter->ExportObjectsList = objManager.GetPackableObjects(); exporter->ExportObjectsList = objManager.GetPackableObjects();
auto& gameState = getGameState(); auto& gameState = getGameState();
exporter->Export(gameState, saveFilePathUTF8.c_str()); exporter->Export(gameState, saveFilePathUTF8.c_str(), kParkFileSaveCompressionLevel);
savedGameDumped = true; savedGameDumped = true;
} }
catch (const std::exception& e) catch (const std::exception& e)

View File

@@ -120,7 +120,7 @@ static bool ExportSave(MemoryStream& stream, std::unique_ptr<IContext>& context)
exporter->ExportObjectsList = objManager.GetPackableObjects(); exporter->ExportObjectsList = objManager.GetPackableObjects();
auto& gameState = getGameState(); auto& gameState = getGameState();
exporter->Export(gameState, stream); exporter->Export(gameState, stream, OpenRCT2::kParkFileSaveCompressionLevel);
return true; return true;
} }