Skip to content
Merged
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
14 changes: 14 additions & 0 deletions opensteamtool.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ enabled = false
# library_x64 = "OpenSteamTool.GameHook.x64.dll"
# library_x86 = "OpenSteamTool.GameHook.x86.dll"

[cloud]
# Optional Steam Cloud save redirection for unlocked ("lua") games, powered by
# CloudRedirect (https://github.com/Selectively11/CloudRedirect).
# When enabled, OpenSteamTool loads cloud_redirect.dll inside Steam, registers
# every addappid() game as a redirected app, and routes their Steam Cloud RPCs
# through CloudRedirect's cloud-save engine.
#
# Provider sign-in (Google Drive / OneDrive / local folder) is still done through
# CloudRedirect's own companion app — OpenSteamTool only hosts the DLL.
enabled = false
# Path to cloud_redirect.dll. Absolute, or relative to the Steam root directory.
# Defaults to "<Steam>/cloud_redirect.dll" when unset.
# library = "cloud_redirect.dll"

[remote]
# Optional metadata mirror. Leave unset to use GitHub with jsDelivr fallback.
# A custom mirror replaces the built-in remote sources and must include all
Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ add_library(OpenSteamTool SHARED
Utils/Config/ConfigFileWatcher.cpp
Utils/Config/LuaConfig.cpp
Utils/Config/LuaFileWatcher.cpp
Utils/CloudRedirect/CloudRedirectHost.cpp
Utils/HookSupport/VehCommon.cpp
Utils/Logging/Log.cpp
Utils/SteamMetadata/IPCLoader.cpp
Expand Down
2,456 changes: 1,327 additions & 1,129 deletions src/Hook/Hooks_NetPacket.cpp

Large diffs are not rendered by default.

168 changes: 168 additions & 0 deletions src/Utils/CloudRedirect/CloudRedirectHost.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#include "CloudRedirectHost.h"

#include "OSTPlatform/include/DynamicLibrary.h"
#include "Utils/Config/Config.h"
#include "Utils/Config/LuaConfig.h"
#include "Utils/Logging/Log.h"

#include <atomic>
#include <filesystem>
#include <mutex>
#include <vector>

namespace CloudRedirectHost {
namespace {

// --- CloudRedirect third-party client ABI (CloudRedirect/src/common/cr_api.h)
// Declared locally so OpenSteamTool does not need CloudRedirect's headers.
using CR_NotifyFn = void (*)(int level, const char* title, const char* message);
using CR_InitCloudSave_t = bool (*)(const char* steamPath, CR_NotifyFn notify);
using CR_HandleCloudRpc_t = bool (*)(const char* method, uint32_t appId, uint32_t accountId,
const uint8_t* reqBody, uint32_t reqLen,
uint8_t* respBuf, uint32_t respMaxLen,
uint32_t* respLen, int32_t* eresult);
using CR_AddApp_t = void (*)(uint32_t appId);
using CR_RemoveApp_t = void (*)(uint32_t appId);
using CR_IsApp_t = bool (*)(uint32_t appId);
using CR_SetApps_t = void (*)(const uint32_t* appIds, uint32_t count);
using CR_Shutdown_t = void (*)();

std::mutex g_mutex;
std::atomic<bool> g_active{false};
OSTPlatform::DynamicLibrary::ModuleHandle g_module = nullptr;

CR_InitCloudSave_t g_initCloudSave = nullptr;
CR_HandleCloudRpc_t g_handleCloudRpc = nullptr;
CR_SetApps_t g_setApps = nullptr;
CR_IsApp_t g_isApp = nullptr;
CR_Shutdown_t g_shutdownFn = nullptr;

// Routes CloudRedirect's notifications into OpenSteamTool's log instead of
// popping a MessageBox from inside Steam.
void CloudNotify(int level, const char* title, const char* message) {
const char* t = title ? title : "CloudRedirect";
const char* m = message ? message : "";
switch (level) {
case 2: LOG_ERROR("[CloudRedirect] {}: {}", t, m); break;
case 1: LOG_WARN("[CloudRedirect] {}: {}", t, m); break;
default: LOG_INFO("[CloudRedirect] {}: {}", t, m); break;
}
}

std::filesystem::path ResolveLibraryPath(const std::string& steamRoot,
const std::string& configured) {
if (configured.empty())
return std::filesystem::path(steamRoot) / "cloud_redirect.dll";

std::filesystem::path lib(configured);
if (lib.is_absolute())
return lib;
return std::filesystem::path(steamRoot) / lib;
}

template <typename T>
bool ResolveSymbol(OSTPlatform::DynamicLibrary::ModuleHandle module,
const char* name, T& out) {
out = reinterpret_cast<T>(OSTPlatform::DynamicLibrary::GetSymbol(module, name));
if (!out) {
LOG_WARN("CloudRedirect: export {} not found in cloud_redirect.dll", name);
return false;
}
return true;
}

} // namespace

void Initialize(const char* steamInstallPath) {
const Config::CloudSettings cloud = Config::GetCloudSettings();
if (!cloud.enabled) {
LOG_INFO("CloudRedirect: [cloud].enabled is false, cloud save redirection disabled");
return;
}
if (!steamInstallPath || steamInstallPath[0] == '\0') {
LOG_WARN("CloudRedirect: empty Steam install path, cannot initialise");
return;
}

std::lock_guard lock(g_mutex);
if (g_active.load(std::memory_order_acquire)) return;

const std::filesystem::path libPath = ResolveLibraryPath(steamInstallPath, cloud.library);
if (!std::filesystem::exists(libPath)) {
LOG_WARN("CloudRedirect: cloud_redirect.dll not found at {}", libPath.string());
return;
}

g_module = OSTPlatform::DynamicLibrary::Load(libPath);
if (!g_module) {
LOG_WARN("CloudRedirect: failed to load {} (err={})",
libPath.string(), OSTPlatform::DynamicLibrary::GetLastErrorCode());
return;
}

bool ok = true;
ok &= ResolveSymbol(g_module, "CR_InitCloudSave", g_initCloudSave);
ok &= ResolveSymbol(g_module, "CR_HandleCloudRpc", g_handleCloudRpc);
ok &= ResolveSymbol(g_module, "CR_SetApps", g_setApps);
ok &= ResolveSymbol(g_module, "CR_IsApp", g_isApp);
ok &= ResolveSymbol(g_module, "CR_Shutdown", g_shutdownFn);
if (!ok) {
LOG_WARN("CloudRedirect: cloud_redirect.dll is missing required exports, disabling");
g_module = nullptr;
return;
}

if (!g_initCloudSave(steamInstallPath, &CloudNotify)) {
LOG_WARN("CloudRedirect: CR_InitCloudSave failed, disabling cloud save redirection");
g_module = nullptr;
return;
}

g_active.store(true, std::memory_order_release);
LOG_INFO("CloudRedirect: loaded {} and initialised cloud save redirection",
libPath.string());

// Push the current unlocked-app set without re-locking g_mutex.
std::vector<AppId_t> depots = LuaConfig::GetAllDepotIds();
std::vector<uint32_t> appIds(depots.begin(), depots.end());
g_setApps(appIds.empty() ? nullptr : appIds.data(),
static_cast<uint32_t>(appIds.size()));
LOG_INFO("CloudRedirect: registered {} redirected app(s)", appIds.size());
}

void SyncAppSet() {
if (!g_active.load(std::memory_order_acquire) || !g_setApps) return;

std::vector<AppId_t> depots = LuaConfig::GetAllDepotIds();
std::vector<uint32_t> appIds(depots.begin(), depots.end());
g_setApps(appIds.empty() ? nullptr : appIds.data(),
static_cast<uint32_t>(appIds.size()));
LOG_DEBUG("CloudRedirect: re-synced redirected app set ({} app(s))", appIds.size());
}

bool IsActive() {
return g_active.load(std::memory_order_acquire);
}

bool IsApp(uint32_t appId) {
if (!g_active.load(std::memory_order_acquire) || !g_isApp) return false;
return g_isApp(appId);
}

bool HandleCloudRpc(const char* method, uint32_t appId, uint32_t accountId,
const uint8_t* reqBody, uint32_t reqLen,
uint8_t* respBuf, uint32_t respMaxLen,
uint32_t* respLen, int32_t* eresult) {
if (!g_active.load(std::memory_order_acquire) || !g_handleCloudRpc) return false;
return g_handleCloudRpc(method, appId, accountId, reqBody, reqLen,
respBuf, respMaxLen, respLen, eresult);
}

void Shutdown() {
std::lock_guard lock(g_mutex);
if (!g_active.exchange(false)) return;
if (g_shutdownFn) g_shutdownFn();
LOG_INFO("CloudRedirect: shut down");
}

} // namespace CloudRedirectHost
40 changes: 40 additions & 0 deletions src/Utils/CloudRedirect/CloudRedirectHost.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#pragma once

#include <cstdint>

// Hosts CloudRedirect's prebuilt cloud_redirect.dll inside the Steam process and
// drives its third-party client API (see CloudRedirect/src/common/cr_api.h):
// - loads the DLL and resolves the CR_* exports,
// - calls CR_InitCloudSave in cloud-save-only mode,
// - registers every addappid() game as a redirected ("namespace") app,
// - forwards Cloud.* RPCs from the NetPacket hook to CR_HandleCloudRpc.
//
// All entry points are safe no-ops unless [cloud].enabled is set and the DLL
// loaded and initialised successfully.
namespace CloudRedirectHost {

// Load + initialise. Called once from the init worker thread, after the
// Steam hooks are installed. steamInstallPath is the Steam root directory.
void Initialize(const char* steamInstallPath);

// Re-push the current unlocked-app set to CloudRedirect. Called after a Lua
// hot-reload so the redirected set tracks addappid() changes.
void SyncAppSet();

// True once the DLL is loaded and CR_InitCloudSave succeeded.
bool IsActive();

// Whether CloudRedirect is currently redirecting saves for this appid.
bool IsApp(uint32_t appId);

// Bridge from the NetPacket hook: forwards a single Cloud.* RPC to
// CloudRedirect. Returns false when not handled (caller chains to original).
bool HandleCloudRpc(const char* method, uint32_t appId, uint32_t accountId,
const uint8_t* reqBody, uint32_t reqLen,
uint8_t* respBuf, uint32_t respMaxLen,
uint32_t* respLen, int32_t* eresult);

// Teardown. Called from DLL_PROCESS_DETACH.
void Shutdown();

}
19 changes: 19 additions & 0 deletions src/Utils/Config/Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ namespace {
std::string remoteUrlTemplate;
bool statsEnableApi = true;
InjectionSettings injection;
CloudSettings cloud;
};

std::mutex g_mutex;
Expand Down Expand Up @@ -55,6 +56,8 @@ namespace {
injectEnabled = snapshot.injection.enabled;
injectLibraryX86 = snapshot.injection.libraryX86;
injectLibraryX64 = snapshot.injection.libraryX64;
cloudEnabled = snapshot.cloud.enabled;
cloudLibrary = snapshot.cloud.library;
}

void ApplyManifestProvider(const std::string& provider) {
Expand Down Expand Up @@ -155,6 +158,14 @@ namespace {
snapshot.injection.libraryX64 = *val;
}

// [cloud]
if (auto cloud = tbl["cloud"].as_table()) {
if (auto val = (*cloud)["enabled"].value<bool>())
snapshot.cloud.enabled = *val;
if (auto val = (*cloud)["library"].value<std::string>())
snapshot.cloud.library = *val;
}

ApplyManifestProvider(snapshot.manifestProvider);
LoadResult result = ApplySnapshotLocked(snapshot);
LOG_INFO("Config loaded: manifest.url={} log.level={} lua.paths={} stats.enable_api={} remote.url_template={}",
Expand Down Expand Up @@ -230,4 +241,12 @@ namespace {
return statsEnableApi;
}

CloudSettings GetCloudSettings() {
std::lock_guard lock(g_mutex);
return {
cloudEnabled,
cloudLibrary,
};
}

}
34 changes: 22 additions & 12 deletions src/Utils/Config/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ namespace Config {
std::string libraryX64;
};

struct CloudSettings {
bool enabled = false;
std::string library;
};

struct LoadResult {
bool applied = false;
bool luaPathsChanged = false;
Expand All @@ -34,23 +39,24 @@ namespace Config {
std::vector<std::string> GetLuaPaths();
std::string GetRemoteUrlTemplate();
InjectionSettings GetInjectionSettings();
CloudSettings GetCloudSettings();
bool GetStatsEnableApi();
// [manifest] — provider selection lives in ManifestClient (table-driven).

// [manifest] — provider selection lives in ManifestClient (table-driven).
inline uint32_t manifestTimeoutResolve = 5000;
inline uint32_t manifestTimeoutConnect = 5000;
inline uint32_t manifestTimeoutSend = 10000;
inline uint32_t manifestTimeoutRecv = 10000;
// [log]
inline LogLevel logLevel = LogLevel::Debug;
// derived from configPath: <steam>/opensteamtool/
inline std::string logDir;
// [lua]
inline std::vector<std::string> luaPaths;

// [log]
inline LogLevel logLevel = LogLevel::Debug;

// derived from configPath: <steam>/opensteamtool/
inline std::string logDir;

// [lua]
inline std::vector<std::string> luaPaths;

// [remote]
inline std::string remoteUrlTemplate;

Expand All @@ -62,4 +68,8 @@ namespace Config {
inline std::string injectLibraryX86;
inline std::string injectLibraryX64;

// [cloud] - optional Steam Cloud save redirection via CloudRedirect.
inline bool cloudEnabled = false;
inline std::string cloudLibrary;

}
2 changes: 2 additions & 0 deletions src/Utils/Config/LuaFileWatcher.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "Hook/Hooks_Package.h"
#include "Utils/Config/LuaFileWatcher.h"
#include "Utils/Config/LuaConfig.h"
#include "Utils/CloudRedirect/CloudRedirectHost.h"
#include "Utils/Logging/Log.h"
#include "OSTPlatform/include/DirectoryWatch.h"

Expand Down Expand Up @@ -115,6 +116,7 @@ void ProcessChanges(const std::vector<FileChange>& changes) {
}

Hooks_Package::NotifyLicenseChanged();
CloudRedirectHost::SyncAppSet();
LOG_PACKAGE_DEBUG("Lua refresh completed");
}

Expand Down
6 changes: 6 additions & 0 deletions src/dllmain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "Hook/HookManager.h"
#include "Utils/Config/ConfigFileWatcher.h"
#include "Utils/Config/LuaFileWatcher.h"
#include "Utils/CloudRedirect/CloudRedirectHost.h"
#include "Utils/SteamMetadata/IPCLoader.h"
#include "Utils/SteamMetadata/PatternLoader.h"
#include "Utils/SteamMetadata/SteamDiagnostics.h"
Expand Down Expand Up @@ -81,6 +82,10 @@ static uint32_t InitThread(OSTPlatform::DynamicLibrary::ModuleHandle selfModule)
// Surface any functions that FindPattern() could not locate.
PatternLoader::ReportMissingFunctions();

// Optional Steam Cloud save redirection (CloudRedirect). No-op unless
// [cloud].enabled is set and cloud_redirect.dll is present.
CloudRedirectHost::Initialize(SteamInstallPath);

LOG_INFO("OpenSteamTool init complete");
return 0;
}
Expand All @@ -102,6 +107,7 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
LuaFileWatcher::Stop();
SteamUI::CoreUnhook();
SteamClient::CoreUnhook();
CloudRedirectHost::Shutdown();
}

return TRUE;
Expand Down
Loading