diff --git a/code/framework/CMakeLists.txt b/code/framework/CMakeLists.txt index 8e0ede7f7..650cc563b 100644 --- a/code/framework/CMakeLists.txt +++ b/code/framework/CMakeLists.txt @@ -146,6 +146,7 @@ if(WIN32) src/utils/minidump.cpp src/launcher/loaders/exe_ldr.cpp src/external/steam/wrapper.cpp + src/external/epic/manifest.cpp src/utils/hooking/hook_function.cpp src/utils/hooking/hooking_patterns.cpp src/utils/hooking/hooking.cpp diff --git a/code/framework/src/external/epic/manifest.cpp b/code/framework/src/external/epic/manifest.cpp new file mode 100644 index 000000000..ad588b1ce --- /dev/null +++ b/code/framework/src/external/epic/manifest.cpp @@ -0,0 +1,110 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#include "manifest.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace Framework::External::Epic { + namespace { + std::string ToLower(std::string s) { + std::ranges::transform(s, s.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return s; + } + + // File name (portion after the last / or \) of a possibly-relative path. + std::string FileName(const std::string &path) { + const auto pos = path.find_last_of("/\\"); + return pos == std::string::npos ? path : path.substr(pos + 1); + } + } // namespace + + std::string GetManifestsDir() { + std::string base = "C:\\ProgramData"; + if (const char *pd = std::getenv("PROGRAMDATA"); pd && *pd) { + base = pd; + } + return base + "\\Epic\\EpicGamesLauncher\\Data\\Manifests"; + } + + std::vector EnumerateInstalledApps() { + std::vector apps; + + // The dir holds an .item for every installed Epic title, so one malformed manifest must + // not sink the scan: iterate with the non-throwing (ec) overloads, and try/catch each + // parse — nlohmann::value() throws on a wrong-typed key or a non-object document. + std::error_code ec; + std::filesystem::directory_iterator it(GetManifestsDir(), ec); + if (ec) { + return apps; // Epic not installed / manifests dir unreadable + } + + for (const std::filesystem::directory_iterator end; it != end; it.increment(ec)) { + if (ec) { + break; + } + const std::filesystem::directory_entry &entry = *it; + + std::error_code entryEc; + if (!entry.is_regular_file(entryEc) || entry.path().extension() != ".item") { + continue; + } + + try { + std::ifstream f(entry.path(), std::ios::binary); + if (!f) { + continue; + } + + nlohmann::json doc; + f >> doc; + + InstalledApp app; + app.appName = doc.value("AppName", std::string {}); + app.displayName = doc.value("DisplayName", std::string {}); + app.installLocation = doc.value("InstallLocation", std::string {}); + app.launchExecutable = doc.value("LaunchExecutable", std::string {}); + app.catalogNamespace = doc.value("CatalogNamespace", std::string {}); + app.catalogItemId = doc.value("CatalogItemId", std::string {}); + + if (app.IsValid()) { + apps.push_back(std::move(app)); + } + } + catch (const std::exception &) { + continue; // skip a bad manifest + } + } + + return apps; + } + + InstalledApp FindInstalledApp(const std::string &exeFileName, const std::string &appName) { + // Strip the directory off both sides: the manifest's launch exe is install-root-relative, + // and exeFileName isn't guaranteed bare elsewhere in the launcher. + const std::string wantExe = ToLower(FileName(exeFileName)); + const auto apps = EnumerateInstalledApps(); + + const auto it = std::ranges::find_if(apps, [&](const InstalledApp &app) { + if (!appName.empty()) { + return app.appName == appName; + } + return !wantExe.empty() && ToLower(FileName(app.launchExecutable)) == wantExe; + }); + return it != apps.end() ? *it : InstalledApp {}; + } +} // namespace Framework::External::Epic diff --git a/code/framework/src/external/epic/manifest.h b/code/framework/src/external/epic/manifest.h new file mode 100644 index 000000000..f4f3005a8 --- /dev/null +++ b/code/framework/src/external/epic/manifest.h @@ -0,0 +1,42 @@ +/* + * MafiaHub OSS license + * Copyright (c) 2021-2023, MafiaHub. All rights reserved. + * + * This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework. + * See LICENSE file in the source repository for information regarding licensing. + */ + +#pragma once + +#include +#include + +namespace Framework::External::Epic { + // One installed title as described by an Epic Games Launcher manifest (.item file). + struct InstalledApp { + std::string appName; // Epic catalog id ("AppName") + std::string displayName; // human-readable ("DisplayName") + std::string installLocation; // install root ("InstallLocation") + std::string launchExecutable; // exe path relative to installLocation ("LaunchExecutable") + std::string catalogNamespace; // EOS sandbox id ("CatalogNamespace") + std::string catalogItemId; // EOS item id ("CatalogItemId") + + bool IsValid() const { + return !installLocation.empty(); + } + }; + + // Directory holding the Epic Games Launcher manifests + // (%PROGRAMDATA%\Epic\EpicGamesLauncher\Data\Manifests). Never empty (falls back to the + // conventional location), but the directory may not exist if Epic isn't installed. + std::string GetManifestsDir(); + + // Every installed title the Epic launcher knows about. Empty if Epic isn't installed or + // no manifests could be read. + std::vector EnumerateInstalledApps(); + + // Find an installed title. When `appName` is non-empty, matches on the Epic catalog id; + // otherwise matches the first title whose launch executable file name equals `exeFileName` + // (case-insensitive). Returns an invalid InstalledApp when nothing matches. + InstalledApp FindInstalledApp(const std::string &exeFileName, const std::string &appName = {}); +} // namespace Framework::External::Epic diff --git a/code/framework/src/launcher/project.cpp b/code/framework/src/launcher/project.cpp index 11c8dc82f..83b7a272c 100644 --- a/code/framework/src/launcher/project.cpp +++ b/code/framework/src/launcher/project.cpp @@ -8,6 +8,7 @@ #include "project.h" +#include "external/epic/manifest.h" #include "loaders/exe_ldr.h" #include "logging/logger.h" #include "sfd.h" @@ -266,6 +267,11 @@ namespace Framework::Launcher { return false; } } + else if (_config.platform == ProjectPlatform::EPIC) { + if (!RunInnerEpicChecks()) { + return false; + } + } else { if (!RunInnerClassicChecks()) { return false; @@ -458,6 +464,30 @@ namespace Framework::Launcher { return true; } + bool Project::RunInnerEpicChecks() { + // Locate the game via the Epic launcher's plaintext manifests — no SDK or running client + // needed, just Epic having installed it once. Matched by AppName, else by exe file name. + const auto exeName = Utils::StringUtils::WideToNormal(_config.executableName); + const auto appName = Utils::StringUtils::WideToNormal(_config.epicAppName); + + const auto app = External::Epic::FindInstalledApp(exeName, appName); + if (!app.IsValid()) { + MessageBox(nullptr, "The destination game is not installed through the Epic Games Launcher", _config.name.c_str(), MB_ICONERROR); + return false; + } + + _gamePath = Utils::StringUtils::NormalToWide(app.installLocation); + std::replace(_gamePath.begin(), _gamePath.end(), '\\', '/'); + + // Mirror the Steam path: the launch code appends executableName to this root, and we + // stash it in classicGamePath purely so it lands in the persisted JSON config. + _config.classicGamePath = _gamePath; + + // Unlike Steam there's no runtime DLL to inject or app-id file to drop; any Epic launch + // args go through ProjectConfiguration::additionalLaunchArguments. + return true; + } + bool Project::RunInnerClassicChecks() { cppfs::FileHandle handle = cppfs::fs::open(Utils::StringUtils::WideToNormal(_config.classicGamePath)); if (!handle.isDirectory() && !_config.promptForGameExe) { diff --git a/code/framework/src/launcher/project.h b/code/framework/src/launcher/project.h index ad152d1d3..b360a9cd3 100644 --- a/code/framework/src/launcher/project.h +++ b/code/framework/src/launcher/project.h @@ -22,7 +22,8 @@ namespace Framework::Launcher { enum class ProjectPlatform { CLASSIC, - STEAM + STEAM, + EPIC }; enum class ProjectLaunchType { PE_LOADING, @@ -61,6 +62,10 @@ namespace Framework::Launcher { // if promptForGameExe is true, and steam dll is found in the game's library, switch to steam platform bool preferSteam = false; + // EPIC platform: Epic catalog id ("AppName") of the destination game. Optional — when + // empty the Epic manifest is matched by the launch executable's file name instead. + std::wstring epicAppName; + // game exe integrity checks (uses CRC32 checksum) bool verifyGameIntegrity = false; std::vector supportedGameVersions; @@ -159,6 +164,7 @@ namespace Framework::Launcher { uint32_t GetGameVersion() const; bool RunInnerSteamChecks(); + bool RunInnerEpicChecks(); bool RunInnerClassicChecks(); // Extracts a launch URL from the command line into the environment for the client.