Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions code/framework/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
110 changes: 110 additions & 0 deletions code/framework/src/external/epic/manifest.cpp
Original file line number Diff line number Diff line change
@@ -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 <nlohmann/json.hpp>

#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <ranges>

namespace Framework::External::Epic {
namespace {
std::string ToLower(std::string s) {
std::ranges::transform(s, s.begin(), [](unsigned char c) {
return static_cast<char>(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<InstalledApp> EnumerateInstalledApps() {
std::vector<InstalledApp> 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
42 changes: 42 additions & 0 deletions code/framework/src/external/epic/manifest.h
Original file line number Diff line number Diff line change
@@ -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 <string>
#include <vector>

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<InstalledApp> 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
30 changes: 30 additions & 0 deletions code/framework/src/launcher/project.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include "project.h"

#include "external/epic/manifest.h"
#include "loaders/exe_ldr.h"
#include "logging/logger.h"
#include "sfd.h"
Expand Down Expand Up @@ -266,6 +267,11 @@ namespace Framework::Launcher {
return false;
}
}
else if (_config.platform == ProjectPlatform::EPIC) {
if (!RunInnerEpicChecks()) {
return false;
}
}
else {
if (!RunInnerClassicChecks()) {
return false;
Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 7 additions & 1 deletion code/framework/src/launcher/project.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
namespace Framework::Launcher {
enum class ProjectPlatform {
CLASSIC,
STEAM
STEAM,
EPIC
};
enum class ProjectLaunchType {
PE_LOADING,
Expand Down Expand Up @@ -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<uint32_t> supportedGameVersions;
Expand Down Expand Up @@ -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.
Expand Down
Loading