Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 20 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,11 @@ The `extract_tickets` tool dumps the `AppTicket` and `ETicket` hex strings you n

> **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, falls back to the hardcoded default SteamID `76561198028121353`.
### 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.
Expand Down Expand Up @@ -116,9 +117,9 @@ setAppTicket(1361510,"0100000000000000...") -- write AppTicket to the credential

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, default SteamID 76561198028121353 is used.
```
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.).

Expand All @@ -139,13 +140,18 @@ url = "opensteamtool"

# HTTP timeouts for manifest requests (milliseconds)
timeout_resolve_ms = 5000
timeout_connect_ms = 5000
timeout_send_ms = 10000
timeout_recv_ms = 10000

# Additional Lua config directories (optional).
# Files are loaded after the default <Steam>/config/lua folder.
# The default folder is always loaded last so user files take priority.
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 <Steam>/config/lua folder.
# The default folder is always loaded last so user files take priority.
[lua]
paths = []

Expand Down
10 changes: 8 additions & 2 deletions README_ES.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ La herramienta `extract_tickets` vuelca las cadenas hexadecimales de `AppTicket`
### Estadísticas y logros
- Activa las estadísticas y los logros para los juegos que no poseas.
- Utiliza `setStat(appid, "steamid")` para configurar de qué SteamID se deben extraer los datos de los logros.
- Si no hay ningún `setStat` configurado para una aplicación, recurre por defecto al SteamID predefinido en el código `76561198028121353`.
- Si no hay ningún `setStat` configurado para una aplicación, OpenSteamTool consulta `https://stats.opensteamtool.com/{appid}` cuando `[stats] enable_api = true` (valor predeterminado).
- Prioridad: `setStat` > API de estadísticas cuando está habilitada y devuelve un valor válido > SteamID predefinido `76561198028121353`.

### Online Fix(Reparacion para habilitar el Online)
- Añade `-onlinefix` a los parámetros de lanzamiento de Steam para habilitar el juego en línea basado en el AppId 480 en juegos que utilizan emparejamiento (matchmaking) por salas (lobbies). La limitación actual es que solo se puede ejecutar uno de estos juegos a la vez. Para revertirlo, simplemente elimina -onlinefix de los parámetros de lanzamiento; el juego en línea volverá a la normalidad en el próximo inicio.
Expand Down Expand Up @@ -117,7 +118,7 @@ setAppTicket(1361510,"0100000000000000...") -- escribe AppTicket (REG_BINARY) en
setETicket(1361510,"0100000000000000...") -- escribe ETicket (REG_BINARY) en HKCU\Software\Valve\Steam\Apps\1361510\ETicket

setStat(1361510, "76561197960287930") -- utiliza los datos de logros del SteamID especificado para el appid 1361510
-- Si no se configura, se utilizará el SteamID por defecto 76561198028121353.
-- Si no se configura, se utiliza la API de estadísticas cuando está habilitada; de lo contrario se usa el SteamID por defecto 76561198028121353.
```

Los nombres de todas las funciones **no distinguen entre mayúsculas y minúsculas**. `setAppTicket`, `setappticket`, `SetAppticket`, `SETAPPTICKET`, etc., son todas equivalentes. Lo mismo se aplica a cada función registrada (`addAppId`, `AddToken`, `SETManifestid`, etc.).
Expand All @@ -142,6 +143,11 @@ timeout_connect_ms = 5000
timeout_send_ms = 10000
timeout_recv_ms = 10000

[stats]
# Consulta https://stats.opensteamtool.com/{appid} cuando no existe setStat en Lua.
# Prioridad: setStat > API de estadísticas > SteamID predefinido.
enable_api = true

# Directorios adicionales de configuración de Lua (opcional).
# Los archivos se cargan después de la carpeta predeterminada <Steam>/config/lua.
# La carpeta predeterminada siempre se carga al final para que los archivos del usuario tengan prioridad.
Expand Down
6 changes: 6 additions & 0 deletions opensteamtool.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ timeout_connect_ms = 5000
timeout_send_ms = 10000
timeout_recv_ms = 10000

[stats]
# Automatically query https://stats.opensteamtool.com/{appid} for a recommended
# SteamID when no Lua setStat(appid, "steamid") override exists.
# Priority: setStat > stats API when enabled and valid > hardcoded preset SteamID.
enable_api = true

# Additional Lua config directories (optional).
# Files are loaded after the default <Steam>/config/lua folder.
# The default folder is always loaded last so user files take priority.
Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ add_library(OpenSteamTool SHARED
Utils/SteamMetadata/ManifestClient.cpp
Utils/SteamMetadata/PatternLoader.cpp
Utils/SteamMetadata/RemoteToml.cpp
Utils/SteamMetadata/StatsClient.cpp
Utils/SteamMetadata/SteamDiagnostics.cpp

# Pipe subsystem: generic handshake registry + ProcessInspector core, plus
Expand Down
20 changes: 18 additions & 2 deletions src/Utils/Config/Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ namespace {
std::string logDir;
std::vector<std::string> luaPaths;
std::string remoteUrlTemplate;
bool statsEnableApi = true;
InjectionSettings injection;
};

Expand Down Expand Up @@ -50,6 +51,7 @@ namespace {
logDir = snapshot.logDir;
luaPaths = snapshot.luaPaths;
remoteUrlTemplate = snapshot.remoteUrlTemplate;
statsEnableApi = snapshot.statsEnableApi;
injectEnabled = snapshot.injection.enabled;
injectLibraryX86 = snapshot.injection.libraryX86;
injectLibraryX64 = snapshot.injection.libraryX64;
Expand Down Expand Up @@ -80,10 +82,11 @@ namespace {
LOG_INFO("Config file not found, using defaults");
ApplyManifestProvider(snapshot.manifestProvider);
LoadResult result = ApplySnapshotLocked(snapshot);
LOG_INFO("Config loaded: manifest.url={} log.level={} lua.paths={} remote.url_template={}",
LOG_INFO("Config loaded: manifest.url={} log.level={} lua.paths={} stats.enable_api={} remote.url_template={}",
ManifestClient::ActiveProviderName(),
ToString(GetLogLevel()),
(uint32_t)GetLuaPaths().size(),
GetStatsEnableApi(),
GetRemoteUrlTemplate().empty() ? "<default>" : GetRemoteUrlTemplate());
return result;
}
Expand Down Expand Up @@ -135,6 +138,13 @@ namespace {
}
}

// [stats]
if (auto stats = tbl["stats"].as_table()) {
if (auto val = (*stats)["enable_api"].value<bool>()) {
snapshot.statsEnableApi = *val;
}
}

// [inject]
if (auto inject = tbl["inject"].as_table()) {
if (auto val = (*inject)["enabled"].value<bool>())
Expand All @@ -147,10 +157,11 @@ namespace {

ApplyManifestProvider(snapshot.manifestProvider);
LoadResult result = ApplySnapshotLocked(snapshot);
LOG_INFO("Config loaded: manifest.url={} log.level={} lua.paths={} remote.url_template={}",
LOG_INFO("Config loaded: manifest.url={} log.level={} lua.paths={} stats.enable_api={} remote.url_template={}",
ManifestClient::ActiveProviderName(),
ToString(snapshot.logLevel),
(uint32_t)snapshot.luaPaths.size(),
snapshot.statsEnableApi,
snapshot.remoteUrlTemplate.empty() ? "<default>" : snapshot.remoteUrlTemplate);
return result;

Expand Down Expand Up @@ -214,4 +225,9 @@ namespace {
};
}

bool GetStatsEnableApi() {
std::lock_guard lock(g_mutex);
return statsEnableApi;
}

}
10 changes: 7 additions & 3 deletions src/Utils/Config/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ namespace Config {
std::vector<std::string> GetLuaPaths();
std::string GetRemoteUrlTemplate();
InjectionSettings GetInjectionSettings();
bool GetStatsEnableApi();

// [manifest] — provider selection lives in ManifestClient (table-driven).
inline uint32_t manifestTimeoutResolve = 5000;
Expand All @@ -50,9 +51,12 @@ namespace Config {
// [lua]
inline std::vector<std::string> luaPaths;

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

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

// [stats]
inline bool statsEnableApi = true;

// [inject] - optional library injection into game processes.
inline bool injectEnabled = false;
inline std::string injectLibraryX86;
Expand Down
4 changes: 4 additions & 0 deletions src/Utils/Config/LuaConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "OSTPlatform/include/Http.h"
#include "OSTPlatform/include/Numbers.h"
#include "Utils/Config/LuaConfig.h"
#include "Utils/SteamMetadata/StatsClient.h"
#include "Utils/Tickets/AppTicket.h"

#include <lua.hpp>
Expand Down Expand Up @@ -511,6 +512,9 @@ namespace LuaConfig{
uint64_t GetStatSteamId(AppId_t AppId) {
if (StatSteamIdSet.count(AppId))
return StatSteamIdSet[AppId];
uint64_t apiSteamId = 0;
if (StatsClient::FetchStatSteamId(AppId, &apiSteamId))
return apiSteamId;
return kDefaultStatSteamId;
}

Expand Down
74 changes: 74 additions & 0 deletions src/Utils/SteamMetadata/StatsClient.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#include "StatsClient.h"

#include "OSTPlatform/include/Http.h"
#include "OSTPlatform/include/Numbers.h"
#include "Utils/Config/Config.h"
#include "Utils/Logging/Log.h"

#include <cctype>
#include <cstdio>
#include <mutex>
#include <string_view>
#include <unordered_map>

namespace StatsClient {
namespace {

std::mutex g_mutex;
std::unordered_map<AppId_t, uint64_t> g_cache;

bool ParseSteamId(std::string_view body, uint64_t* outSteamId) {
const auto parsed = OSTPlatform::Numbers::ParseUInt64(body);
if (!parsed || *parsed == 0) return false;
*outSteamId = *parsed;
return true;
}

} // namespace

bool FetchStatSteamId(AppId_t appId, uint64_t* outSteamId) {
if (!outSteamId || appId == k_uAppIdInvalid) return false;

if (!Config::GetStatsEnableApi()) {
LOG_ACHIEVEMENT_DEBUG("Stats SteamID API disabled for appid={}", appId);
return false;
}

{
std::lock_guard<std::mutex> lock(g_mutex);
auto it = g_cache.find(appId);
if (it != g_cache.end()) {
*outSteamId = it->second;
LOG_ACHIEVEMENT_DEBUG("Stats SteamID API cache hit appid={} steamid={}", appId, *outSteamId);
return true;
}
}

char url[128];
std::snprintf(url, sizeof(url), "https://stats.opensteamtool.com/%u", appId);

auto r = OSTPlatform::Http::Execute(L"GET", url);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid blocking stats packet sends on the API call

When an unlocked app has no setStat override and the stats API is enabled by default, this synchronous WinHTTP request runs inside LuaConfig::GetStatSteamId, which is called from the outgoing user-stats packet handlers before forwarding the packet. If stats.opensteamtool.com is slow or unreachable, each uncached app can stall Steam/game networking until the HTTP timeouts expire; failures are not cached, so the stall repeats on later stats requests. Consider resolving this asynchronously, prefetching, or caching failures/using much shorter configurable timeouts.

Useful? React with 👍 / 👎.

LOG_ACHIEVEMENT_INFO("Stats SteamID API status={} appid={}", r.status, appId);

if (!r.ok || r.status != 200) {
LOG_ACHIEVEMENT_WARN("Stats SteamID API failed appid={} status={} ok={}", appId, r.status, r.ok);
return false;
}

uint64_t steamId = 0;
if (!ParseSteamId(r.body, &steamId)) {
LOG_ACHIEVEMENT_WARN("Stats SteamID API returned invalid body appid={} bytes={}", appId, r.body.size());
return false;
}

{
std::lock_guard<std::mutex> lock(g_mutex);
g_cache[appId] = steamId;
}

*outSteamId = steamId;
LOG_ACHIEVEMENT_INFO("Stats SteamID API resolved appid={} steamid={}", appId, steamId);
return true;
}

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

#include "Steam/Types.h"

#include <cstdint>

namespace StatsClient {

bool FetchStatSteamId(AppId_t appId, uint64_t* outSteamId);

} // namespace StatsClient
Loading