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
543 changes: 273 additions & 270 deletions README.md

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion README_ES.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
- Desbloquea una cantidad ilimitada de juegos que no poseas.
- Desbloquea todos los DLC para juegos que no poseas.
- Soporta la carga automática de claves de descifrado de depósitos(depots) desde la configuración de Lua.
- Soporta la descarga automática de manifiestos a través de las APIs ascendentes (upstream APIs) de `opensteamtool` / `steamrun` / `wudrm` (por defecto es opensteamtool), o mediante un endpoint personalizado de Lua (ver [Manifest a traves de Lua](#manifest-via-lua)).
- Soporta la descarga automática de manifiestos a través de las APIs ascendentes (upstream APIs) de `opensteamtool` / `steamrun` / `wudrm` (por defecto es opensteamtool), una plantilla URL personalizada, o mediante un endpoint personalizado de Lua (ver [Manifest a traves de Lua](#manifest-via-lua)).
- Soporta la descarga de juegos protegidos o DLCs que requieran un token de acceso.
- Soporta la vinculación de manifiestos para evitar que juegos específicos se actualicen.

Expand Down Expand Up @@ -135,7 +135,10 @@ level = "info"

[manifest]
# API ascendente para los códigos de solicitud de manifiestos de depósito. Opciones: "opensteamtool", "steamrun", "wudrm"
# También se acepta URL personalizada con {gid}, p. ej. url = "https://my.server/manifest/{gid}".
# format: "plain" (solo dígitos) o "steamrun" ({"content":"..."}); los integrados lo ignoran.
url = "opensteamtool"
format = "plain"

# Tiempos de espera HTTP (timeouts) para las solicitudes de manifiestos (en milisegundos)
timeout_resolve_ms = 5000
Expand Down
5 changes: 4 additions & 1 deletion README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
- 解锁任意数量未拥有的游戏
- 解锁未拥有游戏的所有 DLC
- 支持从 Lua 配置自动加载仓库(depot)解密密钥
- 支持通过 `opensteamtool` / `steamrun` / `wudrm` 上游 API 自动下载 manifest(默认为 `opensteamtool`),或通过自定义 Lua 端点(参见 [通过 Lua 获取 Manifest](#通过-lua-获取-manifest))
- 支持通过 `opensteamtool` / `steamrun` / `wudrm` 上游 API 自动下载 manifest(默认为 `opensteamtool`)、自定义 URL 模板,或通过自定义 Lua 端点(参见 [通过 Lua 获取 Manifest](#通过-lua-获取-manifest))
- 支持下载需要访问令牌的保护游戏或 DLC
- 支持绑定 manifest 以防止特定游戏被更新

Expand Down Expand Up @@ -142,7 +142,10 @@ level = "info"

[manifest]
# 仓库 manifest 请求码的上游 API。选项:"opensteamtool"、"steamrun"、"wudrm"
# 也支持包含 {gid} 的自定义 URL,例如 url = "https://my.server/manifest/{gid}"。
# format:"plain"(纯数字)或 "steamrun"({"content":"..."});内置名称忽略此项。
url = "opensteamtool"
format = "plain"

# manifest 请求的 HTTP 超时(毫秒)
timeout_resolve_ms = 5000
Expand Down
9 changes: 8 additions & 1 deletion opensteamtool.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
level = "debug"

[manifest]
# Which upstream API to query for depot manifest request codes.
# Which upstream API to query for depot manifest request codes: a built-in
# name or a custom URL template containing {gid}.
# "opensteamtool" → https://manifest.opensteamtool.com/{gid} (default)
# "wudrm" → http://gmrc.wudrm.com/manifest/{gid} (recommended for China users)
# "steamrun" → https://manifest.steam.run/api/manifest/{gid}
# custom → e.g. "https://my.server/manifest/{gid}" (must be
# http(s) with {gid}; invalid values fall back to "opensteamtool")
# If <Steam>/config/lua/manifest.lua defines fetch_manifest_code(gid) or
# fetch_manifest_code_ex(app_id, depot_id, gid), those Lua functions take
# priority over the url setting below.
Expand Down Expand Up @@ -46,6 +49,10 @@ level = "debug"
# end
url = "opensteamtool"

# Response shape of a custom url (built-ins ignore this): "plain" (bare
# digits, default) or "steamrun" ({"content":"..."}).
format = "plain"

# HTTP timeouts for manifest requests (milliseconds).
# timeout_resolve_ms — DNS resolution (default: 5000)
# timeout_connect_ms — TCP handshake (default: 5000)
Expand Down
22 changes: 13 additions & 9 deletions src/Utils/Config/Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ namespace Config {
namespace {

struct Snapshot {
std::string manifestProvider = "opensteamtool";
std::string manifestProvider = std::string(ManifestClient::kDefaultProviderName);
std::string manifestFormat = "plain";
ManifestTimeouts manifestTimeouts;
LogLevel logLevel = LogLevel::Debug;
std::string logDir;
Expand Down Expand Up @@ -60,11 +61,11 @@ namespace {
cloudLibrary = snapshot.cloud.library;
}

void ApplyManifestProvider(const std::string& provider) {
if (!ManifestClient::SetProvider(provider)) {
LOG_WARN("Unknown manifest.url \"{}\", keeping default", provider);
ManifestClient::SetProvider("opensteamtool");
}
void ApplyManifestProvider(const std::string& provider, const std::string& format) {
if (ManifestClient::SetProvider(provider)) return;
if (ManifestClient::SetCustomProvider(provider, format)) return;
LOG_WARN("Unknown manifest.url \"{}\", keeping default", provider);
ManifestClient::SetProvider(ManifestClient::kDefaultProviderName);
}

LoadResult ApplySnapshotLocked(const Snapshot& snapshot) {
Expand All @@ -83,7 +84,7 @@ namespace {
Snapshot snapshot = MakeDefaultSnapshot(configPath);
if (!std::filesystem::exists(configPath)) {
LOG_INFO("Config file not found, using defaults");
ApplyManifestProvider(snapshot.manifestProvider);
ApplyManifestProvider(snapshot.manifestProvider, snapshot.manifestFormat);
LoadResult result = ApplySnapshotLocked(snapshot);
LOG_INFO("Config loaded: manifest.url={} log.level={} lua.paths={} stats.enable_api={} remote.url_template={}",
ManifestClient::ActiveProviderName(),
Expand All @@ -102,6 +103,9 @@ namespace {
if (auto val = (*manifest)["url"].value<std::string>()) {
snapshot.manifestProvider = *val;
}
if (auto val = (*manifest)["format"].value<std::string>()) {
snapshot.manifestFormat = *val;
}
if (auto val = (*manifest)["timeout_resolve_ms"].value<int64_t>())
snapshot.manifestTimeouts.resolve = static_cast<uint32_t>(*val);
if (auto val = (*manifest)["timeout_connect_ms"].value<int64_t>())
Expand Down Expand Up @@ -166,7 +170,7 @@ namespace {
snapshot.cloud.library = *val;
}

ApplyManifestProvider(snapshot.manifestProvider);
ApplyManifestProvider(snapshot.manifestProvider, snapshot.manifestFormat);
LoadResult result = ApplySnapshotLocked(snapshot);
LOG_INFO("Config loaded: manifest.url={} log.level={} lua.paths={} stats.enable_api={} remote.url_template={}",
ManifestClient::ActiveProviderName(),
Expand All @@ -187,7 +191,7 @@ namespace {
shouldApplyDefault = !g_loadedOnce;
}
if (shouldApplyDefault) {
ApplyManifestProvider(snapshot.manifestProvider);
ApplyManifestProvider(snapshot.manifestProvider, snapshot.manifestFormat);
std::lock_guard lock(g_mutex);
const bool luaChanged = luaPaths != snapshot.luaPaths;
ApplySnapshot(snapshot);
Expand Down
3 changes: 2 additions & 1 deletion src/Utils/Config/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ namespace Config {
CloudSettings GetCloudSettings();
bool GetStatsEnableApi();

// [manifest] — provider selection lives in ManifestClient (table-driven).
// [manifest] — provider selection lives in ManifestClient
// (built-in name or custom {gid} URL template + format).
inline uint32_t manifestTimeoutResolve = 5000;
inline uint32_t manifestTimeoutConnect = 5000;
inline uint32_t manifestTimeoutSend = 10000;
Expand Down
81 changes: 68 additions & 13 deletions src/Utils/SteamMetadata/ManifestClient.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#include "ManifestClient.h"
#include "OSTPlatform/include/Http.h"
#include "OSTPlatform/include/Numbers.h"
#include "Utils/Config/Config.h"
#include "Utils/Config/LuaConfig.h"
#include "Utils/Logging/Log.h"

#include <algorithm>
#include <charconv>
#include <cstdio>
#include <mutex>
#include <string>
#include <string_view>

namespace ManifestClient {
Expand All @@ -15,10 +17,11 @@ namespace ManifestClient {
using Parser = bool (*)(std::string_view body, uint64_t* out);

static bool ParsePlainUint(std::string_view body, uint64_t* out) {
uint64_t code = 0;
auto [_, ec] = std::from_chars(body.data(), body.data() + body.size(), code);
if (ec != std::errc{}) return false;
*out = code;
const size_t end = body.find_last_not_of(" \t\r\n");
if (end == std::string_view::npos) return false;
const auto code = OSTPlatform::Numbers::ParseUInt64(body.substr(0, end + 1));
if (!code) return false;
*out = *code;
return true;
}

Expand All @@ -34,13 +37,12 @@ namespace ManifestClient {

// ── provider table ────────────────────────────────────────────
//
// Adding a new provider: add one row to kProviders below.
// host / port / tls / path are all derived from the URL template
// by Make() at compile time.
// Built-in providers below; anything else in [manifest] url is used
// as a custom URL template via SetCustomProvider, no code change needed.

struct Provider {
std::string_view name; // matches [manifest] url = "..."
const char* urlTemplate; // full literal with one %llu — for log & path
const char* urlTemplate; // %llu (built-in) or {gid} (custom)
Parser parse;
};

Expand All @@ -54,7 +56,10 @@ namespace ManifestClient {
Make("steamrun", "https://manifest.steam.run/api/manifest/%llu", ParseSteamRunJson),
};

static const Provider* g_active = &kProviders[0]; // opensteamtool
static const Provider* g_active = &kProviders[0];
static_assert(kProviders[0].name == kDefaultProviderName);
static std::string g_customUrl;
static Provider g_custom = {"custom", nullptr, ParsePlainUint};
static std::mutex g_mutex;

bool SetProvider(std::string_view name) {
Expand All @@ -67,6 +72,47 @@ namespace ManifestClient {
return false;
}

static bool IsCustomTemplate(std::string_view url) {
if (url.empty() || url.size() > 512) return false;
std::string_view rest;
if (url.starts_with("https://")) rest = url.substr(8);
else if (url.starts_with("http://")) rest = url.substr(7);
else return false;
if (rest.find("{gid}") == std::string_view::npos) return false;
// Same authority rules Http::Execute enforces: expand the placeholder,
// then require a non-empty host and a valid port.
std::string expanded(rest);
for (size_t pos = 0; (pos = expanded.find("{gid}", pos)) != std::string::npos;)
expanded.replace(pos, 5, "0");
const size_t slash = expanded.find('/');
const std::string_view hostPart(expanded.data(), slash == std::string::npos ? expanded.size() : slash);
const size_t colon = hostPart.find(':');
if (hostPart.substr(0, colon).empty()) return false;
for (const char c : hostPart.substr(0, colon))
if (static_cast<unsigned char>(c) <= 0x20 || c == 0x7f) return false;
if (colon != std::string_view::npos) {
const auto port = OSTPlatform::Numbers::ParseUInt32(hostPart.substr(colon + 1));
if (!port || *port == 0 || *port > 65535) return false;
}
return true;
}

static Parser ParserFor(std::string_view format) {
if (format == "steamrun") return ParseSteamRunJson;
return ParsePlainUint;
}

bool SetCustomProvider(std::string_view urlTemplate, std::string_view format) {
if (!IsCustomTemplate(urlTemplate)) return false;
if (format != "plain" && format != "steamrun")
LOG_WARN("Unknown manifest.format \"{}\", using plain", format);
std::lock_guard<std::mutex> lock(g_mutex);
g_customUrl.assign(urlTemplate);
g_custom = {"custom", g_customUrl.c_str(), ParserFor(format)};
Comment thread
aitronz marked this conversation as resolved.
g_active = &g_custom;
return true;
}

const char* ActiveProviderName() {
std::lock_guard<std::mutex> lock(g_mutex);
return g_active->name.data();
Expand All @@ -84,12 +130,21 @@ namespace ManifestClient {
const Provider& p = *g_active;
const Config::ManifestTimeouts timeouts = Config::GetManifestTimeouts();

char urlLog[256];
std::snprintf(urlLog, sizeof(urlLog), p.urlTemplate, gid);
std::string url;
if (g_active == &g_custom) {
url.assign(p.urlTemplate);
const std::string id = std::to_string(gid);
for (size_t pos = 0; (pos = url.find("{gid}", pos)) != std::string::npos;)
url.replace(pos, 5, id);
} else {
char urlLog[256];
std::snprintf(urlLog, sizeof(urlLog), p.urlTemplate, gid);
url.assign(urlLog);
}

auto r = OSTPlatform::Http::Execute(
L"GET",
urlLog,
url.c_str(),
nullptr,
0,
nullptr,
Expand Down
12 changes: 10 additions & 2 deletions src/Utils/SteamMetadata/ManifestClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@

// ─────────────────────────────────────────────────────────────────
// ManifestClient — HTTP client for depot manifest request codes.
// Provider table is internal (see kProviders in ManifestClient.cpp);
// adding a new provider only requires one row there.
// Built-in providers live in kProviders (ManifestClient.cpp); any
// other [manifest] url is used as a custom URL template as-is.
//
// Thread-safe — serialises access to the underlying WinHTTP connection.
// ─────────────────────────────────────────────────────────────────
namespace ManifestClient {

inline constexpr std::string_view kDefaultProviderName = "opensteamtool";

// Select the active provider by its string name (matches kProviders[i].name).
// Returns false if no provider matches; the previous selection is kept.
bool SetProvider(std::string_view name);

// Use a custom URL template containing one {gid} placeholder, e.g.
// "https://my.server/manifest/{gid}". Format selects the response
// parser: "plain" (bare digits) or "steamrun" ({"content":"..."}).
// Returns false if the template is invalid; the previous selection is kept.
bool SetCustomProvider(std::string_view urlTemplate, std::string_view format);

// Name of the currently active provider (for logging / diagnostics).
const char* ActiveProviderName();

Expand Down