diff --git a/data/language/en-GB.txt b/data/language/en-GB.txt index ce4f078c69..b10b65b668 100644 --- a/data/language/en-GB.txt +++ b/data/language/en-GB.txt @@ -3843,3 +3843,6 @@ STR_7010 :Could not start replay, file ‘{STRING}’ doesn’t exist or isn STR_7011 :Could not start replay STR_7012 :Polish Złoty (PLN) STR_7013 :Drag areas of path +STR_7014 :I own the game on Steam, but I haven’t installed it yet. +STR_7015 :Please close Steam if it’s running, then click ‘OK’. +STR_7016 :OpenRCT2 has tried to trigger a download in Steam. Please open Steam and let it download the game. When Steam is finished, click ‘OK’. diff --git a/src/openrct2-android/app/src/main/CMakeLists.txt b/src/openrct2-android/app/src/main/CMakeLists.txt index ef63285416..375481a0dd 100644 --- a/src/openrct2-android/app/src/main/CMakeLists.txt +++ b/src/openrct2-android/app/src/main/CMakeLists.txt @@ -272,6 +272,7 @@ target_include_directories(openrct2-ui PRIVATE "${ORCT2_ROOT}/src/thirdparty/duk target_include_directories(openrct2-ui PRIVATE "${ORCT2_ROOT}/src") target_include_directories(openrct2-ui SYSTEM PRIVATE "${ORCT2_ROOT}/src/thirdparty") target_include_directories(openrct2-cli PRIVATE "${ORCT2_ROOT}/src") +target_include_directories(openrct2-cli SYSTEM PRIVATE "${ORCT2_ROOT}/src/thirdparty") target_include_directories(openrct2 PRIVATE "/opt/openrct2/include/nlohmann/../") target_include_directories(openrct2-ui PRIVATE "/opt/openrct2/include/nlohmann/../") diff --git a/src/openrct2/command_line/CommandLine.hpp b/src/openrct2/command_line/CommandLine.hpp index 9d5dcdfd02..86dedc0233 100644 --- a/src/openrct2/command_line/CommandLine.hpp +++ b/src/openrct2/command_line/CommandLine.hpp @@ -125,5 +125,6 @@ namespace OpenRCT2 exitcode_t HandleCommandDefault(); exitcode_t HandleCommandUri(CommandLineArgEnumerator* enumerator); + exitcode_t HandleCommandTriggerSteamDownload(CommandLineArgEnumerator* enumerator); } // namespace CommandLine } // namespace OpenRCT2 diff --git a/src/openrct2/command_line/RootCommands.cpp b/src/openrct2/command_line/RootCommands.cpp index a8f7dcd3d6..6629f801f4 100644 --- a/src/openrct2/command_line/RootCommands.cpp +++ b/src/openrct2/command_line/RootCommands.cpp @@ -135,6 +135,7 @@ namespace OpenRCT2 DefineCommand("set-rct2", "", kStandardOptions, HandleCommandSetRCT2), DefineCommand("scan-objects", "", kStandardOptions, HandleCommandScanObjects), DefineCommand("handle-uri", "openrct2://.../", kStandardOptions, CommandLine::HandleCommandUri), + DefineCommand("trigger-steam-download", "", kStandardOptions, CommandLine::HandleCommandTriggerSteamDownload), #if defined(_WIN32) DefineCommand("register-shell", "", RegisterShellOptions, HandleCommandRegisterShell), @@ -496,4 +497,14 @@ namespace OpenRCT2 // TODO Print other potential information (e.g. user, hardware) } + + exitcode_t CommandLine::HandleCommandTriggerSteamDownload([[maybe_unused]] CommandLineArgEnumerator* enumerator) + { + if (!Platform::triggerSteamDownload()) + { + return EXITCODE_FAIL; + } + + return EXITCODE_OK; + } } // namespace OpenRCT2 diff --git a/src/openrct2/config/Config.cpp b/src/openrct2/config/Config.cpp index a2fa8d867f..60d46e9523 100644 --- a/src/openrct2/config/Config.cpp +++ b/src/openrct2/config/Config.cpp @@ -716,13 +716,24 @@ namespace OpenRCT2::Config } } - auto steamPath = Platform::GetSteamPath(); - if (!steamPath.empty()) + auto steamPaths = Platform::GetSteamPaths(); + if (steamPaths.isSteamPresent()) { - std::string location = Path::Combine(steamPath, Platform::GetRCT1SteamDir()); - if (RCT1DataPresentAtLocation(location)) + for (const auto& root : steamPaths.roots) { - return location; + auto nativePath = Path::Combine(root, steamPaths.nativeFolder, Platform::kSteamRCT1Data.nativeFolder); + if (RCT1DataPresentAtLocation(nativePath)) + { + return nativePath; + } + if (!steamPaths.downloadDepotFolder.empty()) + { + auto downloadDepotPath = steamPaths.getDownloadDepotFolder(root, Platform::kSteamRCT1Data); + if (RCT1DataPresentAtLocation(downloadDepotPath)) + { + return downloadDepotPath; + } + } } } @@ -734,6 +745,39 @@ namespace OpenRCT2::Config return {}; } + static u8string FindRCT2SteamPath() + { + auto steamPaths = Platform::GetSteamPaths(); + if (steamPaths.isSteamPresent()) + { + const std::array gamesToCheck = { + Platform::kSteamRCT2Data, + Platform::kSteamRCTCData, + }; + for (const auto& root : steamPaths.roots) + { + for (const auto& game : gamesToCheck) + { + auto nativePath = Path::Combine(root, steamPaths.nativeFolder, game.nativeFolder); + if (Platform::OriginalGameDataExists(nativePath)) + { + return nativePath; + } + if (!steamPaths.downloadDepotFolder.empty()) + { + auto downloadDepotPath = steamPaths.getDownloadDepotFolder(root, game); + if (Platform::OriginalGameDataExists(downloadDepotPath)) + { + return downloadDepotPath; + } + } + } + } + } + + return {}; + } + /** * Attempts to find the RCT2 installation directory. * This should be created from some other resource when OpenRCT2 grows. @@ -753,20 +797,11 @@ namespace OpenRCT2::Config } } - auto steamPath = Platform::GetSteamPath(); + // Will only return a path if the RCT2 data is present, so no need to check twice. + auto steamPath = FindRCT2SteamPath(); if (!steamPath.empty()) { - std::string location = Path::Combine(steamPath, Platform::GetRCT2SteamDir()); - if (Platform::OriginalGameDataExists(location)) - { - return location; - } - - std::string location2 = Path::Combine(steamPath, Platform::GetRCTClassicSteamDir()); - if (Platform::OriginalGameDataExists(location2)) - { - return location2; - } + return steamPath; } auto discordPath = Platform::GetFolderPath(SpecialFolder::rct2Discord); @@ -874,6 +909,7 @@ namespace OpenRCT2::Config { uiContext.ShowMessageBox(LanguageGetString(STR_NEEDS_RCT2_FILES)); std::string gog = LanguageGetString(STR_OWN_ON_GOG); + std::string steam = LanguageGetString(STR_OWN_ON_STEAM); std::string hdd = LanguageGetString(STR_INSTALLED_ON_HDD); std::vector options; @@ -883,6 +919,7 @@ namespace OpenRCT2::Config { options.push_back(hdd); options.push_back(gog); + options.push_back(steam); int optionIndex = uiContext.ShowMenuDialog( options, LanguageGetString(STR_OPENRCT2_SETUP), LanguageGetString(STR_WHICH_APPLIES_BEST)); if (optionIndex < 0 || static_cast(optionIndex) >= options.size()) @@ -940,6 +977,21 @@ namespace OpenRCT2::Config possibleInstallPaths.emplace_back(dest); possibleInstallPaths.emplace_back(Path::Combine(dest, u8"app")); } + else if (chosenOption == steam) + { + uiContext.ShowMessageBox(LanguageGetString(STR_PLEASE_CLOSE_STEAM)); + + Platform::triggerSteamDownload(); + + uiContext.ShowMessageBox(LanguageGetString(STR_WAIT_FOR_STEAM_DOWNLOAD)); + + auto steamPath = FindRCT2SteamPath(); + if (!steamPath.empty()) + { + Get().general.rct2Path = steamPath; + return true; + } + } if (possibleInstallPaths.empty()) { return false; diff --git a/src/openrct2/localisation/StringIds.h b/src/openrct2/localisation/StringIds.h index 397ec2d7da..89bf3294f8 100644 --- a/src/openrct2/localisation/StringIds.h +++ b/src/openrct2/localisation/StringIds.h @@ -1602,6 +1602,9 @@ enum : StringId STR_THIS_WILL_TAKE_A_FEW_MINUTES = 6407, STR_INSTALL_INNOEXTRACT = 6408, STR_NOT_THE_GOG_INSTALLER = 6409, + STR_OWN_ON_STEAM = 7014, + STR_PLEASE_CLOSE_STEAM = 7015, + STR_WAIT_FOR_STEAM_DOWNLOAD = 7016, STR_TILE_INSPECTOR_TOGGLE_INVISIBILITY_TIP = 6436, diff --git a/src/openrct2/platform/Platform.Android.cpp b/src/openrct2/platform/Platform.Android.cpp index 1ae2dbcaa4..dc0c0e4d6a 100644 --- a/src/openrct2/platform/Platform.Android.cpp +++ b/src/openrct2/platform/Platform.Android.cpp @@ -148,22 +148,7 @@ namespace OpenRCT2::Platform return isImperial == JNI_TRUE ? MeasurementFormat::Imperial : MeasurementFormat::Metric; } - std::string GetSteamPath() - { - return {}; - } - - u8string GetRCT1SteamDir() - { - return {}; - } - - u8string GetRCT2SteamDir() - { - return {}; - } - - u8string GetRCTClassicSteamDir() + SteamPaths GetSteamPaths() { return {}; } diff --git a/src/openrct2/platform/Platform.Common.cpp b/src/openrct2/platform/Platform.Common.cpp index e33cc7b7e3..fbe440fd2b 100644 --- a/src/openrct2/platform/Platform.Common.cpp +++ b/src/openrct2/platform/Platform.Common.cpp @@ -213,4 +213,43 @@ namespace OpenRCT2::Platform return false; } + bool SteamPaths::isSteamPresent() const + { + return !roots.empty(); + } + + u8string SteamPaths::getDownloadDepotFolder(u8string_view steamroot, const SteamGameData& data) const + { + return Path::Combine( + steamroot, downloadDepotFolder, "app_" + std::to_string(data.appId), "depot_" + std::to_string(data.depotId)); + } + + bool triggerSteamDownload() + { + const auto steamPaths = GetSteamPaths(); + if (!steamPaths.isSteamPresent() || steamPaths.manifests.empty()) + return false; + + const auto manifestsDir = Path::Combine(steamPaths.roots[0], steamPaths.manifests); + const std::array gamesToTrigger = { kSteamRCT2Data, kSteamRCTCData, kSteamRCT1Data }; + for (const auto& game : gamesToTrigger) + { + auto fullFilename = Path::Combine(manifestsDir, "appmanifest_" + std::to_string(game.appId) + ".acf"); + // If the file exists, we assume a download has been triggered already. + if (File::Exists(fullFilename)) + continue; + + // clang-format off + auto buffer = u8string("\"AppState\"\r\n") + u8string("{\r\n") + + u8string(" \"AppID\" \"" + std::to_string(game.appId) + "\"\r\n") + + u8string(" \"Universe\" \"1\"\r\n") + + u8string(" \"installdir\" \"" + game.nativeFolder + "\"\r\n") + + u8string(" \"StateFlags\" \"1026\"\r\n") + u8string("}\r\n"); + // clang-format on + File::WriteAllBytes(fullFilename, buffer.data(), buffer.size()); + } + + return true; + } + } // namespace OpenRCT2::Platform diff --git a/src/openrct2/platform/Platform.Emscripten.cpp b/src/openrct2/platform/Platform.Emscripten.cpp index 5eb5453bdd..24f73120de 100644 --- a/src/openrct2/platform/Platform.Emscripten.cpp +++ b/src/openrct2/platform/Platform.Emscripten.cpp @@ -97,22 +97,7 @@ namespace OpenRCT2::Platform return isImperial == 1 ? MeasurementFormat::Imperial : MeasurementFormat::Metric; } - std::string GetSteamPath() - { - return {}; - } - - u8string GetRCT1SteamDir() - { - return {}; - } - - u8string GetRCT2SteamDir() - { - return {}; - } - - u8string GetRCTClassicSteamDir() + SteamPaths GetSteamPaths() { return {}; } diff --git a/src/openrct2/platform/Platform.Linux.cpp b/src/openrct2/platform/Platform.Linux.cpp index 28ab83344f..5bcb8f9d5e 100644 --- a/src/openrct2/platform/Platform.Linux.cpp +++ b/src/openrct2/platform/Platform.Linux.cpp @@ -320,65 +320,52 @@ namespace OpenRCT2::Platform return MeasurementFormat::Metric; } - std::string GetSteamPath() + SteamPaths GetSteamPaths() { + SteamPaths ret = {}; + ret.nativeFolder = "steamapps/common"; + ret.downloadDepotFolder = "ubuntu12_32/steamapps/content"; + ret.manifests = "steamapps"; + const char* steamRoot = getenv("STEAMROOT"); if (steamRoot != nullptr) { - return Path::Combine(steamRoot, u8"ubuntu12_32/steamapps/content"); + ret.roots.emplace_back(steamRoot); } const char* localSharePath = getenv("XDG_DATA_HOME"); if (localSharePath != nullptr) { - auto steamPath = Path::Combine(localSharePath, u8"Steam/ubuntu12_32/steamapps/content"); - if (Path::DirectoryExists(steamPath)) + auto xdgDataHomeSteamPath = Path::Combine(localSharePath, u8"Steam"); + if (Path::DirectoryExists(xdgDataHomeSteamPath)) { - return steamPath; + ret.roots.emplace_back(xdgDataHomeSteamPath); } } const char* homeDir = getpwuid(getuid())->pw_dir; - if (homeDir == nullptr) + if (homeDir != nullptr) { - return {}; + auto localShareSteamPath = Path::Combine(homeDir, u8".local/share/Steam"); + if (Path::DirectoryExists(localShareSteamPath)) + { + ret.roots.emplace_back(localShareSteamPath); + } + + auto oldSteamPath = Path::Combine(homeDir, u8".steam/steam"); + if (Path::DirectoryExists(oldSteamPath)) + { + ret.roots.emplace_back(oldSteamPath); + } + + auto snapLocalShareSteamPath = Path::Combine(homeDir, u8"snap/steam/common/.local/share/Steam"); + if (Path::DirectoryExists(snapLocalShareSteamPath)) + { + ret.roots.emplace_back(snapLocalShareSteamPath); + } } - // Prefer new path for Steam, which is the default when using with Proton - auto steamPath = Path::Combine(homeDir, u8".local/share/Steam/steamapps/common"); - if (Path::DirectoryExists(steamPath)) - { - return steamPath; - } - - // Fallback paths - steamPath = Path::Combine(homeDir, u8".local/share/Steam/ubuntu12_32/steamapps/content"); - if (Path::DirectoryExists(steamPath)) - { - return steamPath; - } - - steamPath = Path::Combine(homeDir, u8".steam/steam/ubuntu12_32/steamapps/content"); - if (Path::DirectoryExists(steamPath)) - { - return steamPath; - } - return {}; - } - - u8string GetRCT1SteamDir() - { - return u8"Rollercoaster Tycoon Deluxe"; - } - - u8string GetRCT2SteamDir() - { - return u8"Rollercoaster Tycoon 2"; - } - - u8string GetRCTClassicSteamDir() - { - return u8"RollerCoaster Tycoon Classic"; + return ret; } std::vector GetSearchablePathsRCT1() diff --git a/src/openrct2/platform/Platform.Win32.cpp b/src/openrct2/platform/Platform.Win32.cpp index 23605cc3ca..1dd206642a 100644 --- a/src/openrct2/platform/Platform.Win32.cpp +++ b/src/openrct2/platform/Platform.Win32.cpp @@ -737,7 +737,7 @@ namespace OpenRCT2::Platform return isElevated; } - std::string GetSteamPath() + SteamPaths GetSteamPaths() { wchar_t* wSteamPath; HKEY hKey; @@ -759,12 +759,18 @@ namespace OpenRCT2::Platform result = RegQueryValueExW(hKey, L"SteamPath", nullptr, &type, reinterpret_cast(wSteamPath), &size); if (result == ERROR_SUCCESS) { - auto utf8SteamPath = String::toUtf8(wSteamPath); - outPath = Path::Combine(utf8SteamPath, u8"steamapps", u8"common"); + outPath = String::toUtf8(wSteamPath); } free(wSteamPath); RegCloseKey(hKey); - return outPath; + + SteamPaths ret = {}; + ret.roots.emplace_back(outPath); + ret.nativeFolder = "steamapps/common"; + ret.downloadDepotFolder = "steamapps/content"; + ret.manifests = "steamapps"; + + return ret; } std::string GetFontPath(const TTFFontDescriptor& font) @@ -796,21 +802,6 @@ namespace OpenRCT2::Platform return GetLogicalDrives(); } - u8string GetRCT1SteamDir() - { - return u8"Rollercoaster Tycoon Deluxe"; - } - - u8string GetRCT2SteamDir() - { - return u8"Rollercoaster Tycoon 2"; - } - - u8string GetRCTClassicSteamDir() - { - return u8"RollerCoaster Tycoon Classic"; - } - time_t FileGetModifiedTime(u8string_view path) { WIN32_FILE_ATTRIBUTE_DATA data{}; diff --git a/src/openrct2/platform/Platform.h b/src/openrct2/platform/Platform.h index 00603ad076..6d477c7cf9 100644 --- a/src/openrct2/platform/Platform.h +++ b/src/openrct2/platform/Platform.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #ifdef _WIN32 @@ -60,6 +61,49 @@ struct TTFFontDescriptor; namespace OpenRCT2::Platform { + struct SteamGameData + { + u8string nativeFolder; + uint32_t appId; + uint32_t depotId; + }; + const SteamGameData kSteamRCT1Data = { + .nativeFolder = u8"Rollercoaster Tycoon Deluxe", + .appId = 285310, + .depotId = 285311, + }; + const SteamGameData kSteamRCT2Data = { + .nativeFolder = u8"Rollercoaster Tycoon 2", + .appId = 285330, + .depotId = 285331, + }; + const SteamGameData kSteamRCTCData = { + .nativeFolder = u8"RollerCoaster Tycoon Classic", + .appId = 683900, + .depotId = 683901, + }; + + struct SteamPaths + { + sfl::static_vector roots{}; + /** + * Used by native applications and applications installed through Steam Play. + */ + u8string nativeFolder{}; + /** + * Used by applications downloaded through download_depot. Most likely used on macOS and Linux, + * though technically possible on Windows too. + */ + u8string downloadDepotFolder{}; + /** + * Directory that contains the manifests to trigger a download. + */ + u8string manifests{}; + + bool isSteamPresent() const; + u8string getDownloadDepotFolder(u8string_view steamroot, const SteamGameData& data) const; + }; + constexpr u8string_view kRCTClassicWindowsDataFolder = u8"Assets"; // clang-format off constexpr u8string_view kRCTClassicMacOSDataFolder = @@ -101,7 +145,8 @@ namespace OpenRCT2::Platform std::string GetUsername(); - std::string GetSteamPath(); + SteamPaths GetSteamPaths(); + bool triggerSteamDownload(); #if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD__) || defined(__NetBSD__) std::string GetEnvironmentPath(const char* name); std::string GetHomePath(); @@ -138,9 +183,6 @@ namespace OpenRCT2::Platform bool LockSingleInstance(); - u8string GetRCT1SteamDir(); - u8string GetRCT2SteamDir(); - u8string GetRCTClassicSteamDir(); datetime64 GetDatetimeNowUTC(); uint32_t GetTicks(); diff --git a/src/openrct2/platform/Platform.macOS.mm b/src/openrct2/platform/Platform.macOS.mm index e4d280cf1f..cdc1b6812c 100644 --- a/src/openrct2/platform/Platform.macOS.mm +++ b/src/openrct2/platform/Platform.macOS.mm @@ -235,7 +235,7 @@ namespace OpenRCT2::Platform } } - std::string GetSteamPath() + SteamPaths GetSteamPaths() { const char* homeDir = getpwuid(getuid())->pw_dir; if (homeDir == nullptr) @@ -244,27 +244,18 @@ namespace OpenRCT2::Platform } auto steamPath = Path::Combine(homeDir, "Library/Application Support/Steam"); - if (Path::DirectoryExists(steamPath)) + if (!Path::DirectoryExists(steamPath)) { - return steamPath; + return {}; } - return {}; - } + SteamPaths ret = {}; + ret.roots.emplace_back(steamPath); + ret.nativeFolder = "steamapps/common"; + ret.downloadDepotFolder = "Steam.AppBundle/Steam/Contents/MacOS/steamapps/content"; + ret.manifests = "steamapps"; - u8string GetRCT1SteamDir() - { - return u8"Steam.AppBundle/Steam/Contents/MacOS/steamapps/content/app_285310/depot_285311"; - } - - u8string GetRCT2SteamDir() - { - return u8"Steam.AppBundle/Steam/Contents/MacOS/steamapps/content/app_285330/depot_285331"; - } - - u8string GetRCTClassicSteamDir() - { - return u8"steamapps/common/RollerCoaster Tycoon Classic"; + return ret; } std::string GetFontPath(const TTFFontDescriptor& font)