From bb869311e00aacf19173bb77ac8fca49186c84c7 Mon Sep 17 00:00:00 2001 From: KHeartz Date: Wed, 8 Jul 2026 20:41:33 -0400 Subject: [PATCH 1/4] Launcher: add Epic Games Store platform Resolve the game through the Epic launcher's plaintext manifests: new External::Epic component, ProjectPlatform::EPIC, and RunInnerEpicChecks mirroring the Steam path. --- code/framework/CMakeLists.txt | 1 + code/framework/src/external/epic/manifest.cpp | 110 ++++++++++++++++++ code/framework/src/external/epic/manifest.h | 42 +++++++ code/framework/src/launcher/project.cpp | 30 +++++ code/framework/src/launcher/project.h | 8 +- 5 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 code/framework/src/external/epic/manifest.cpp create mode 100644 code/framework/src/external/epic/manifest.h diff --git a/code/framework/CMakeLists.txt b/code/framework/CMakeLists.txt index c18f4ef63..700aacd9f 100644 --- a/code/framework/CMakeLists.txt +++ b/code/framework/CMakeLists.txt @@ -162,6 +162,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 00890d6aa..69af40477 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" @@ -267,6 +268,11 @@ namespace Framework::Launcher { return false; } } + else if (_config.platform == ProjectPlatform::EPIC) { + if (!RunInnerEpicChecks()) { + return false; + } + } else { if (!RunInnerClassicChecks()) { return false; @@ -459,6 +465,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 191f1f9be..0dbea2c58 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. From f053c113f721dbd78afc133e0d3e3adbc28e9cc6 Mon Sep 17 00:00:00 2001 From: KHeartz Date: Fri, 31 Jul 2026 19:10:02 -0400 Subject: [PATCH 2/4] Launcher: move Epic manifest string helpers to StringUtils ToLower and FileName are not Epic-specific; drop the anonymous namespace in manifest.cpp and share them from utils/string_utils.h. --- code/framework/src/external/epic/manifest.cpp | 22 ++++--------------- code/framework/src/utils/string_utils.h | 14 ++++++++++++ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/code/framework/src/external/epic/manifest.cpp b/code/framework/src/external/epic/manifest.cpp index ad588b1ce..d28104168 100644 --- a/code/framework/src/external/epic/manifest.cpp +++ b/code/framework/src/external/epic/manifest.cpp @@ -8,31 +8,17 @@ #include "manifest.h" +#include "utils/string_utils.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) { @@ -96,14 +82,14 @@ namespace Framework::External::Epic { 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 std::string wantExe = Utils::StringUtils::ToLower(Utils::StringUtils::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 !wantExe.empty() && Utils::StringUtils::ToLower(Utils::StringUtils::FileName(app.launchExecutable)) == wantExe; }); return it != apps.end() ? *it : InstalledApp {}; } diff --git a/code/framework/src/utils/string_utils.h b/code/framework/src/utils/string_utils.h index 7fa9a84c2..da631fca2 100644 --- a/code/framework/src/utils/string_utils.h +++ b/code/framework/src/utils/string_utils.h @@ -9,6 +9,7 @@ #pragma once #include +#include #include #include #include @@ -42,4 +43,17 @@ namespace Framework::Utils::StringUtils { inline std::string Trim(std::string_view s) { return LeftTrim(RightTrim(s)); } + + inline std::string ToLower(std::string s) { + std::transform(s.begin(), s.end(), 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. + inline std::string FileName(std::string_view path) { + const auto pos = path.find_last_of("/\\"); + return std::string(pos == std::string_view::npos ? path : path.substr(pos + 1)); + } } // namespace Framework::Utils::StringUtils From e93686bc8ebc062192cd119e556c6d6edcb27c47 Mon Sep 17 00:00:00 2001 From: KHeartz Date: Fri, 31 Jul 2026 19:13:13 -0400 Subject: [PATCH 3/4] Launcher: use std::ranges::replace in RunInnerEpicChecks --- code/framework/src/launcher/project.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/framework/src/launcher/project.cpp b/code/framework/src/launcher/project.cpp index 69af40477..43a2062d5 100644 --- a/code/framework/src/launcher/project.cpp +++ b/code/framework/src/launcher/project.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -478,7 +479,7 @@ namespace Framework::Launcher { } _gamePath = Utils::StringUtils::NormalToWide(app.installLocation); - std::replace(_gamePath.begin(), _gamePath.end(), '\\', '/'); + std::ranges::replace(_gamePath, L'\\', L'/'); // 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. From 9e093ef7609f49696a35acc9b395c6fc45c03238 Mon Sep 17 00:00:00 2001 From: KHeartz Date: Fri, 31 Jul 2026 19:20:50 -0400 Subject: [PATCH 4/4] Launcher: use std::ranges::transform in StringUtils::ToLower --- code/framework/src/utils/string_utils.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/framework/src/utils/string_utils.h b/code/framework/src/utils/string_utils.h index da631fca2..9c0a317a7 100644 --- a/code/framework/src/utils/string_utils.h +++ b/code/framework/src/utils/string_utils.h @@ -45,7 +45,7 @@ namespace Framework::Utils::StringUtils { } inline std::string ToLower(std::string s) { - std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { + std::ranges::transform(s, s.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return s;