From 3d7bf8193d6b91bb1198ca30e917585ec7d3ec2d Mon Sep 17 00:00:00 2001
From: Aitron <74368135+aitronz@users.noreply.github.com>
Date: Sun, 13 Sep 2026 20:36:05 +0200
Subject: [PATCH] Allow custom manifest sources via url template + format
[manifest] url now accepts a built-in provider name or a custom
http(s) URL template containing {gid}. A new [manifest] format key
("plain" | "steamrun", default "plain") selects the response
parser for custom URLs; built-in providers keep their hardcoded
parsers. Invalid values warn and fall back to opensteamtool.
---
README.md | 543 +++++++++++----------
README_ES.md | 5 +-
README_ZH.md | 5 +-
opensteamtool.example.toml | 9 +-
src/Utils/Config/Config.cpp | 22 +-
src/Utils/Config/Config.h | 3 +-
src/Utils/SteamMetadata/ManifestClient.cpp | 81 ++-
src/Utils/SteamMetadata/ManifestClient.h | 12 +-
8 files changed, 382 insertions(+), 298 deletions(-)
diff --git a/README.md b/README.md
index d62b9fa6..a7ec1eab 100644
--- a/README.md
+++ b/README.md
@@ -1,270 +1,273 @@
-
-
-## Feature
-
-### Core Unlocks
-- Unlock an unlimited number of unowned games.
-- Unlock all DLCs for unowned games.
-- Support auto load depot decryption keys from Lua config.
-- Support auto manifest download via `opensteamtool` / `steamrun` / `wudrm` upstream APIs (default is `opensteamtool`), or a custom Lua endpoint (see [Manifest via Lua](#manifest-via-lua)).
-- Support downloading protected games or DLCs that require an access token.
-- Support binding manifest to prevent specific games from being updated.
-
-### Hot Reload
-- Adding, modifying, deleting, or overwriting `.lua` files in any watched directory automatically triggers a reload. No restart, no offline/online toggle needed.
-
-### Injection
-- Add optional game-process library injection through `[inject]` in `opensteamtool.toml`.
-- Configure `enabled`, `library_x64`, and `library_x86`; the injected library must match the target process architecture.`library_x64` and `library_x86` may be absolute paths, or relative paths resolved from the Steam root directory.
-
-### Family Sharing and Remote Play
-- Bypass Steam Family Sharing restrictions for games that have been added to the library with `addappid` in Lua. All accounts in the Steam Family that participate in sharing must use OpenSteamTool for this to work.
-
-### Compatible with games protected by Denuvo and SteamStub
-- SteamStub-only games do not require configuring `AppTicket`. OpenSteamTool can reuse Steam's local ConfigStore ticket and forge the requested AppId through a SteamDRMP off-by-four ticket parsing vulnerability, without injecting into the game process.
-- Denuvo-protected games still require explicit ticket data. OpenSteamTool stores `AppTicket` and `ETicket` through the platform credential store.
-- Use `setAppTicket(appid, "hex")` and `setETicket(appid, "hex")` in Lua config to write these values to the platform credential store automatically.
-- Denuvo verification has a 30-minute validity window. After this window expires, authorization may fail with Denuvo error code `88500005`; refresh the ticket data before retrying.
-- AppTicket priority: explicit tickets have the highest priority, including tickets configured by `setAppTicket` and existing cached `AppTicket` credential values. If no explicit AppTicket is available, OpenSteamTool falls back to the forged local ConfigStore ticket path.
-- SteamID priority: read cached `SteamID` first; if missing, parse from explicit `AppTicket`. On Windows, the credential store backend currently uses `HKCU\Software\Valve\Steam\Apps\`. The Linux backend is not implemented yet.
-
-#### Extracting tickets with `extract_tickets`
-
-The `extract_tickets` tool dumps the `AppTicket` and `ETicket` hex strings you need for `setAppTicket` / `setETicket`. Run it on a machine where Steam is running and logged into an account that **owns** the target game.
-
-1. Build the tools (see [Build](#build)); the binary lands in `build/tools/Release/extract_tickets.exe`.
-2. Run it with the target AppId (or run it with no argument and type the AppId when prompted):
- ```powershell
- extract_tickets.exe 1361510
- ```
-3. It reads the Steam install path from the registry, loads `steamclient64.dll`, and writes everything into an `/` folder next to the executable:
- - `appticket.bin` — raw app ownership ticket (binary)
- - `eticket.bin` — raw encrypted app ticket (binary)
- - `tickets.txt` — plain-text summary with the hex strings:
- ```
- appid:1361510
- appticket(184 bytes):14000000...
- eticket(143 bytes):...
- ```
- A ticket that could not be obtained is reported as `appticket:null` / `eticket:null`.
-4. Paste the hex strings from `tickets.txt` into your Lua config:
- ```lua
- setAppTicket(1361510, "14000000...")
- setETicket(1361510, "...")
- ```
-
-> **Note:** Tickets are only valid when extracted from an account that **genuinely owns** the game.
-
-### Stats and Achievements
-- Enable stats and achievements for unowned games.
-- Uses `setStat(appid, "steamid")` to configure which SteamID's achievement data to pull.
-- If no `setStat` is configured for an app, OpenSteamTool queries `https://stats.opensteamtool.com/{appid}` when `[stats] enable_api = true` (default).
-- Priority: `setStat` > stats API when enabled and valid > hardcoded preset SteamID `76561198028121353`.
-
-### Online Fix
-- Add `-onlinefix` to the Steam launch parameters to enable 480-based online play in games that use lobby matchmaking. The current limitation is that only one such game can run at a time.To revert, simply remove -onlinefix from the launch parameters — online play returns to normal on the next launch.
-
-## Future
-- Steam Cloud synchronization support.(This is a huge project)
-
-## Usage
-1. Run `build.bat` from the project root to build the project.
-2. Copy generated `dwmapi.dll`, `xinput1_4.dll` and `OpenSteamTool.dll` to the Steam root directory.
-3. Create Lua directory (for example `C:\steam\config\lua`) and place Lua scripts there. The DLL will automatically load and execute them.
-4. Lua example:
-```lua
-addappid(1361510) -- unlock game with appid 1361510
-
-addappid(1361511, 0,"5954562e7f5260400040a818bc29b60b335bb690066ff767e20d145a3b6b4af0") -- unlock game with appid 1361511 depotKey is "5954562e7f5260400040a818bc29b60b335bb690066ff767e20d145a3b6b4af0"
-
-addtoken(1361510,"2764735786934684318") -- add access token ("2764735786934684318") for game with appid 1361510
--- No Longer Supported:
---pinApp(1361510) -- pin game with appid 1361510 to prevent it from being updated
-
-setManifestid(1361511,"5656605350306673283") -- pin depotid:1361511 manifest_gid:5656605350306673283, size defaults to 0
-setManifestid(1361511,"5656605350306673283", 12345678) -- same but with explicit size
-
-setAppTicket(1361510,"0100000000000000...") -- write AppTicket to the credential store; on Windows: HKCU\Software\Valve\Steam\Apps\1361510\AppTicket
-
-setETicket(1361510,"0100000000000000...") -- write ETicket to the credential store; on Windows: HKCU\Software\Valve\Steam\Apps\1361510\ETicket
-
-setStat(1361510, "76561197960287930") -- use the specified SteamID's achievement data for appid 1361510
--- If not configured, the stats API is used when enabled; otherwise default SteamID 76561198028121353 is used.
-```
-
-All function names are **case-insensitive**. `setAppTicket`, `setappticket`, `SetAppticket`, `SETAPPTICKET` etc. are all equivalent. The same applies to every registered function (`addAppId`, `AddToken`, `SETManifestid`, etc.).
-
-### Configuration (optional)
-
-Rename `opensteamtool.example.toml` to `opensteamtool.toml` and place it in the Steam root directory (next to `steam.exe`).
-If no config file is found, built-in defaults are used — no auto-creation.
-The file is watched while Steam is running; valid changes are hot-reloaded without restarting Steam.
-
-```toml
-[log]
-# Debug build only. Level: trace, debug, info, warn, error
-level = "info"
-
-[manifest]
-# Upstream API for depot manifest request codes. Options: "opensteamtool", "steamrun", "wudrm"
-url = "opensteamtool"
-
-# HTTP timeouts for manifest requests (milliseconds)
-timeout_resolve_ms = 5000
-timeout_connect_ms = 5000
-timeout_send_ms = 10000
-timeout_recv_ms = 10000
-
-[stats]
-# Query https://stats.opensteamtool.com/{appid} when no Lua setStat override exists.
-# Priority: setStat > stats API > hardcoded preset SteamID.
-enable_api = true
-
-# Additional Lua config directories (optional).
-# Files are loaded after the default /config/lua folder.
-# The default folder is always loaded last so user files take priority.
-[lua]
-paths = []
-
-[inject]
-# Optional library injection into game processes.
-# The injected library must match the target process architecture.
-enabled = false
-# library_x64 = "OpenSteamTool.GameHook.x64.dll"
-# library_x86 = "OpenSteamTool.GameHook.x86.dll"
-
-# Optional metadata mirror. See "Steam version compatibility" below.
-[remote]
-# url_template = "https://your.server/{channel}/{component}/{sha256}.toml"
-```
-
-### Manifest via Lua
-
-Two manifest code functions are supported:
-
-#### `fetch_manifest_code(gid)`
-
-Basic function that receives only the manifest GID.
-
-#### `fetch_manifest_code_ex(app_id, depot_id, gid)` *(recommended)*
-
-Extended function that receives `app_id`, `depot_id`, and `gid`. Allows constructing API endpoints that require app identification.
-
-The C++ runtime provides two Lua helpers:
-
-| Function | Signature | Returns |
-|----------|-----------|---------|
-| `http_get` | `http_get(url [, headers])` | `body, status_code` |
-| `http_post` | `http_post(url, body [, headers])` | `body, status_code` |
-
-`headers` is an optional table: `{["Key"]="Value", ...}`.
-
-### Steam version compatibility
-
-OpenSteamTool no longer ships byte-pattern signatures inside the DLL. Instead, on each launch it computes the SHA-256 of `steamclient64.dll` and `steamui.dll` on disk and looks up a matching pattern file from the upstream tracker at [`OpenSteam001/steam-monitor`](https://github.com/OpenSteam001/steam-monitor) (`pattern` branch).
-
-Lookup order (every launch):
-
-1. **GitHub raw** — `https://raw.githubusercontent.com/OpenSteam001/steam-monitor/pattern/...`. Canonical source.
-2. **jsDelivr CDN** — automatic fallback if GitHub raw is unreachable (connection refused / timeout / 5xx). No configuration required. Useful in regions where `raw.githubusercontent.com` is blocked but jsDelivr is reachable (e.g. mainland China).
-3. **Local cache** — `\opensteamtool\pattern\\.toml`. Used **only** when remote is unreachable. The cache is overwritten after every successful remote fetch.
-
-Remote is consulted on every launch so users automatically pick up upstream re-publications (e.g. the bot adding a new signature, or fixing an existing one) without having to clear any cache.
-
-If a step returns **HTTP 404** the mirror loop stops immediately — all mirrors serve the same content, so a 404 means the upstream bot has not yet published a TOML for this Steam build. The code then falls back to the local cache if one exists; otherwise a one-shot popup appears with the unmatched DLL name, its SHA-256, the expected cache path, and the upstream URL. Only the hooks tied to that DLL are disabled — the rest of OpenSteamTool keeps working.
-
-You can also drop a pattern TOML into the cache directory manually if you know the layout for a given build; the file name must be `.toml`. The cache fallback will pick it up the next time remote is unreachable.
-
-> A short outbound HTTPS request is performed at every launch (one per DLL: `steamclient64.dll`, `steamui.dll`). The downloaded bodies are tiny (~10 KB each) and the work runs on a worker thread, so it never blocks Steam's loader.
-
-#### Using a different mirror
-
-For most users, the built-in **GitHub -> jsDelivr** fallback is enough. To use a private mirror or intranet server, configure a full URL template. A custom mirror replaces the built-in remote sources; local cache fallback remains available.
-
-The template must include `{channel}`, `{component}`, and `{sha256}`. Channels currently used are `pattern` and `ipc`.
-
-```toml
-[remote]
-url_template = "https://your.server/{channel}/{component}/{sha256}.toml"
-# url_template = "https://fast.jsdelivr.net/gh/OpenSteam001/steam-monitor@{channel}/{component}/{sha256}.toml"
-```
-
-### Debug logging
-
-Debug builds write per-module log files under `/opensteamtool/`:
-
-| File | Source | Content |
-|------|--------|---------|
-| `main.log` | General | Init, config loading, Lua parsing, utilities |
-| `ipc.log` | `LOG_IPC_*` | IPC commands, InterfaceCall dispatch, spoofing |
-| `netpacket.log` | `LOG_NETPACKET_*` | Network packet send/recv, eMsg dispatch |
-| `manifest.log` | `LOG_MANIFEST_*` | Manifest download, `fetch_manifest_code`, manifest binding |
-| `decryptionkey.log` | `LOG_DECRYPTIONKEY_*` | Depot decryption key injection |
-| `keyvalue.log` | `LOG_KEYVALUE_*` | KeyValues patching (manifest binding) |
-| `misc.log` | `LOG_MISC_*` | Engine pointer capture, AppId hints |
-| `achievement.log` | `LOG_ACHIEVEMENT_*` | UserStats requests/responses, steamid spoofing |
-| `pics.log` | `LOG_PICS_*` | PICS access token injection |
-| `package.log` | `LOG_PACKAGE_*` | Package injection, FileWatcher events |
-| `onlinefix.log` | `LOG_ONLINEFIX_*` | Online fix (480 AppId spoofing) |
-| `richpresence.log` | `LOG_RICHPRESENCE_*` | Rich Presence packet construction and injection |
-| `steamui.log` | `LOG_STEAMUI_*` | SteamUI hook diagnostics |
-| `pipe.log` | `LOG_PIPE_*` | Pipe handshakes, process inspection, Denuvo authorization, library injection |
-| `platform.log` | `LOG_PLATFORM_*` | Platform helper diagnostics, including remote-process operations |
-
-The log level is controlled by `[log] level` in `opensteamtool.toml`.
-
-## Build
-
-### Requirements
-- Windows 10/11
-- CMake 3.20+
-- Visual Studio 2022 with MSVC (x64 toolchain)
-
-### Runtime requirements
-- Outbound HTTPS access to `raw.githubusercontent.com` on first launch after a Steam update (see [Steam version compatibility](#steam-version-compatibility)). Cached afterwards.
-
-### Quick build
-```powershell
-build.bat
-```
-
-### Output
-- Debug: `build/Debug/OpenSteamTool.dll`, `build/Debug/dwmapi.dll`, `build/Debug/xinput1_4.dll`
-- Release: `build/Release/OpenSteamTool.dll`, `build/Release/dwmapi.dll`, `build/Release/xinput1_4.dll`
-
-## Disclaimer
-This project is provided for research and educational purposes only. You are responsible for complying with local laws, platform terms of service, and software licenses.
+
+
+## Feature
+
+### Core Unlocks
+- Unlock an unlimited number of unowned games.
+- Unlock all DLCs for unowned games.
+- Support auto load depot decryption keys from Lua config.
+- Support auto manifest download via `opensteamtool` / `steamrun` / `wudrm` upstream APIs (default is `opensteamtool`), a custom URL template, or a custom Lua endpoint (see [Manifest via Lua](#manifest-via-lua)).
+- Support downloading protected games or DLCs that require an access token.
+- Support binding manifest to prevent specific games from being updated.
+
+### Hot Reload
+- Adding, modifying, deleting, or overwriting `.lua` files in any watched directory automatically triggers a reload. No restart, no offline/online toggle needed.
+
+### Injection
+- Add optional game-process library injection through `[inject]` in `opensteamtool.toml`.
+- Configure `enabled`, `library_x64`, and `library_x86`; the injected library must match the target process architecture.`library_x64` and `library_x86` may be absolute paths, or relative paths resolved from the Steam root directory.
+
+### Family Sharing and Remote Play
+- Bypass Steam Family Sharing restrictions for games that have been added to the library with `addappid` in Lua. All accounts in the Steam Family that participate in sharing must use OpenSteamTool for this to work.
+
+### Compatible with games protected by Denuvo and SteamStub
+- SteamStub-only games do not require configuring `AppTicket`. OpenSteamTool can reuse Steam's local ConfigStore ticket and forge the requested AppId through a SteamDRMP off-by-four ticket parsing vulnerability, without injecting into the game process.
+- Denuvo-protected games still require explicit ticket data. OpenSteamTool stores `AppTicket` and `ETicket` through the platform credential store.
+- Use `setAppTicket(appid, "hex")` and `setETicket(appid, "hex")` in Lua config to write these values to the platform credential store automatically.
+- Denuvo verification has a 30-minute validity window. After this window expires, authorization may fail with Denuvo error code `88500005`; refresh the ticket data before retrying.
+- AppTicket priority: explicit tickets have the highest priority, including tickets configured by `setAppTicket` and existing cached `AppTicket` credential values. If no explicit AppTicket is available, OpenSteamTool falls back to the forged local ConfigStore ticket path.
+- SteamID priority: read cached `SteamID` first; if missing, parse from explicit `AppTicket`. On Windows, the credential store backend currently uses `HKCU\Software\Valve\Steam\Apps\`. The Linux backend is not implemented yet.
+
+#### Extracting tickets with `extract_tickets`
+
+The `extract_tickets` tool dumps the `AppTicket` and `ETicket` hex strings you need for `setAppTicket` / `setETicket`. Run it on a machine where Steam is running and logged into an account that **owns** the target game.
+
+1. Build the tools (see [Build](#build)); the binary lands in `build/tools/Release/extract_tickets.exe`.
+2. Run it with the target AppId (or run it with no argument and type the AppId when prompted):
+ ```powershell
+ extract_tickets.exe 1361510
+ ```
+3. It reads the Steam install path from the registry, loads `steamclient64.dll`, and writes everything into an `/` folder next to the executable:
+ - `appticket.bin` — raw app ownership ticket (binary)
+ - `eticket.bin` — raw encrypted app ticket (binary)
+ - `tickets.txt` — plain-text summary with the hex strings:
+ ```
+ appid:1361510
+ appticket(184 bytes):14000000...
+ eticket(143 bytes):...
+ ```
+ A ticket that could not be obtained is reported as `appticket:null` / `eticket:null`.
+4. Paste the hex strings from `tickets.txt` into your Lua config:
+ ```lua
+ setAppTicket(1361510, "14000000...")
+ setETicket(1361510, "...")
+ ```
+
+> **Note:** Tickets are only valid when extracted from an account that **genuinely owns** the game.
+
+### Stats and Achievements
+- Enable stats and achievements for unowned games.
+- Uses `setStat(appid, "steamid")` to configure which SteamID's achievement data to pull.
+- If no `setStat` is configured for an app, OpenSteamTool queries `https://stats.opensteamtool.com/{appid}` when `[stats] enable_api = true` (default).
+- Priority: `setStat` > stats API when enabled and valid > hardcoded preset SteamID `76561198028121353`.
+
+### Online Fix
+- Add `-onlinefix` to the Steam launch parameters to enable 480-based online play in games that use lobby matchmaking. The current limitation is that only one such game can run at a time.To revert, simply remove -onlinefix from the launch parameters — online play returns to normal on the next launch.
+
+## Future
+- Steam Cloud synchronization support.(This is a huge project)
+
+## Usage
+1. Run `build.bat` from the project root to build the project.
+2. Copy generated `dwmapi.dll`, `xinput1_4.dll` and `OpenSteamTool.dll` to the Steam root directory.
+3. Create Lua directory (for example `C:\steam\config\lua`) and place Lua scripts there. The DLL will automatically load and execute them.
+4. Lua example:
+```lua
+addappid(1361510) -- unlock game with appid 1361510
+
+addappid(1361511, 0,"5954562e7f5260400040a818bc29b60b335bb690066ff767e20d145a3b6b4af0") -- unlock game with appid 1361511 depotKey is "5954562e7f5260400040a818bc29b60b335bb690066ff767e20d145a3b6b4af0"
+
+addtoken(1361510,"2764735786934684318") -- add access token ("2764735786934684318") for game with appid 1361510
+-- No Longer Supported:
+--pinApp(1361510) -- pin game with appid 1361510 to prevent it from being updated
+
+setManifestid(1361511,"5656605350306673283") -- pin depotid:1361511 manifest_gid:5656605350306673283, size defaults to 0
+setManifestid(1361511,"5656605350306673283", 12345678) -- same but with explicit size
+
+setAppTicket(1361510,"0100000000000000...") -- write AppTicket to the credential store; on Windows: HKCU\Software\Valve\Steam\Apps\1361510\AppTicket
+
+setETicket(1361510,"0100000000000000...") -- write ETicket to the credential store; on Windows: HKCU\Software\Valve\Steam\Apps\1361510\ETicket
+
+setStat(1361510, "76561197960287930") -- use the specified SteamID's achievement data for appid 1361510
+-- If not configured, the stats API is used when enabled; otherwise default SteamID 76561198028121353 is used.
+```
+
+All function names are **case-insensitive**. `setAppTicket`, `setappticket`, `SetAppticket`, `SETAPPTICKET` etc. are all equivalent. The same applies to every registered function (`addAppId`, `AddToken`, `SETManifestid`, etc.).
+
+### Configuration (optional)
+
+Rename `opensteamtool.example.toml` to `opensteamtool.toml` and place it in the Steam root directory (next to `steam.exe`).
+If no config file is found, built-in defaults are used — no auto-creation.
+The file is watched while Steam is running; valid changes are hot-reloaded without restarting Steam.
+
+```toml
+[log]
+# Debug build only. Level: trace, debug, info, warn, error
+level = "info"
+
+[manifest]
+# Upstream API for depot manifest request codes. Options: "opensteamtool", "steamrun", "wudrm"
+# Custom URL with {gid} also accepted, e.g. url = "https://my.server/manifest/{gid}".
+# format: "plain" (bare digits) or "steamrun" ({"content":"..."}); ignored by built-ins.
+url = "opensteamtool"
+format = "plain"
+
+# HTTP timeouts for manifest requests (milliseconds)
+timeout_resolve_ms = 5000
+timeout_connect_ms = 5000
+timeout_send_ms = 10000
+timeout_recv_ms = 10000
+
+[stats]
+# Query https://stats.opensteamtool.com/{appid} when no Lua setStat override exists.
+# Priority: setStat > stats API > hardcoded preset SteamID.
+enable_api = true
+
+# Additional Lua config directories (optional).
+# Files are loaded after the default /config/lua folder.
+# The default folder is always loaded last so user files take priority.
+[lua]
+paths = []
+
+[inject]
+# Optional library injection into game processes.
+# The injected library must match the target process architecture.
+enabled = false
+# library_x64 = "OpenSteamTool.GameHook.x64.dll"
+# library_x86 = "OpenSteamTool.GameHook.x86.dll"
+
+# Optional metadata mirror. See "Steam version compatibility" below.
+[remote]
+# url_template = "https://your.server/{channel}/{component}/{sha256}.toml"
+```
+
+### Manifest via Lua
+
+Two manifest code functions are supported:
+
+#### `fetch_manifest_code(gid)`
+
+Basic function that receives only the manifest GID.
+
+#### `fetch_manifest_code_ex(app_id, depot_id, gid)` *(recommended)*
+
+Extended function that receives `app_id`, `depot_id`, and `gid`. Allows constructing API endpoints that require app identification.
+
+The C++ runtime provides two Lua helpers:
+
+| Function | Signature | Returns |
+|----------|-----------|---------|
+| `http_get` | `http_get(url [, headers])` | `body, status_code` |
+| `http_post` | `http_post(url, body [, headers])` | `body, status_code` |
+
+`headers` is an optional table: `{["Key"]="Value", ...}`.
+
+### Steam version compatibility
+
+OpenSteamTool no longer ships byte-pattern signatures inside the DLL. Instead, on each launch it computes the SHA-256 of `steamclient64.dll` and `steamui.dll` on disk and looks up a matching pattern file from the upstream tracker at [`OpenSteam001/steam-monitor`](https://github.com/OpenSteam001/steam-monitor) (`pattern` branch).
+
+Lookup order (every launch):
+
+1. **GitHub raw** — `https://raw.githubusercontent.com/OpenSteam001/steam-monitor/pattern/...`. Canonical source.
+2. **jsDelivr CDN** — automatic fallback if GitHub raw is unreachable (connection refused / timeout / 5xx). No configuration required. Useful in regions where `raw.githubusercontent.com` is blocked but jsDelivr is reachable (e.g. mainland China).
+3. **Local cache** — `\opensteamtool\pattern\\.toml`. Used **only** when remote is unreachable. The cache is overwritten after every successful remote fetch.
+
+Remote is consulted on every launch so users automatically pick up upstream re-publications (e.g. the bot adding a new signature, or fixing an existing one) without having to clear any cache.
+
+If a step returns **HTTP 404** the mirror loop stops immediately — all mirrors serve the same content, so a 404 means the upstream bot has not yet published a TOML for this Steam build. The code then falls back to the local cache if one exists; otherwise a one-shot popup appears with the unmatched DLL name, its SHA-256, the expected cache path, and the upstream URL. Only the hooks tied to that DLL are disabled — the rest of OpenSteamTool keeps working.
+
+You can also drop a pattern TOML into the cache directory manually if you know the layout for a given build; the file name must be `.toml`. The cache fallback will pick it up the next time remote is unreachable.
+
+> A short outbound HTTPS request is performed at every launch (one per DLL: `steamclient64.dll`, `steamui.dll`). The downloaded bodies are tiny (~10 KB each) and the work runs on a worker thread, so it never blocks Steam's loader.
+
+#### Using a different mirror
+
+For most users, the built-in **GitHub -> jsDelivr** fallback is enough. To use a private mirror or intranet server, configure a full URL template. A custom mirror replaces the built-in remote sources; local cache fallback remains available.
+
+The template must include `{channel}`, `{component}`, and `{sha256}`. Channels currently used are `pattern` and `ipc`.
+
+```toml
+[remote]
+url_template = "https://your.server/{channel}/{component}/{sha256}.toml"
+# url_template = "https://fast.jsdelivr.net/gh/OpenSteam001/steam-monitor@{channel}/{component}/{sha256}.toml"
+```
+
+### Debug logging
+
+Debug builds write per-module log files under `/opensteamtool/`:
+
+| File | Source | Content |
+|------|--------|---------|
+| `main.log` | General | Init, config loading, Lua parsing, utilities |
+| `ipc.log` | `LOG_IPC_*` | IPC commands, InterfaceCall dispatch, spoofing |
+| `netpacket.log` | `LOG_NETPACKET_*` | Network packet send/recv, eMsg dispatch |
+| `manifest.log` | `LOG_MANIFEST_*` | Manifest download, `fetch_manifest_code`, manifest binding |
+| `decryptionkey.log` | `LOG_DECRYPTIONKEY_*` | Depot decryption key injection |
+| `keyvalue.log` | `LOG_KEYVALUE_*` | KeyValues patching (manifest binding) |
+| `misc.log` | `LOG_MISC_*` | Engine pointer capture, AppId hints |
+| `achievement.log` | `LOG_ACHIEVEMENT_*` | UserStats requests/responses, steamid spoofing |
+| `pics.log` | `LOG_PICS_*` | PICS access token injection |
+| `package.log` | `LOG_PACKAGE_*` | Package injection, FileWatcher events |
+| `onlinefix.log` | `LOG_ONLINEFIX_*` | Online fix (480 AppId spoofing) |
+| `richpresence.log` | `LOG_RICHPRESENCE_*` | Rich Presence packet construction and injection |
+| `steamui.log` | `LOG_STEAMUI_*` | SteamUI hook diagnostics |
+| `pipe.log` | `LOG_PIPE_*` | Pipe handshakes, process inspection, Denuvo authorization, library injection |
+| `platform.log` | `LOG_PLATFORM_*` | Platform helper diagnostics, including remote-process operations |
+
+The log level is controlled by `[log] level` in `opensteamtool.toml`.
+
+## Build
+
+### Requirements
+- Windows 10/11
+- CMake 3.20+
+- Visual Studio 2022 with MSVC (x64 toolchain)
+
+### Runtime requirements
+- Outbound HTTPS access to `raw.githubusercontent.com` on first launch after a Steam update (see [Steam version compatibility](#steam-version-compatibility)). Cached afterwards.
+
+### Quick build
+```powershell
+build.bat
+```
+
+### Output
+- Debug: `build/Debug/OpenSteamTool.dll`, `build/Debug/dwmapi.dll`, `build/Debug/xinput1_4.dll`
+- Release: `build/Release/OpenSteamTool.dll`, `build/Release/dwmapi.dll`, `build/Release/xinput1_4.dll`
+
+## Disclaimer
+This project is provided for research and educational purposes only. You are responsible for complying with local laws, platform terms of service, and software licenses.
diff --git a/README_ES.md b/README_ES.md
index 5324dcab..1b74ff6e 100644
--- a/README_ES.md
+++ b/README_ES.md
@@ -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.
@@ -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
diff --git a/README_ZH.md b/README_ZH.md
index 39940a6a..20501e6c 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -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 以防止特定游戏被更新
@@ -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
diff --git a/opensteamtool.example.toml b/opensteamtool.example.toml
index 574a8f75..0960ab68 100644
--- a/opensteamtool.example.toml
+++ b/opensteamtool.example.toml
@@ -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 /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.
@@ -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)
diff --git a/src/Utils/Config/Config.cpp b/src/Utils/Config/Config.cpp
index 953f4b69..8a1c7884 100644
--- a/src/Utils/Config/Config.cpp
+++ b/src/Utils/Config/Config.cpp
@@ -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;
@@ -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) {
@@ -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(),
@@ -102,6 +103,9 @@ namespace {
if (auto val = (*manifest)["url"].value()) {
snapshot.manifestProvider = *val;
}
+ if (auto val = (*manifest)["format"].value()) {
+ snapshot.manifestFormat = *val;
+ }
if (auto val = (*manifest)["timeout_resolve_ms"].value())
snapshot.manifestTimeouts.resolve = static_cast(*val);
if (auto val = (*manifest)["timeout_connect_ms"].value())
@@ -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(),
@@ -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);
diff --git a/src/Utils/Config/Config.h b/src/Utils/Config/Config.h
index 82f145a2..08ae83e3 100644
--- a/src/Utils/Config/Config.h
+++ b/src/Utils/Config/Config.h
@@ -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;
diff --git a/src/Utils/SteamMetadata/ManifestClient.cpp b/src/Utils/SteamMetadata/ManifestClient.cpp
index 7cef98bc..d1866e48 100644
--- a/src/Utils/SteamMetadata/ManifestClient.cpp
+++ b/src/Utils/SteamMetadata/ManifestClient.cpp
@@ -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
-#include
+#include
#include
+#include
#include
namespace ManifestClient {
@@ -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;
}
@@ -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;
};
@@ -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) {
@@ -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(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 lock(g_mutex);
+ g_customUrl.assign(urlTemplate);
+ g_custom = {"custom", g_customUrl.c_str(), ParserFor(format)};
+ g_active = &g_custom;
+ return true;
+ }
+
const char* ActiveProviderName() {
std::lock_guard lock(g_mutex);
return g_active->name.data();
@@ -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,
diff --git a/src/Utils/SteamMetadata/ManifestClient.h b/src/Utils/SteamMetadata/ManifestClient.h
index 2f19c301..ab62498d 100644
--- a/src/Utils/SteamMetadata/ManifestClient.h
+++ b/src/Utils/SteamMetadata/ManifestClient.h
@@ -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();