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
284 changes: 260 additions & 24 deletions src/Hook/Hooks_SteamUI.cpp

Large diffs are not rendered by default.

58 changes: 46 additions & 12 deletions src/Utils/CloudRedirect/CloudRedirectHost.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include "CloudRedirectHost.h"
#include "dllmain.h"

#include "OSTPlatform/include/DynamicLibrary.h"
#include "OSTPlatform/include/Encoding.h"
#include "Utils/Config/Config.h"
#include "Utils/Config/LuaConfig.h"
#include "Utils/Logging/Log.h"
Expand Down Expand Up @@ -63,13 +65,28 @@ namespace {

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())
std::error_code ec;
const std::string filename = configured.empty() ? "cloud_redirect.dll" : configured;
const std::filesystem::path lib(OSTPlatform::Encoding::Utf8ToWide(filename));
if (lib.is_absolute()) {
return lib;
return std::filesystem::path(steamRoot) / lib;
}

// Lookup order: next to this DLL, next to opensteamtool.toml (when
// different from the Steam root), then the Steam root itself.
if (DllDir[0] != '\0') {
const auto p = std::filesystem::path(OSTPlatform::Encoding::Utf8ToWide(DllDir)) / lib;
if (std::filesystem::exists(p, ec) && !ec) return p;
}
if (ConfigPath[0] != '\0') {
const auto configParent = std::filesystem::path(OSTPlatform::Encoding::Utf8ToWide(ConfigPath)).parent_path();
const auto steamRootPath = std::filesystem::path(OSTPlatform::Encoding::Utf8ToWide(steamRoot));
if (configParent != steamRootPath) {
const auto p = configParent / lib;
if (std::filesystem::exists(p, ec) && !ec) return p;
}
}
return std::filesystem::path(OSTPlatform::Encoding::Utf8ToWide(steamRoot)) / lib;
}

template <typename T>
Expand Down Expand Up @@ -100,15 +117,17 @@ void Initialize(const char* steamInstallPath) {
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());
std::error_code libEc;
const std::string libPathUtf8 = OSTPlatform::Encoding::WideToUtf8(libPath.wstring());
if (!std::filesystem::exists(libPath, libEc) || libEc) {
LOG_WARN("CloudRedirect: cloud_redirect.dll not found at {}", libPathUtf8);
return;
}

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

Expand Down Expand Up @@ -139,8 +158,8 @@ void Initialize(const char* steamInstallPath) {
}

g_active.store(true, std::memory_order_release);
LOG_INFO("CloudRedirect: loaded {} and initialised cloud save redirection",
libPath.string());
LOG_INFO("CloudRedirect: loaded {} and initialised cloud save redirection (diversion: {:p})",
libPathUtf8, static_cast<void*>(client_hModule));

if (g_enableStatsSync) {
g_enableStatsSync(true, true);
Expand All @@ -157,7 +176,7 @@ void Initialize(const char* steamInstallPath) {
// Vtable hooks let CR handle Cloud RPCs synchronously (slot4 semantics).
if (g_installVtableHooks) {
if (g_installVtableHooks())
LOG_INFO("CloudRedirect: vtable hooks installed");
LOG_INFO("CloudRedirect: vtable hooks installed (routed to diversion module)");
else
LOG_WARN("CloudRedirect: vtable hook install failed, using packet-layer path");
}
Expand Down Expand Up @@ -215,6 +234,21 @@ void Shutdown() {
std::lock_guard lock(g_mutex);
if (!g_active.exchange(false)) return;
if (g_shutdownFn) g_shutdownFn();
// Drop every function pointer and the module handle: after shutdown the
// cloud_redirect.dll image may be unloaded and dangling calls must not
// be possible.
g_initCloudSave = nullptr;
g_handleCloudRpc = nullptr;
g_setApps = nullptr;
g_isApp = nullptr;
g_shutdownFn = nullptr;
g_enableStatsSync = nullptr;
g_setAccountId = nullptr;
g_notifyAppRunning = nullptr;
g_notifyStatsStored = nullptr;
g_getAchievements = nullptr;
g_installVtableHooks = nullptr;
g_module = nullptr;
LOG_INFO("CloudRedirect: shut down");
}

Expand Down
2 changes: 1 addition & 1 deletion src/Utils/SteamMetadata/IPCLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ bool Load(const std::string& steamclientPath)

LOG_INFO("IPCLoader: loaded {} methods across {} interfaces ({})",
MethodCount(), InterfaceCount(),
r.fromCache ? "cache fallback" : "remote");
r.fromCache ? "cache" : "remote");
return true;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Utils/SteamMetadata/PatternLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ bool Load(OSTPlatform::DynamicLibrary::ModuleHandle module, const std::string& d
PatternMap map = ParsePatternString(r.body, &parseErr);
if (!map.empty()) {
LOG_INFO("PatternLoader: loaded {} patterns for {} ({})",
map.size(), component, r.fromCache ? "cache fallback" : "remote");
map.size(), component, r.fromCache ? "cache" : "remote");
g_moduleMaps[module] = std::move(map);
return true;
}
Expand Down
44 changes: 34 additions & 10 deletions src/Utils/SteamMetadata/RemoteToml.cpp
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
#include "RemoteToml.h"
#include "OSTPlatform/include/Encoding.h"
#include "OSTPlatform/include/Http.h"
#include "Utils/Config/Config.h"
#include "Utils/Logging/Log.h"
#include "Utils/SteamMetadata/SteamDiagnostics.h"

#include <chrono>
#include <filesystem>
#include <format>
#include <fstream>
#include <iterator>
#include <string_view>
#include <vector>
#include <windows.h>

namespace RemoteToml {

Expand Down Expand Up @@ -95,17 +98,19 @@ Result Fetch(const Request& request)
LOG_INFO("RemoteToml({}/{}): sha256 = {} ({} ms)",
request.channel, request.component, out.sha256, hashMs);

// 2. Cache path & dir.
fs::path steamRoot = fs::path(request.dllPath).parent_path();
// 2. Cache path & dir. UTF-8 input is converted explicitly so non-ASCII
// install paths resolve correctly.
fs::path steamRoot = fs::path(OSTPlatform::Encoding::Utf8ToWide(request.dllPath)).parent_path();
fs::path cacheDir = steamRoot / "opensteamtool" / request.channel / request.component;
fs::path cachePath = cacheDir / (out.sha256 + ".toml");
const std::string cachePathText = cachePath.string();
const std::string cachePathText = OSTPlatform::Encoding::WideToUtf8(cachePath.wstring());

std::error_code mkdirEc;
fs::create_directories(cacheDir, mkdirEc);
if (mkdirEc) {
LOG_WARN("RemoteToml({}/{}): could not create cache dir {} ({})",
request.channel, request.component, cacheDir.string(), mkdirEc.message());
request.channel, request.component,
OSTPlatform::Encoding::WideToUtf8(cacheDir.wstring()), mkdirEc.message());
}

// 3. Try remote (mirror chain with early-out on 404).
Expand Down Expand Up @@ -135,25 +140,44 @@ Result Fetch(const Request& request)
}
}

// 4. Remote OK → write cache, return body.
// 4. Remote OK -> write the cache atomically, return body.
if (http.ok && http.status == 200 && !http.body.empty()) {
std::ofstream ofs(cachePath, std::ios::binary);
std::string tempFileName = std::format("{}_{}.tmp", out.sha256, GetCurrentProcessId());
fs::path tempPath = cacheDir / tempFileName;
std::ofstream ofs(tempPath, std::ios::binary);
if (ofs) {
ofs.write(http.body.data(),
static_cast<std::streamsize>(http.body.size()));
LOG_INFO("RemoteToml({}/{}): cached to {}",
request.channel, request.component, cachePathText);
ofs.close();

if (MoveFileExW(tempPath.c_str(), cachePath.c_str(), MOVEFILE_REPLACE_EXISTING)) {
LOG_INFO("RemoteToml({}/{}): cached to {}",
request.channel, request.component, cachePathText);
} else {
std::error_code renameEc;
fs::rename(tempPath, cachePath, renameEc);
if (!renameEc) {
LOG_INFO("RemoteToml({}/{}): cached to {}",
request.channel, request.component, cachePathText);
} else {
LOG_WARN("RemoteToml({}/{}): atomic cache rename failed: {}",
request.channel, request.component, renameEc.message());
fs::remove(tempPath, renameEc);
}
}
} else {
LOG_WARN("RemoteToml({}/{}): could not open {} for writing",
request.channel, request.component, cachePathText);
request.channel, request.component,
OSTPlatform::Encoding::WideToUtf8(tempPath.wstring()));
}
out.body = std::move(http.body);
out.ok = true;
return out;
}

// 5. Remote failed → fall back to whatever is cached for this exact SHA.
if (fs::exists(cachePath)) {
std::error_code existsEc;
if (fs::exists(cachePath, existsEc) && !existsEc) {
LOG_WARN("RemoteToml({}/{}): remote failed (last URL {} HTTP {}); "
"falling back to local cache {}",
request.channel, request.component,
Expand Down
2 changes: 1 addition & 1 deletion src/Utils/SteamMetadata/RemoteToml.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace RemoteToml {
std::string sha256;
};

// Fetch remote TOML first, then fall back to the exact local cache entry.
// Load exact local cache entry first, then fall back to fetching remote TOML.
Result Fetch(const Request& request);

} // namespace RemoteToml
Loading