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 @@ -
- OpenSteamTool logo - -

OpenSteamTool

- -

- Open-Source Steam Unlock Tool -

- -

- C++ 20+ - CMake 3.20+ - Windows only - - Ask DeepWiki - -

- -

- - United States flag - English - -  |  - - Spain flag - Español - -  |  - - China flag - 中文 - -

-
- -## 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. +
+ OpenSteamTool logo + +

OpenSteamTool

+ +

+ Open-Source Steam Unlock Tool +

+ +

+ C++ 20+ + CMake 3.20+ + Windows only + + Ask DeepWiki + +

+ +

+ + United States flag + English + +  |  + + Spain flag + Español + +  |  + + China flag + 中文 + +

+
+ +## 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();