diff --git a/OpenRCT2.xcodeproj/project.pbxproj b/OpenRCT2.xcodeproj/project.pbxproj index be9e413636..21d2880ca4 100644 --- a/OpenRCT2.xcodeproj/project.pbxproj +++ b/OpenRCT2.xcodeproj/project.pbxproj @@ -70,6 +70,7 @@ 932A211E22D73CFA00C57EDB /* GameActionCompat.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 932A20CF22D73CEE00C57EDB /* GameActionCompat.cpp */; }; 932A211F22D73CFA00C57EDB /* GameActionRegistration.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 932A20D322D73CEF00C57EDB /* GameActionRegistration.cpp */; }; 932A212022D73CFA00C57EDB /* GameAction.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 932A211C22D73CFA00C57EDB /* GameAction.cpp */; }; + 933C55B524B858490057E64B /* SeaDecrypt.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 933C55B424B858490057E64B /* SeaDecrypt.cpp */; }; 933CBDB520CB1ACD00134678 /* Widget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 933CBDB120CB1ACC00134678 /* Widget.cpp */; }; 933CBDB620CB1ACD00134678 /* Theme.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 933CBDB220CB1ACD00134678 /* Theme.cpp */; }; 933CBDBB20CB1B3F00134678 /* TitleSequencePlayer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 933CBDB920CB1B3F00134678 /* TitleSequencePlayer.cpp */; }; @@ -1061,6 +1062,7 @@ 932A211B22D73CFA00C57EDB /* TrackSetBrakeSpeedAction.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TrackSetBrakeSpeedAction.hpp; sourceTree = ""; }; 932A211C22D73CFA00C57EDB /* GameAction.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = GameAction.cpp; sourceTree = ""; }; 932A211D22D73CFA00C57EDB /* LargeSceneryRemoveAction.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = LargeSceneryRemoveAction.hpp; sourceTree = ""; }; + 933C55B424B858490057E64B /* SeaDecrypt.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SeaDecrypt.cpp; sourceTree = ""; }; 933CBDB120CB1ACC00134678 /* Widget.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Widget.cpp; sourceTree = ""; }; 933CBDB220CB1ACD00134678 /* Theme.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Theme.cpp; sourceTree = ""; }; 933CBDB320CB1ACD00134678 /* Theme.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Theme.h; sourceTree = ""; }; @@ -3158,6 +3160,7 @@ F76C847D1EC4E7CC00FA49E2 /* S6Exporter.cpp */, F76C847E1EC4E7CC00FA49E2 /* S6Exporter.h */, F76C847F1EC4E7CC00FA49E2 /* S6Importer.cpp */, + 933C55B424B858490057E64B /* SeaDecrypt.cpp */, 01C6F0C522FD51FC0057E2F7 /* T6Exporter.cpp */, 01C6F0C722FD51FC0057E2F7 /* T6Exporter.h */, 01C6F0C622FD51FC0057E2F7 /* T6Importer.cpp */, @@ -4202,6 +4205,7 @@ C68878E220289B9B0084B384 /* Staff.cpp in Sources */, F76C85CF1EC4E88300FA49E2 /* Console.cpp in Sources */, C68878DC20289B9B0084B384 /* Painter.cpp in Sources */, + 933C55B524B858490057E64B /* SeaDecrypt.cpp in Sources */, C688790120289B9B0084B384 /* ReverserRollerCoaster.cpp in Sources */, C688786120289A0A0084B384 /* MapAnimation.cpp in Sources */, F76C85D11EC4E88300FA49E2 /* Diagnostics.cpp in Sources */, diff --git a/distribution/changelog.txt b/distribution/changelog.txt index d462dfd729..21befc88d6 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -13,6 +13,7 @@ - Feature: [#11788] Command to extract images from a .DAT file. - Feature: [#11959] Hacked go-kart tracks can now use 2x2 bends, 3x3 bends and S-bends. - Feature: [#12090] Boosters for the Wooden Roller Coaster (if the "Show all track pieces" cheat is enabled). +- Feature: [#12184] .sea (RCT Classic) scenario files can now be imported. - Change: [#11209] Warn when user is running OpenRCT2 through Wine. - Change: [#11358] Switch copy and paste button positions in tile inspector. - Change: [#11449] Remove complete circuit requirement from Air Powered Vertical Coaster (for RCT1 parity). diff --git a/src/openrct2-ui/scripting/CustomWindow.cpp b/src/openrct2-ui/scripting/CustomWindow.cpp index 9c279d537c..05a6f8b91c 100644 --- a/src/openrct2-ui/scripting/CustomWindow.cpp +++ b/src/openrct2-ui/scripting/CustomWindow.cpp @@ -1205,7 +1205,7 @@ namespace OpenRCT2::Ui::Windows { if (customWidgetInfo->Name == name) { - return i; + return static_cast(i); } } } diff --git a/src/openrct2-ui/windows/LoadSave.cpp b/src/openrct2-ui/windows/LoadSave.cpp index 8e98422ef5..6e80c7965c 100644 --- a/src/openrct2-ui/windows/LoadSave.cpp +++ b/src/openrct2-ui/windows/LoadSave.cpp @@ -150,7 +150,7 @@ static std::vector _listItems; static char _directory[MAX_PATH]; static char _shortenedDirectory[MAX_PATH]; static char _parentDirectory[MAX_PATH]; -static char _extension[32]; +static char _extension[256]; static char _defaultName[MAX_PATH]; static int32_t _type; @@ -218,10 +218,10 @@ static const char* getFilterPatternByType(const int32_t type, const bool isSave) switch (type & 0x0E) { case LOADSAVETYPE_GAME: - return isSave ? "*.sv6" : "*.sv6;*.sc6;*.sc4;*.sv4;*.sv7"; + return isSave ? "*.sv6" : "*.sv6;*.sc6;*.sc4;*.sv4;*.sv7;*.sea;"; case LOADSAVETYPE_LANDSCAPE: - return isSave ? "*.sc6" : "*.sc6;*.sv6;*.sc4;*.sv4;*.sv7"; + return isSave ? "*.sc6" : "*.sc6;*.sv6;*.sc4;*.sv4;*.sv7;*.sea;"; case LOADSAVETYPE_SCENARIO: return "*.sc6"; diff --git a/src/openrct2-win/openrct2-win.cpp b/src/openrct2-win/openrct2-win.cpp index 0e882a7be7..4ba50c15f0 100644 --- a/src/openrct2-win/openrct2-win.cpp +++ b/src/openrct2-win/openrct2-win.cpp @@ -18,6 +18,7 @@ // Then the rest #include +#include #include #include #include diff --git a/src/openrct2/Context.cpp b/src/openrct2/Context.cpp index 8d8abe7aac..0a9e830673 100644 --- a/src/openrct2/Context.cpp +++ b/src/openrct2/Context.cpp @@ -541,8 +541,22 @@ namespace OpenRCT2 log_verbose("Context::LoadParkFromFile(%s)", path.c_str()); try { - auto fs = FileStream(path, FILE_MODE_OPEN); - return LoadParkFromStream(&fs, path, loadTitleScreenOnFail); + if (String::Equals(Path::GetExtension(path), ".sea", true)) + { + auto data = DecryptSea(fs::u8path(path)); + auto ms = MemoryStream(data.data(), data.size(), MEMORY_ACCESS::READ); + if (!LoadParkFromStream(&ms, path, loadTitleScreenOnFail)) + { + Console::Error::WriteLine(".sea file may have been renamed."); + return false; + } + return true; + } + else + { + auto fs = FileStream(path, FILE_MODE_OPEN); + return LoadParkFromStream(&fs, path, loadTitleScreenOnFail); + } } catch (const std::exception& e) { diff --git a/src/openrct2/core/FileSystem.hpp b/src/openrct2/core/FileSystem.hpp index 2f1c1fd6b8..2924f16de2 100644 --- a/src/openrct2/core/FileSystem.hpp +++ b/src/openrct2/core/FileSystem.hpp @@ -43,6 +43,7 @@ namespace fs = std::filesystem; # define WIN32_LEAN_AND_MEAN # endif # define BITMAP WIN32_BITMAP +# define PATTERN WIN32_PATTERN # endif # include # ifdef _WIN32 @@ -50,6 +51,7 @@ namespace fs = std::filesystem; # undef CreateWindow # undef GetMessage # undef BITMAP +# undef PATTERN # endif namespace fs = ghc::filesystem; #endif diff --git a/src/openrct2/libopenrct2.vcxproj b/src/openrct2/libopenrct2.vcxproj index 70d0434993..b65f8a4704 100644 --- a/src/openrct2/libopenrct2.vcxproj +++ b/src/openrct2/libopenrct2.vcxproj @@ -629,6 +629,7 @@ + diff --git a/src/openrct2/rct2/RCT2.h b/src/openrct2/rct2/RCT2.h index 85282def1d..09598ea66f 100644 --- a/src/openrct2/rct2/RCT2.h +++ b/src/openrct2/rct2/RCT2.h @@ -10,11 +10,14 @@ #pragma once #include "../common.h" +#include "../core/FileSystem.hpp" #include "../object/Object.h" #include "../rct12/RCT12.h" #include "../ride/RideRatings.h" #include "../ride/Vehicle.h" +#include + constexpr const uint8_t RCT2_MAX_STAFF = 200; constexpr const uint8_t RCT2_MAX_BANNERS_IN_PARK = 250; constexpr const uint8_t RCT2_MAX_VEHICLES_PER_RIDE = 31; @@ -760,3 +763,5 @@ struct RCT2RideRatingCalculationData assert_struct_size(RCT2RideRatingCalculationData, 76); #pragma pack(pop) + +std::vector DecryptSea(const fs::path& path); diff --git a/src/openrct2/rct2/SeaDecrypt.cpp b/src/openrct2/rct2/SeaDecrypt.cpp new file mode 100644 index 0000000000..89b427edf4 --- /dev/null +++ b/src/openrct2/rct2/SeaDecrypt.cpp @@ -0,0 +1,96 @@ +/***************************************************************************** + * Copyright (c) 2014-2020 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#include "../common.h" +#include "../core/File.h" +#include "../core/Path.hpp" +#include "RCT2.h" + +#include +#include +#include + +constexpr int32_t MASK_SIZE = 0x1000; + +struct EncryptionKey +{ + uint32_t Seed0{}; + uint32_t Seed1{}; +}; + +static EncryptionKey GetEncryptionKey(const std::string_view& fileName) +{ + auto fileNameLen = static_cast(fileName.size()); + uint32_t s0 = 0; + for (int i = fileNameLen - 1; i >= 0; i--) + { + s0 = (s0 + (s0 << 5)) ^ fileName[i]; + } + + uint32_t s1 = 0; + for (int i = 0; i < fileNameLen; i++) + { + s1 = (s1 + (s1 << 5)) ^ fileName[i]; + } + + return EncryptionKey{ s0, s1 }; +} + +static std::vector CreateMask(const EncryptionKey& key) +{ + std::vector result; + result.resize(MASK_SIZE); + uint32_t seed0 = key.Seed0; + uint32_t seed1 = key.Seed1; + for (size_t i = 0; i < MASK_SIZE; i += 4) + { + uint32_t s0 = seed0; + uint32_t s1 = seed1 ^ 0xF7654321; + seed0 = rol32(s1, 25) + s0; + seed1 = rol32(s0, 29); + result[i + 0] = (s0 >> 3) & 0xFF; + result[i + 1] = (s0 >> 11) & 0xFF; + result[i + 2] = (s0 >> 19) & 0xFF; + result[i + 3] = (seed1 >> 24) & 0xFF; + } + return result; +} + +static void Decrypt(std::vector& data, const EncryptionKey& key) +{ + auto mask = CreateMask(key); + uint32_t b = 0; + uint32_t c = 0; + for (size_t i = 0; i < data.size(); i++) + { + auto a = b % MASK_SIZE; + c = c % MASK_SIZE; + b = (a + 1) % MASK_SIZE; + + data[i] = (((data[i] - mask[b]) ^ mask[c]) + mask[a]) & 0xFF; + + c += 3; + b = a + 7; + } +} + +std::vector DecryptSea(const fs::path& path) +{ + auto key = GetEncryptionKey(path.filename().u8string()); + auto data = File::ReadAllBytes(path.u8string()); + + // Last 4 bytes is the checksum + size_t inputSize = data.size() - 4; + uint32_t checksum; + std::memcpy(&checksum, data.data() + inputSize, sizeof(checksum)); + data.resize(inputSize); + + Decrypt(data, key); + return data; +} diff --git a/test/tests/S6ImportExportTests.cpp b/test/tests/S6ImportExportTests.cpp index 536272f734..f5ebe358d4 100644 --- a/test/tests/S6ImportExportTests.cpp +++ b/test/tests/S6ImportExportTests.cpp @@ -1,5 +1,5 @@ /***************************************************************************** - * Copyright (c) 2014-2019 OpenRCT2 developers + * Copyright (c) 2014-2020 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -566,3 +567,13 @@ TEST(S6ImportExportAdvanceTicks, all) SUCCEED(); } + +TEST(SeaDecrypt, DecryptSea) +{ + auto path = TestData::GetParkPath("volcania.sea"); + auto decrypted = DecryptSea(path); + auto sha1 = Crypt::SHA1(decrypted.data(), decrypted.size()); + std::array expected = { 0x1B, 0x85, 0xFC, 0xC0, 0xE8, 0x9B, 0xBE, 0x72, 0xD9, 0x1F, + 0x6E, 0xC8, 0xB1, 0xFF, 0xEC, 0x70, 0x2A, 0x72, 0x05, 0xBB }; + ASSERT_EQ(sha1, expected); +} diff --git a/test/tests/testdata/parks/volcania.sea b/test/tests/testdata/parks/volcania.sea new file mode 100644 index 0000000000..f79c5fa1ec Binary files /dev/null and b/test/tests/testdata/parks/volcania.sea differ