diff --git a/OpenRCT2.xcodeproj/project.pbxproj b/OpenRCT2.xcodeproj/project.pbxproj index 2cf84296cc..f33e59c269 100644 --- a/OpenRCT2.xcodeproj/project.pbxproj +++ b/OpenRCT2.xcodeproj/project.pbxproj @@ -240,6 +240,7 @@ C6E96E321E04072F0076A04F /* TitleSequencePlayer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C6E96E2D1E04072F0076A04F /* TitleSequencePlayer.cpp */; }; C6E96E361E0408B40076A04F /* libzip.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = C6E96E351E0408B40076A04F /* libzip.dylib */; }; C6E96E371E040E040076A04F /* libzip.dylib in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C6E96E351E0408B40076A04F /* libzip.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + C6EABCC41E719691008C09AB /* UriHandler.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C6EABCC31E719691008C09AB /* UriHandler.cpp */; }; D41B73EF1C2101890080A7B9 /* libcurl.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D41B73EE1C2101890080A7B9 /* libcurl.tbd */; }; D41B741D1C210A7A0080A7B9 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D41B741C1C210A7A0080A7B9 /* libiconv.tbd */; }; D41B74731C2125E50080A7B9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D41B74721C2125E50080A7B9 /* Assets.xcassets */; }; @@ -716,6 +717,7 @@ C6E96E331E0408A80076A04F /* zip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = zip.h; sourceTree = ""; }; C6E96E341E0408A80076A04F /* zipconf.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = zipconf.h; sourceTree = ""; }; C6E96E351E0408B40076A04F /* libzip.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = libzip.dylib; sourceTree = ""; }; + C6EABCC31E719691008C09AB /* UriHandler.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = UriHandler.cpp; sourceTree = ""; }; C6FF1BAD1DBCE1A10078DCB5 /* junior_roller_coaster.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = junior_roller_coaster.h; sourceTree = ""; }; D41B73EE1C2101890080A7B9 /* libcurl.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libcurl.tbd; path = usr/lib/libcurl.tbd; sourceTree = SDKROOT; }; D41B741C1C210A7A0080A7B9 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; }; @@ -1686,6 +1688,7 @@ D44270D91CC81B3200D84D28 /* RootCommands.cpp */, D44270DA1CC81B3200D84D28 /* ScreenshotCommands.cpp */, D44270DB1CC81B3200D84D28 /* SpriteCommands.cpp */, + C6EABCC31E719691008C09AB /* UriHandler.cpp */, ); path = cmdline; sourceTree = ""; @@ -2866,6 +2869,7 @@ C686F8B91CDBC37E009F9BFC /* supports.c in Sources */, D442726E1CC81B3200D84D28 /* maze_construction.c in Sources */, D44272751CC81B3200D84D28 /* news_options.c in Sources */, + C6EABCC41E719691008C09AB /* UriHandler.cpp in Sources */, D44272551CC81B3200D84D28 /* changelog.c in Sources */, C686F9471CDBC3B7009F9BFC /* top_spin.c in Sources */, D442720E1CC81B3200D84D28 /* rect.c in Sources */, diff --git a/src/openrct2/OpenRCT2.cpp b/src/openrct2/OpenRCT2.cpp index 9de2e9cf6f..06f82fda1a 100644 --- a/src/openrct2/OpenRCT2.cpp +++ b/src/openrct2/OpenRCT2.cpp @@ -14,16 +14,18 @@ *****************************************************************************/ #pragma endregion +#include #include #include "core/Console.hpp" -#include "core/Guard.hpp" #include "core/File.h" #include "core/FileStream.hpp" +#include "core/Guard.hpp" #include "core/String.hpp" #include "FileClassifier.h" #include "network/network.h" #include "object/ObjectRepository.h" #include "OpenRCT2.h" +#include "ParkImporter.h" #include "platform/crash.h" #include "PlatformEnvironment.h" #include "ride/TrackDesignRepository.h" @@ -518,50 +520,59 @@ namespace OpenRCT2 ClassifiedFile info; if (TryClassifyFile(path, &info)) { - if (info.Type == FILE_TYPE::SAVED_GAME) + if (info.Type == FILE_TYPE::SAVED_GAME || + info.Type == FILE_TYPE::SCENARIO) { + std::unique_ptr parkImporter; if (info.Version <= 2) { - if (rct1_load_saved_game(path)) + parkImporter.reset(ParkImporter::CreateS4()); + } + else + { + parkImporter.reset(ParkImporter::CreateS6()); + } + + if (info.Type == FILE_TYPE::SAVED_GAME) + { + try { + parkImporter->LoadSavedGame(path); + parkImporter->Import(); + game_fix_save_vars(); + sprite_position_tween_reset(); + gScreenAge = 0; + gLastAutoSaveUpdate = AUTOSAVE_PAUSE; game_load_init(); return true; } + catch (const Exception &) + { + Console::Error::WriteLine("Error loading saved game."); + } } else { - if (game_load_save(path)) - { - gFirstTimeSave = 0; - return true; - } - } - Console::Error::WriteLine("Error loading saved game."); - } - else if (info.Type == FILE_TYPE::SCENARIO) - { - if (info.Version <= 2) - { - - if (rct1_load_scenario(path)) + try { + parkImporter->LoadScenario(path); + parkImporter->Import(); + game_fix_save_vars(); + sprite_position_tween_reset(); + gScreenAge = 0; + gLastAutoSaveUpdate = AUTOSAVE_PAUSE; scenario_begin(); return true; } - } - else - { - if (scenario_load_and_play_from_path(path)) + catch (const Exception &) { - return true; + Console::Error::WriteLine("Error loading scenario."); } } - Console::Error::WriteLine("Error loading scenario."); } else { Console::Error::WriteLine("Invalid file type."); - Console::Error::WriteLine("Invalid file type."); } } else diff --git a/src/openrct2/cmdline/CommandLine.hpp b/src/openrct2/cmdline/CommandLine.hpp index 21463f9467..2014326da6 100644 --- a/src/openrct2/cmdline/CommandLine.hpp +++ b/src/openrct2/cmdline/CommandLine.hpp @@ -110,4 +110,5 @@ namespace CommandLine exitcode_t HandleCommandDefault(); exitcode_t HandleCommandConvert(CommandLineArgEnumerator * enumerator); + exitcode_t HandleCommandUri(CommandLineArgEnumerator * enumerator); } diff --git a/src/openrct2/cmdline/RootCommands.cpp b/src/openrct2/cmdline/RootCommands.cpp index b68ca8e7f1..1d64d017b8 100644 --- a/src/openrct2/cmdline/RootCommands.cpp +++ b/src/openrct2/cmdline/RootCommands.cpp @@ -126,6 +126,7 @@ const CommandLineCommand CommandLine::RootCommands[] DefineCommand("set-rct2", "", StandardOptions, HandleCommandSetRCT2), DefineCommand("convert", " ", StandardOptions, CommandLine::HandleCommandConvert), DefineCommand("scan-objects", "", StandardOptions, HandleCommandScanObjects), + DefineCommand("handle-uri", "openrct2://.../", StandardOptions, CommandLine::HandleCommandUri), #if defined(__WINDOWS__) && !defined(__MINGW32__) DefineCommand("register-shell", "", RegisterShellOptions, HandleCommandRegisterShell), diff --git a/src/openrct2/cmdline/UriHandler.cpp b/src/openrct2/cmdline/UriHandler.cpp new file mode 100644 index 0000000000..3890c76daa --- /dev/null +++ b/src/openrct2/cmdline/UriHandler.cpp @@ -0,0 +1,108 @@ +#pragma region Copyright (c) 2014-2016 OpenRCT2 Developers +/***************************************************************************** + * OpenRCT2, an open source clone of Roller Coaster Tycoon 2. + * + * OpenRCT2 is the work of many authors, a full list can be found in contributors.md + * For more information, visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * A full copy of the GNU General Public License can be found in licence.txt + *****************************************************************************/ +#pragma endregion + +#include "../core/Console.hpp" +#include "../core/Memory.hpp" +#include "../core/String.hpp" +#include "../network/network.h" +#include "../OpenRCT2.h" +#include "CommandLine.hpp" + +static exitcode_t HandleUri(const std::string &uri); + +#ifndef DISABLE_NETWORK +static exitcode_t HandleUriJoin(const std::vector &args); +static bool TryParseHostnamePort(const std::string &hostnamePort, std::string * outHostname, sint32 * outPort, sint32 defaultPort); +#endif + +exitcode_t CommandLine::HandleCommandUri(CommandLineArgEnumerator * enumerator) +{ + const utf8 * uri; + if (enumerator->TryPopString(&uri)) + { + if (String::StartsWith(uri, "openrct2://")) + { + const utf8 * uriCommand = uri + 11; + return HandleUri(uriCommand); + } + } + + Console::Error::WriteLine("Invalid URI"); + return EXITCODE_FAIL; +} + +static exitcode_t HandleUri(const std::string &uri) +{ + exitcode_t result = EXITCODE_CONTINUE; + auto args = String::Split(uri, "/"); + if (args.size() > 0) + { +#ifndef DISABLE_NETWORK + std::string arg = args[0]; + if (arg == "join") + { + result = HandleUriJoin(args); + } +#endif + } + return result; +} + +#ifndef DISABLE_NETWORK + +static exitcode_t HandleUriJoin(const std::vector &args) +{ + std::string hostname; + sint32 port; + if (args.size() > 1 && TryParseHostnamePort(args[1], &hostname, &port, NETWORK_DEFAULT_PORT)) + { + // Set the network start configuration + gNetworkStart = NETWORK_MODE_CLIENT; + String::Set(gNetworkStartHost, sizeof(gNetworkStartHost), hostname.c_str()); + gNetworkStartPort = port; + return EXITCODE_CONTINUE; + } + else + { + Console::Error::WriteLine("Expected hostname:port after join"); + return EXITCODE_FAIL; + } +} + +static bool TryParseHostnamePort(const std::string &hostnamePort, std::string * outHostname, sint32 * outPort, sint32 defaultPort) +{ + try + { + // Argument is in hostname:port format, so we need to split + std::string hostname = hostnamePort; + sint32 port = defaultPort; + size_t colonIndex = hostnamePort.find_first_of(':'); + if (colonIndex != std::string::npos) + { + hostname = hostnamePort.substr(0, colonIndex); + port = std::stoi(hostnamePort.substr(colonIndex + 1)); + } + *outPort = port; + *outHostname = hostname; + return true; + } + catch (const std::exception &) + { + return false; + } +} + +#endif // DISABLE_NETWORK diff --git a/src/openrct2/common.h b/src/openrct2/common.h index 1a1d89dafe..14209e0ef3 100644 --- a/src/openrct2/common.h +++ b/src/openrct2/common.h @@ -113,6 +113,9 @@ typedef uint8 colour_t; #endif // __GNUC__ #endif // __cplusplus +// Gets the name of a symbol as a C string +#define nameof(symbol) #symbol + #if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) #include #define STUB() log_warning("Function %s at %s:%d is a stub.\n", __PRETTY_FUNCTION__, __FILE__, __LINE__) diff --git a/src/openrct2/core/String.cpp b/src/openrct2/core/String.cpp index 5f3fafd435..1c96ee3304 100644 --- a/src/openrct2/core/String.cpp +++ b/src/openrct2/core/String.cpp @@ -15,6 +15,8 @@ #pragma endregion #include +#include +#include extern "C" { @@ -116,6 +118,19 @@ namespace String } } + size_t IndexOf(const utf8 * str, utf8 match, size_t startIndex) + { + const utf8 * ch = str + startIndex; + for (; *ch != '\0'; ch++) + { + if (*ch == match) + { + return (size_t)(ch - str); + } + } + return SIZE_MAX; + } + size_t LastIndexOf(const utf8 * str, utf8 match) { const utf8 * lastOccurance = nullptr; @@ -300,6 +315,78 @@ namespace String return DiscardUse(ptr, String::Duplicate(replacement)); } + utf8 * Substring(const utf8 * buffer, size_t index) + { + size_t bufferSize = String::SizeOf(buffer); + bool goodSubstring = index <= bufferSize; + Guard::Assert(goodSubstring, "Substring past end of input string."); + + // If assertion continues, return empty string to avoid crash + if (!goodSubstring) + { + return String::Duplicate(""); + } + + return String::Duplicate(buffer + index); + } + + utf8 * Substring(const utf8 * buffer, size_t index, size_t size) + { + size_t bufferSize = String::SizeOf(buffer); + bool goodSubstring = index + size <= bufferSize; + Guard::Assert(goodSubstring, "Substring past end of input string."); + + // If assertion continues, cap the substring to avoid crash + if (!goodSubstring) + { + if (index >= bufferSize) + { + size = 0; + } + else + { + size = bufferSize - index; + } + } + + utf8 * result = Memory::Allocate(size + 1); + Memory::Copy(result, buffer + index, size); + result[size] = '\0'; + return result; + } + + std::vector Split(const std::string &s, const std::string &delimiter) + { + if (delimiter.empty()) + { + throw std::invalid_argument(nameof(delimiter) " can not be empty."); + } + + std::vector results; + if (!s.empty()) + { + size_t index = 0; + size_t nextIndex; + do + { + nextIndex = s.find(delimiter, index); + std::string value; + if (nextIndex == std::string::npos) + { + value = s.substr(index); + } + else + { + value = s.substr(index, nextIndex - index); + } + results.push_back(value); + index = nextIndex + delimiter.size(); + } + while (nextIndex != SIZE_MAX); + } + return results; + } + utf8 * SkipBOM(utf8 * buffer) { return (utf8*)SkipBOM((const utf8 *)buffer); diff --git a/src/openrct2/core/String.hpp b/src/openrct2/core/String.hpp index 427374b2af..4543a3fd97 100644 --- a/src/openrct2/core/String.hpp +++ b/src/openrct2/core/String.hpp @@ -17,6 +17,7 @@ #pragma once #include +#include #include "../common.h" namespace String @@ -32,6 +33,7 @@ namespace String bool Equals(const std::string &a, const std::string &b, bool ignoreCase = false); bool Equals(const utf8 * a, const utf8 * b, bool ignoreCase = false); bool StartsWith(const utf8 * str, const utf8 * match, bool ignoreCase = false); + size_t IndexOf(const utf8 * str, utf8 match, size_t startIndex = 0); size_t LastIndexOf(const utf8 * str, utf8 match); /** @@ -64,6 +66,22 @@ namespace String */ utf8 * DiscardDuplicate(utf8 * * ptr, const utf8 * replacement); + /** + * Creates a new string containing the characters between index and and end of the input string. + */ + utf8 * Substring(const utf8 * buffer, size_t index); + + /** + * Creates a new string containing the characters between index and index + size of the input string. + */ + utf8 * Substring(const utf8 * buffer, size_t index, size_t size); + + /** + * Splits the given string by a delimiter and returns the values as a new string array. + * @returns the number of values. + */ + std::vector Split(const std::string &s, const std::string &delimiter); + utf8 * SkipBOM(utf8 * buffer); const utf8 * SkipBOM(const utf8 * buffer); diff --git a/src/openrct2/libopenrct2.vcxproj b/src/openrct2/libopenrct2.vcxproj index 8096eb5290..fe636745d1 100644 --- a/src/openrct2/libopenrct2.vcxproj +++ b/src/openrct2/libopenrct2.vcxproj @@ -72,6 +72,7 @@ + diff --git a/src/openrct2/platform/platform.h b/src/openrct2/platform/platform.h index df2181f2c5..535b33fedf 100644 --- a/src/openrct2/platform/platform.h +++ b/src/openrct2/platform/platform.h @@ -224,6 +224,7 @@ void core_init(); HWND windows_get_window_handle(); void platform_setup_file_associations(); void platform_remove_file_associations(); + bool platform_setup_uri_protocol(); // This function cannot be marked as 'static', even though it may seem to be, // as it requires external linkage, which 'static' prevents __declspec(dllexport) sint32 StartOpenRCT(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, sint32 nCmdShow); diff --git a/src/openrct2/platform/windows.c b/src/openrct2/platform/windows.c index fa568c323d..520df12bd4 100644 --- a/src/openrct2/platform/windows.c +++ b/src/openrct2/platform/windows.c @@ -1040,7 +1040,8 @@ utf8* platform_get_username() { // File association setup /////////////////////////////////////////////////////////////////////////////// -#define SOFTWARE_CLASSES L"Software\\Classes" +#define SOFTWARE_CLASSES L"Software\\Classes" +#define MUI_CACHE L"Local Settings\\Software\\Microsoft\\Windows\\Shell\\MuiCache" static void get_progIdName(wchar_t *dst, const utf8 *extension) { @@ -1183,4 +1184,57 @@ void platform_remove_file_associations() } /////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////// +// URI protocol association setup +/////////////////////////////////////////////////////////////////////////////// + +bool platform_setup_uri_protocol() +{ + log_verbose("Setting up URI protocol..."); + + // [HKEY_CURRENT_USER\Software\Classes] + HKEY hRootKey; + if (RegOpenKeyW(HKEY_CURRENT_USER, SOFTWARE_CLASSES, &hRootKey) == ERROR_SUCCESS) { + // [hRootKey\openrct2] + HKEY hClassKey; + if (RegCreateKeyA(hRootKey, "openrct2", &hClassKey) == ERROR_SUCCESS) { + if (RegSetValueA(hClassKey, NULL, REG_SZ, "URL:openrct2", 0) == ERROR_SUCCESS) { + if (RegSetKeyValueA(hClassKey, NULL, "URL Protocol", REG_SZ, "", 0) == ERROR_SUCCESS) { + // [hRootKey\openrct2\shell\open\command] + wchar_t exePath[MAX_PATH]; + GetModuleFileNameW(NULL, exePath, MAX_PATH); + + wchar_t buffer[512]; + swprintf_s(buffer, sizeof(buffer), L"\"%s\" handle-uri \"%%1\"", exePath); + if (RegSetValueW(hClassKey, L"shell\\open\\command", REG_SZ, buffer, 0) == ERROR_SUCCESS) { + // Not compulsory, but gives the application a nicer name + // [HKEY_CURRENT_USER\SOFTWARE\Classes\Local Settings\Software\Microsoft\Windows\Shell\MuiCache] + HKEY hMuiCacheKey; + if (RegCreateKeyW(hRootKey, MUI_CACHE, &hMuiCacheKey) == ERROR_SUCCESS) { + swprintf_s(buffer, sizeof(buffer), L"%s.FriendlyAppName", exePath); +#ifdef __MINGW32__ + // mingw-w64 defines RegSetKeyValueW's signature incorrectly + // A fix has already been submitted upstream, this can be be removed after their next release: + // https://sourceforge.net/p/mingw-w64/mingw-w64/ci/da9341980a4b70be3563ac09b5927539e7da21f7/ + RegSetKeyValueW(hMuiCacheKey, NULL, (LPCSTR)buffer, REG_SZ, (LPCSTR)L"OpenRCT2", sizeof(L"OpenRCT2") + 1); +#else + RegSetKeyValueW(hMuiCacheKey, NULL, buffer, REG_SZ, L"OpenRCT2", sizeof(L"OpenRCT2") + 1); +#endif + } + + log_verbose("URI protocol setup successful"); + return true; + } + } + } + } + } + + log_verbose("URI protocol setup failed"); + return false; +} + +/////////////////////////////////////////////////////////////////////////////// + #endif diff --git a/test/tests/AssertHelpers.hpp b/test/tests/AssertHelpers.hpp new file mode 100644 index 0000000000..1098db7a2c --- /dev/null +++ b/test/tests/AssertHelpers.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +template +void AssertVector(std::vector actual, TExpected expected) +{ + ASSERT_EQ(actual.size(), expected.size()) << + "Expected vector of size " << expected.size() << ", but was " << actual.size(); + size_t i = 0; + for (auto item : expected) + { + EXPECT_EQ(actual[i], item) << + "Element at index " << i << " did not match"; + i++; + } +} + +template +void AssertVector(std::vector actual, std::initializer_list expected) +{ + AssertVector>(actual, expected); +} diff --git a/test/tests/StringTest.cpp b/test/tests/StringTest.cpp index 8644c3c19b..7280eb6200 100644 --- a/test/tests/StringTest.cpp +++ b/test/tests/StringTest.cpp @@ -2,6 +2,7 @@ #include #include #include +#include "AssertHelpers.hpp" using TCase = std::tuple; @@ -28,3 +29,23 @@ TEST_P(StringTest, Trim) std::string actual = String::Trim(input); ASSERT_EQ(expected, actual); } + +TEST_F(StringTest, Split_ByComma) +{ + auto actual = String::Split("a,bb,ccc,dd", ","); + AssertVector(actual, { "a", "bb", "ccc", "dd" }); +} +TEST_F(StringTest, Split_ByColonColon) +{ + auto actual = String::Split("a::bb:ccc:::::dd", "::"); + AssertVector(actual, { "a", "bb:ccc", "", ":dd" }); +} +TEST_F(StringTest, Split_Empty) +{ + auto actual = String::Split("", "."); + AssertVector(actual, { }); +} +TEST_F(StringTest, Split_ByEmpty) +{ + EXPECT_THROW(String::Split("string", ""), std::invalid_argument); +} diff --git a/test/tests/tests.vcxproj b/test/tests/tests.vcxproj index 02dc0344b5..bbc3753f97 100644 --- a/test/tests/tests.vcxproj +++ b/test/tests/tests.vcxproj @@ -47,6 +47,7 @@ +