18 Commits

Author SHA1 Message Date
acidicoala
8ee2d77115 Fixed build errors 2025-09-07 02:36:14 +05:00
acidicoala
4c08816eb6 Reworked late hooking 2025-09-07 02:02:59 +05:00
acidicoala
6b4b7610f4 Fix proxy mode 2 2025-09-05 18:05:30 +05:00
acidicoala
61ff1df065 Fix proxy mode 2025-09-04 23:49:54 +05:00
acidicoala
141a0bcc58 Version bump 2025-09-04 23:05:53 +05:00
acidicoala
bdab9b574f Moved get_app_id() 2025-09-04 19:01:16 +05:00
acidicoala
4581c36913 Deactivated steam_api exports 2025-09-04 18:53:05 +05:00
acidicoala
8ec9b4e374 Added README.txt to release zip 2025-09-01 00:16:22 +05:00
acidicoala
c376b793c1 Added init functions and hook fallback 2025-08-31 03:56:27 +05:00
acidicoala
eac7e87880 Fixed inventory regression 2025-08-29 01:25:24 +05:00
acidicoala
09e1187ab0 Reworked hooking 2025-08-28 22:25:11 +05:00
acidicoala
6432eb3ec9 Fix CI 2025-08-27 23:49:29 +05:00
acidicoala
ec5778d452 Added config logging 2025-08-27 23:38:56 +05:00
acidicoala
1174dcb57a Fixed CI 2025-08-27 21:13:42 +05:00
acidicoala
7685628b6f Updated CI 2025-08-27 21:02:52 +05:00
acidicoala
1e79ce103b Updated CI 2025-08-27 21:00:11 +05:00
acidicoala
49a8a76b28 Updated CI 2025-08-27 20:56:02 +05:00
acidicoala
dfbd7d00d9 Added ISteamHTTP 2025-08-27 20:42:08 +05:00
44 changed files with 1266 additions and 1289 deletions

View File

@@ -4,16 +4,15 @@ on: push
jobs: jobs:
ci: ci:
name: CI name: CI
uses: acidicoala/KoalaBox/.github/workflows/build-and-package.yml@a053502132e51e936820f27b7c99316fdb62b3e4 uses: acidicoala/KoalaBox/.github/workflows/build-and-package.yml@1bdfeefa9933092a747c46679b3d872470ef4998
permissions: permissions:
contents: write contents: write
with: with:
modules: >- arch: '[ 32, 64 ]'
["SmokeAPI"] config: Release
modules: '[ "SmokeAPI" ]'
zip_command: > zip_command: >
zip -j $ZIP_NAME zip -j $ZIP_NAME
artifacts/*/*.dll artifacts/*/*.dll
res/SmokeAPI.config.json res/SmokeAPI.config.json
res/README.txt
config: Release

View File

@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.24) cmake_minimum_required(VERSION 3.24)
project(SmokeAPI VERSION 3.0.0) project(SmokeAPI VERSION 3.1.1)
include(KoalaBox/cmake/KoalaBox.cmake) include(KoalaBox/cmake/KoalaBox.cmake)
@@ -9,13 +9,16 @@ add_subdirectory(tools)
set_32_and_64(STEAMAPI_DLL steam_api) set_32_and_64(STEAMAPI_DLL steam_api)
set_32_and_64(STEAMCLIENT_DLL steamclient) set_32_and_64(STEAMCLIENT_DLL steamclient)
set_32_and_64(STEAM_API_DLL steam_api.dll steam_api64.dll) set_32_and_64(STEAM_API_DLL steam_api steam_api64)
set_32_and_64(SMOKEAPI_DLL SmokeAPI32 SmokeAPI64)
configure_build_config(extra_build_config) configure_build_config(extra_build_config)
set(SMOKE_API_STATIC_SOURCES set(SMOKE_API_STATIC_SOURCES
static/smoke_api/interfaces/steam_apps.hpp static/smoke_api/interfaces/steam_apps.hpp
static/smoke_api/interfaces/steam_apps.cpp static/smoke_api/interfaces/steam_apps.cpp
static/smoke_api/interfaces/steam_http.hpp
static/smoke_api/interfaces/steam_http.cpp
static/smoke_api/interfaces/steam_inventory.hpp static/smoke_api/interfaces/steam_inventory.hpp
static/smoke_api/interfaces/steam_inventory.cpp static/smoke_api/interfaces/steam_inventory.cpp
static/smoke_api/interfaces/steam_user.hpp static/smoke_api/interfaces/steam_user.hpp
@@ -33,55 +36,55 @@ set(SMOKE_API_STATIC_SOURCES
set(SMOKE_API_SOURCES set(SMOKE_API_SOURCES
${SMOKE_API_STATIC_SOURCES} ${SMOKE_API_STATIC_SOURCES}
src/steam_api/exports/steam_api.cpp src/smoke_api/smoke_api.cpp
src/steam_api/exports/steam_api.hpp src/smoke_api/smoke_api.hpp
src/steam_api/exports/steam_api_flat.cpp
src/steam_api/exports/steam_api_internal.cpp
src/steam_api/exports/steam_api_unversioned.cpp
src/steam_api/virtuals/isteamapps.cpp src/steam_api/virtuals/isteamapps.cpp
src/steam_api/virtuals/isteamclient.cpp src/steam_api/virtuals/isteamclient.cpp
src/steam_api/virtuals/isteamgameserver.cpp src/steam_api/virtuals/isteamgameserver.cpp
src/steam_api/virtuals/isteamhttp.cpp
src/steam_api/virtuals/isteaminventory.cpp src/steam_api/virtuals/isteaminventory.cpp
src/steam_api/virtuals/isteamuser.cpp src/steam_api/virtuals/isteamuser.cpp
src/steam_api/virtuals/steam_api_virtuals.hpp src/steam_api/virtuals/steam_api_virtuals.hpp
src/steam_api/steam_client.hpp src/steam_api/steam_client.hpp
src/steam_api/steam_client.cpp src/steam_api/steam_client.cpp
src/steam_api/steam_interface.cpp src/steam_api/steam_interfaces.cpp
src/steam_api/steam_interface.hpp src/steam_api/steam_interfaces.hpp
src/steamclient.cpp src/steamclient/steamclient.cpp
src/main.cpp src/main.cpp
src/smoke_api.cpp
src/smoke_api.hpp
) )
### SmokeAPI interface ### SmokeAPI interface
add_library(SmokeAPI_interface INTERFACE) add_library(SmokeAPI_common INTERFACE)
add_library(SmokeAPI::common ALIAS SmokeAPI_common)
target_include_directories(SmokeAPI_interface INTERFACE target_include_directories(SmokeAPI_common INTERFACE
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/static>" "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/static>"
) )
target_link_libraries(SmokeAPI_interface INTERFACE KoalaBox $<TARGET_OBJECTS:KoalaBox>) target_link_libraries(SmokeAPI_common INTERFACE KoalaBox $<TARGET_OBJECTS:KoalaBox>)
### Static SmokeAPI ### Static SmokeAPI
add_library(SmokeAPI_static STATIC ${SMOKE_API_STATIC_SOURCES}) add_library(SmokeAPI_static STATIC ${SMOKE_API_STATIC_SOURCES})
target_link_libraries(SmokeAPI_static PUBLIC SmokeAPI_interface) add_library(SmokeAPI::static ALIAS SmokeAPI_static)
target_link_libraries(SmokeAPI_static PUBLIC SmokeAPI::common)
### Shared SmokeAPI ### Shared SmokeAPI
add_library(SmokeAPI SHARED ${SMOKE_API_SOURCES}) add_library(SmokeAPI SHARED ${SMOKE_API_SOURCES})
target_link_libraries(SmokeAPI PUBLIC SmokeAPI_interface) target_link_libraries(SmokeAPI PUBLIC SmokeAPI::common)
set_target_properties(SmokeAPI PROPERTIES RUNTIME_OUTPUT_NAME ${STEAMAPI_DLL}) set_target_properties(SmokeAPI PROPERTIES RUNTIME_OUTPUT_NAME ${SMOKEAPI_DLL})
configure_version_resource( configure_version_resource(
TARGET SmokeAPI TARGET SmokeAPI
FILE_DESC "Steamworks DLC unlocker" FILE_DESC "Steamworks DLC unlocker"
ORIG_NAME ${STEAMAPI_DLL} ORIG_NAME ${SMOKEAPI_DLL}
) )
target_include_directories(SmokeAPI PRIVATE target_include_directories(SmokeAPI PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src" "${CMAKE_CURRENT_SOURCE_DIR}/src"
"${CMAKE_CURRENT_BINARY_DIR}") "${CMAKE_CURRENT_BINARY_DIR}"
)
## https://github.com/batterycenter/embed ## https://github.com/batterycenter/embed
CPMAddPackage( CPMAddPackage(
@@ -94,8 +97,8 @@ configure_linker_exports(
TARGET SmokeAPI TARGET SmokeAPI
HEADER_NAME "linker_exports_for_steam_api" HEADER_NAME "linker_exports_for_steam_api"
FORWARDED_DLL "${STEAMAPI_DLL}_o" FORWARDED_DLL "${STEAMAPI_DLL}_o"
INPUT_SOURCES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/steam_api/exports" INPUT_SOURCES_DIR ""
DLL_FILES_GLOB "${CMAKE_CURRENT_SOURCE_DIR}/res/steamworks/*/binaries/${STEAM_API_DLL}" DLL_FILES_GLOB "${CMAKE_CURRENT_SOURCE_DIR}/res/steamworks/*/binaries/${STEAM_API_DLL}.dll"
) )
configure_linker_exports( configure_linker_exports(

View File

@@ -64,7 +64,7 @@ SmokeAPI supports 2 installation modes: hook mode and proxy mode.
|Mode |Advantages |Disadvantages |Mode |Advantages |Disadvantages
|🪝 Hook mode |🪝 Hook mode
|Persists after game updates |Persists after game updates; Can be loaded by other injectors.
|Might need an additional DLL (Koaloader) |Might need an additional DLL (Koaloader)
|🔀 Proxy mode |🔀 Proxy mode
@@ -79,9 +79,14 @@ If it doesn't work, try installing it in proxy mode.
=== 🪝 Hook mode === 🪝 Hook mode
. Download the latest SmokeAPI release zip from {smokeapi_release}. . Download the latest SmokeAPI release zip from {smokeapi_release}.
. From SmokeAPI archive unpack `steam_api.dll` or `steam_api64.dll`, depending on the game bitness, rename it to `version.dll`, and place it next to the game exe file. . From SmokeAPI archive unpack `SmokeAPI32.dll` / `SmokeAPI64.dll`, depending on the game bitness
. Rename the unpacked DLL to `version.dll`.
. Place `version.dll` next to the game's `.exe` file.
=== 🪝 Hook mode (Alternative) === 🪝 Hook mode (Alternative installation)
:special_k: https://www.special-k.info[Special K]
:custom_plugin: https://wiki.special-k.info/en/SpecialK/Tools#custom-plugin[custom plugin]
If a game doesn't load `version.dll`, you can use one of the {koaloader} DLLs that the game does in fact load. If a game doesn't load `version.dll`, you can use one of the {koaloader} DLLs that the game does in fact load.
For example, assuming that the game loads `winmm.dll`: For example, assuming that the game loads `winmm.dll`:
@@ -89,20 +94,31 @@ For example, assuming that the game loads `winmm.dll`:
. Download the latest Koaloader release zip from https://github.com/acidicoala/Koaloader/releases/latest[Koaloader Releases]. . Download the latest Koaloader release zip from https://github.com/acidicoala/Koaloader/releases/latest[Koaloader Releases].
. From Koaloader archive unpack `winmm.dll` from `winmm-32` or `winmm-64`, depending on the game bitness, and place it next to the game exe file. . From Koaloader archive unpack `winmm.dll` from `winmm-32` or `winmm-64`, depending on the game bitness, and place it next to the game exe file.
. Download the latest SmokeAPI release zip from {smokeapi_release}. . Download the latest SmokeAPI release zip from {smokeapi_release}.
. From SmokeAPI archive unpack `steam_api.dll` or `steam_api64.dll`, depending on the game bitness, rename it to `SmokeAPI.dll`, and place it next to the game exe file. . From SmokeAPI archive unpack `SmokeAPI32.dll` / `SmokeAPI64.dll`, depending on the game bitness.
. Place the unpacked DLL next to the game's `exe` file.
[[special_k_note]]
IMPORTANT: There are games which have extra protections that break hook mode.
In such cases, it might be worth trying {special_k}, which can inject SmokeAPI as a {custom_plugin}.
==== 🔀 Proxy mode ==== 🔀 Proxy mode
. Find `steam_api.dll` / `steam_api64.dll` file in game directory, and rename it to `steam_api_o.dll` / `steam_api64_o.dll`. . Find `steam_api.dll` / `steam_api64.dll` file in game directory, and rename it to `steam_api_o.dll` / `steam_api64_o.dll`.
. Download the latest SmokeAPI release zip from {smokeapi_release}. . Download the latest SmokeAPI release zip from {smokeapi_release}.
. From SmokeAPI archive unpack `steam_api.dll`/`steam_api64.dll`, depending on the game bitness, and place it next to the original steam_api DLL file. . From SmokeAPI archive unpack `SmokeAPI32.dll` / `SmokeAPI64.dll`, depending on the game bitness.
. Rename the unpacked DLL to `steam_api.dll` / `steam_api64.dll` and place it next to the `steam_api_o.dll` / `steam_api64_o.dll` file.
IMPORTANT: There are games which have extra protections that break proxy mode.
In such cases, see the note on <<special_k_note, Hook mode with Special K>>
'''
If the unlocker is not working as expected, then please fully read the https://gist.github.com/acidicoala/2c131cb90e251f97c0c1dbeaf2c174dc[Generic Unlocker Installation Instructions] before seeking support in the {forum-topic}. If the unlocker is not working as expected, then please fully read the https://gist.github.com/acidicoala/2c131cb90e251f97c0c1dbeaf2c174dc[Generic Unlocker Installation Instructions] before seeking support in the {forum-topic}.
== ⚙ Configuration == ⚙ Configuration
NOTE: This document describes configuration for version 3 of SmokeAPI. NOTE: This document describes configuration for version 4 of SmokeAPI.
You can find the version 2 documentation https://github.com/acidicoala/SmokeAPI/blob/v2.0.5/README.md#-configuration[here]. You can find the version 3 documentation https://github.com/acidicoala/SmokeAPI/blob/v3.0.0/README.adoc#-configuration[here].
:fn-app-id: footnote:fn-app-id[App/DLC IDs can be obtained from https://steamdb.info[SteamDB] or https://steambase.io[Steambase]. Keep in mind that you need to be signed in with a steam account in order to see accurate inventory item IDs on that website.] :fn-app-id: footnote:fn-app-id[App/DLC IDs can be obtained from https://steamdb.info[SteamDB] or https://steambase.io[Steambase]. Keep in mind that you need to be signed in with a steam account in order to see accurate inventory item IDs on that website.]
@@ -114,6 +130,7 @@ To use it, simply place it next to the SmokeAPI DLL.
It will be read upon each launch of the game. It will be read upon each launch of the game.
In the absence of the config file, default values specified below will be used. In the absence of the config file, default values specified below will be used.
The configuration file is expected to conform to the JSON standard. The configuration file is expected to conform to the JSON standard.
All options within the config file are optional.
`logging`:: Toggles generation of a `SmokeAPI.log.log` file. `logging`:: Toggles generation of a `SmokeAPI.log.log` file.
+ +
@@ -121,6 +138,12 @@ The configuration file is expected to conform to the JSON standard.
Type::: Boolean Type::: Boolean
Default::: `false` Default::: `false`
`log_steam_http`:: Toggles logging of _SteamHTTP_ traffic.
+
[horizontal]
Type::: Boolean
Default::: `false`
`default_app_status`:: This option sets the default DLC unlocking behaviour. `default_app_status`:: This option sets the default DLC unlocking behaviour.
+ +
[horizontal] [horizontal]
@@ -184,8 +207,9 @@ Default::: `{}`
[source,json] [source,json]
---- ----
{ {
"$version": 3, "$version": 4,
"logging": true, "logging": true,
"log_steam_http": true,
"default_app_status": "unlocked", "default_app_status": "unlocked",
"override_app_status": { "override_app_status": {
"1234": "original", "1234": "original",
@@ -223,7 +247,7 @@ Some games that have a large number of DLCs begin ownership verification by quer
Once the game receives the list, it will go over each item and check the ownership. Once the game receives the list, it will go over each item and check the ownership.
The issue arises from the fact that response from Steamworks SDK may max out at 64, depending on how much unowned DLCs the user has. The issue arises from the fact that response from Steamworks SDK may max out at 64, depending on how much unowned DLCs the user has.
To alleviate this issue, SmokeAPI will make a web request to Steam API for a full list of DLCs, which works well most of the time. To alleviate this issue, SmokeAPI will make a web request to Steam API for a full list of DLCs, which works well most of the time.
Unfortunately, even the web API does not solve all of our problems, because it will only return DLCs that are available in Steam store. Unfortunately, even the web API does not solve all of our problems, because it will return only DLCs that are available in Steam store.
This means that DLCs without a dedicated store offer, such as pre-order DLCs will be left out. This means that DLCs without a dedicated store offer, such as pre-order DLCs will be left out.
That's where the `extra_dlcs` config option comes into play. That's where the `extra_dlcs` config option comes into play.
You can specify those missing DLC IDs there, and SmokeAPI will make them available to the game. You can specify those missing DLC IDs there, and SmokeAPI will make them available to the game.

12
res/README.txt Normal file
View File

@@ -0,0 +1,12 @@
Project page: https://github.com/acidicoala/SmokeAPI#readme
Forum topic: https://cs.rin.ru/forum/viewtopic.php?p=2597932#p2597932
DLC Database: https://steamdb.info/
### NOTE ###
Do NOT use the SmokeAPI.config.json file unless you have a good reason.
The default config file enables logging, which might have a negative impact on performance in games.
So, unless you really need to see logs for debugging issues, it is advised to either:
disable logging in the config file by setting the "logging" field to false
or
not use the SmokeAPI.config.json file at all

View File

@@ -1,7 +1,8 @@
{ {
"$schema": "https://raw.githubusercontent.com/acidicoala/SmokeAPI/refs/tags/v3.0.0/res/SmokeAPI.schema.json", "$schema": "https://raw.githubusercontent.com/acidicoala/SmokeAPI/refs/tags/v3.1.0/res/SmokeAPI.schema.json",
"$version": 3, "$version": 4,
"logging": true, "logging": true,
"log_steam_http": true,
"default_app_status": "unlocked", "default_app_status": "unlocked",
"override_app_status": {}, "override_app_status": {},
"override_dlc_status": {}, "override_dlc_status": {},

View File

@@ -11,8 +11,7 @@
}, },
"$version": { "$version": {
"type": "integer", "type": "integer",
"minimum": 0, "const": 4,
"default": 3,
"description": "A technical field reserved for tools like GUI config editors. Do not modify this value." "description": "A technical field reserved for tools like GUI config editors. Do not modify this value."
}, },
"logging": { "logging": {
@@ -20,6 +19,11 @@
"default": false, "default": false,
"description": "Toggles generation of a SmokeAPI.log.log file." "description": "Toggles generation of a SmokeAPI.log.log file."
}, },
"log_steam_http": {
"type": "boolean",
"default": false,
"description": "Toggles logging of SteamHTTP traffic"
},
"default_app_status": { "default_app_status": {
"description": "Sets the default DLC unlocking behaviour.", "description": "Sets the default DLC unlocking behaviour.",
"default": "unlocked", "default": "unlocked",
@@ -111,9 +115,10 @@
}, },
"examples": [ "examples": [
{ {
"$schema": "https://raw.githubusercontent.com/acidicoala/SmokeAPI/refs/heads/master/res/SmokeAPI.schema.json", "$schema": "https://raw.githubusercontent.com/acidicoala/SmokeAPI/refs/tags/v3.1.0/res/SmokeAPI.schema.json",
"$version": 3, "$version": 4,
"logging": true, "logging": true,
"log_steam_http": true,
"default_app_status": "unlocked", "default_app_status": "unlocked",
"override_app_status": { "override_app_status": {
"1234": "original", "1234": "original",

View File

@@ -1,4 +1,4 @@
#include "smoke_api.hpp" #include "smoke_api/smoke_api.hpp"
// This header will be populated at build time // This header will be populated at build time
#include "linker_exports_for_steam_api.h" #include "linker_exports_for_steam_api.h"

View File

@@ -1,119 +0,0 @@
#include <koalabox/config.hpp>
#include <koalabox/dll_monitor.hpp>
#include <koalabox/globals.hpp>
#include <koalabox/hook.hpp>
#include <koalabox/loader.hpp>
#include <koalabox/logger.hpp>
#include <koalabox/path.hpp>
#include <koalabox/paths.hpp>
#include <koalabox/str.hpp>
#include <koalabox/util.hpp>
#include <koalabox/win.hpp>
#include "build_config.h"
#include "smoke_api.hpp"
#include "smoke_api/config.hpp"
#include "smoke_api/steamclient/steamclient.hpp"
#include "steam_api/exports/steam_api.hpp"
// Hooking steam_api has shown itself to be less desirable than steamclient
// for the reasons outlined below:
//
// Calling original in flat functions will actually call the hooked functions
// because the original function redirects the execution to a function taken
// from self pointer, which would have been hooked by SteamInternal_*Interface
// functions.
//
// Furthermore, turns out that many flat functions share the same body,
// which looks like the following snippet:
//
// mov rax, qword ptr ds:[rcx]
// jmp qword ptr ds:[rax+immediate]
//
// This means that we end up inadvertently hooking unintended functions.
// Given that hooking steam_api has no apparent benefits, but has inherent flaws,
// the support for it has been dropped from this project.
namespace {
namespace kb = koalabox;
void init_proxy_mode() {
LOG_INFO("Detected proxy mode");
const auto self_path = kb::paths::get_self_dir();
smoke_api::steamapi_module = kb::loader::load_original_library(self_path, STEAMAPI_DLL);
}
void init_hook_mode() {
LOG_INFO("Detected hook mode");
kb::dll_monitor::init_listener(
{STEAMCLIENT_DLL, STEAMAPI_DLL},
[&](const HMODULE& module_handle, const std::string& library_name) {
if(kb::str::eq(library_name, STEAMCLIENT_DLL)) {
KB_HOOK_DETOUR_MODULE(CreateInterface, module_handle);
} else if(kb::str::eq(library_name, STEAMAPI_DLL)) {
KB_HOOK_DETOUR_MODULE(SteamAPI_RestartAppIfNecessary, module_handle);
KB_HOOK_DETOUR_MODULE(SteamAPI_Shutdown, module_handle);
}
}
);
}
}
namespace smoke_api {
HMODULE steamapi_module = nullptr;
bool hook_mode = false;
void init(const HMODULE module_handle) {
try {
kb::globals::init_globals(module_handle, PROJECT_NAME);
config::instance = kb::config::parse<config::Config>();
if(config::instance.logging) {
kb::logger::init_file_logger(kb::paths::get_log_path());
}
// This kind of timestamp is reliable only for CI builds, as it will reflect the
// compilation time stamp only when this file gets recompiled.
LOG_INFO("{} v{} | Compiled at '{}'", PROJECT_NAME, PROJECT_VERSION, __TIMESTAMP__);
const auto exe_path = kb::win::get_module_path(nullptr);
const auto exe_name = kb::path::to_str(exe_path.filename());
LOG_DEBUG("Process name: '{}' [{}-bit]", exe_name, kb::util::BITNESS);
// We need to hook functions in either mode
kb::hook::init(true);
if(kb::hook::is_hook_mode(module_handle, STEAMAPI_DLL)) {
hook_mode = true;
init_hook_mode();
} else {
init_proxy_mode();
}
LOG_INFO("Initialization complete");
} catch(const std::exception& ex) {
kb::util::panic(fmt::format("Initialization error: {}", ex.what()));
}
}
void shutdown() {
try {
if(steamapi_module != nullptr) {
kb::win::free_library(steamapi_module);
steamapi_module = nullptr;
}
LOG_INFO("Shutdown complete");
} catch(const std::exception& e) {
const auto msg = std::format("Shutdown error: {}", e.what());
LOG_ERROR(msg);
}
kb::logger::shutdown();
}
}

View File

@@ -1,38 +0,0 @@
#pragma once
#include <koalabox/hook.hpp>
constexpr auto STEAM_APPS = "STEAMAPPS_INTERFACE_VERSION";
constexpr auto STEAM_CLIENT = "SteamClient";
constexpr auto STEAM_USER = "SteamUser";
constexpr auto STEAM_INVENTORY = "STEAMINVENTORY_INTERFACE_V";
constexpr auto STEAM_GAME_SERVER = "SteamGameServer";
// IMPORTANT: DLL_EXPORT is hardcoded in exports_generator.cpp,
// so any name changes here must be reflected there as well.
#define DLL_EXPORT(TYPE) extern "C" [[maybe_unused]] __declspec(dllexport) TYPE __cdecl
// These macros are meant to be used for callbacks that should return original result
#define MODULE_CALL(FUNC, ...) \
static const auto _##FUNC = KB_HOOK_GET_MODULE_FN(smoke_api::steamapi_module, FUNC); \
return _##FUNC(__VA_ARGS__)
#define MODULE_CALL_CLOSURE(FUNC, ...) \
[&] { MODULE_CALL(FUNC, __VA_ARGS__); }
#define AUTO_CALL(FUNC, ...) \
static const auto _##FUNC = smoke_api::hook_mode \
? KB_HOOK_GET_HOOKED_FN(FUNC) \
: KB_HOOK_GET_MODULE_FN(smoke_api::steamapi_module, FUNC); \
return _##FUNC(__VA_ARGS__)
namespace smoke_api {
extern HMODULE steamapi_module;
extern bool hook_mode;
void init(HMODULE module_handle);
void shutdown();
}

168
src/smoke_api/smoke_api.cpp Normal file
View File

@@ -0,0 +1,168 @@
#include <regex>
#include <set>
#include <koalabox/config.hpp>
#include <koalabox/dll_monitor.hpp>
#include <koalabox/globals.hpp>
#include <koalabox/hook.hpp>
#include <koalabox/loader.hpp>
#include <koalabox/logger.hpp>
#include <koalabox/path.hpp>
#include <koalabox/paths.hpp>
#include <koalabox/util.hpp>
#include <koalabox/win.hpp>
#include "smoke_api.hpp"
#include "smoke_api/config.hpp"
#include "smoke_api/steamclient/steamclient.hpp"
#include "steam_api/steam_interfaces.hpp"
#include "build_config.h"
// Hooking steam_api has shown itself to be less desirable than steamclient
// for the reasons outlined below:
//
// Calling original in flat functions will actually call the hooked functions
// because the original function redirects the execution to a function taken
// from self pointer, which would have been hooked by SteamInternal_*Interface
// functions.
//
// Furthermore, turns out that many flat functions share the same body,
// which looks like the following snippet:
//
// mov rax, qword ptr ds:[rcx]
// jmp qword ptr ds:[rax+immediate]
//
// This means that we end up inadvertently hooking unintended functions.
// Given that hooking steam_api has no apparent benefits, but has inherent flaws,
// the support for it has been dropped from this project.
namespace {
namespace kb = koalabox;
HMODULE original_steamapi_handle = nullptr;
std::set<std::string> find_steamclient_versions(const HMODULE steamapi_handle) {
std::set<std::string> versions;
const auto rdata = kb::win::get_pe_section_or_throw(steamapi_handle, ".rdata").to_string();
const std::regex pattern(R"(SteamClient\d{3})");
auto matches_begin = std::sregex_iterator(rdata.begin(), rdata.end(), pattern);
auto matches_end = std::sregex_iterator();
for(std::sregex_iterator i = matches_begin; i != matches_end; ++i) {
versions.insert(i->str());
}
return versions;
}
// ReSharper disable once CppDFAConstantFunctionResult
bool on_steamclient_loaded(const HMODULE steamclient_handle) noexcept {
auto* const steamapi_handle = original_steamapi_handle
? original_steamapi_handle
: GetModuleHandle(TEXT(STEAMAPI_DLL));
if(!steamapi_handle) {
LOG_ERROR("{} -> {} is not loaded", __func__, STEAMAPI_DLL);
return true;
}
static const auto CreateInterface$ = KB_WIN_GET_PROC(steamclient_handle, CreateInterface);
const auto steamclient_versions = find_steamclient_versions(steamapi_handle);
for(const auto& steamclient_version : steamclient_versions) {
if(CreateInterface$(steamclient_version.c_str(), nullptr)) {
LOG_WARN("'{}' was already initialized. SmokeAPI might not work as expected.", steamclient_version);
LOG_WARN("Probable cause: SmokeAPI was injected too late. If possible, try injecting it earlier.");
steam_interfaces::hook_steamclient_interface(steamclient_handle, steamclient_version);
} else {
LOG_INFO("'{}' is not initialized. Waiting for initialization.", steamclient_version);
}
}
KB_HOOK_DETOUR_MODULE(CreateInterface, steamclient_handle);
return true;
}
void start_dll_listener() {
kb::dll_monitor::init_listener({{STEAMCLIENT_DLL, on_steamclient_loaded}});
}
}
namespace smoke_api {
void init(const HMODULE module_handle) {
try {
kb::globals::init_globals(module_handle, PROJECT_NAME);
config::instance = kb::config::parse<config::Config>();
if(config::instance.logging) {
kb::logger::init_file_logger(kb::paths::get_log_path());
}
LOG_INFO("{} v{} | Built at '{}'", PROJECT_NAME, PROJECT_VERSION, __TIMESTAMP__);
LOG_DEBUG("Parsed config:\n{}", nlohmann::ordered_json(config::instance).dump(2));
const auto exe_path = kb::win::get_module_path(nullptr);
const auto exe_name = kb::path::to_str(exe_path.filename());
LOG_DEBUG("Process name: '{}' [{}-bit]", exe_name, kb::util::BITNESS);
// We need to hook functions in either mode
kb::hook::init(true);
if(kb::hook::is_hook_mode(module_handle, STEAMAPI_DLL)) {
LOG_INFO("Detected hook mode");
start_dll_listener();
} else {
LOG_INFO("Detected proxy mode");
const auto self_path = kb::paths::get_self_dir();
original_steamapi_handle = kb::loader::load_original_library(
self_path,
STEAMAPI_DLL
);
start_dll_listener();
}
LOG_INFO("Initialization complete");
} catch(const std::exception& e) {
kb::util::panic(fmt::format("Initialization error: {}", e.what()));
}
}
void shutdown() {
try {
if(original_steamapi_handle != nullptr) {
kb::win::free_library(original_steamapi_handle);
original_steamapi_handle = nullptr;
}
// TODO: Unhook everything
LOG_INFO("Shutdown complete");
} catch(const std::exception& e) {
const auto msg = std::format("Shutdown error: {}", e.what());
LOG_ERROR(msg);
}
kb::logger::shutdown();
}
AppId_t get_app_id() {
try {
const auto app_id_str = kb::win::get_env_var("SteamAppId");
static auto app_id = std::stoi(app_id_str);
return app_id;
} catch(const std::exception& e) {
LOG_ERROR("Failed to get app id: {}", e.what());
return 0;
}
}
}

View File

@@ -0,0 +1,22 @@
#pragma once
#include "smoke_api/types.hpp"
constexpr auto STEAM_APPS = "STEAMAPPS_INTERFACE_VERSION";
constexpr auto STEAM_CLIENT = "SteamClient";
constexpr auto STEAM_HTTP = "STEAMHTTP_INTERFACE_VERSION";
constexpr auto STEAM_USER = "SteamUser";
constexpr auto STEAM_INVENTORY = "STEAMINVENTORY_INTERFACE_V";
constexpr auto STEAM_GAME_SERVER = "SteamGameServer";
// IMPORTANT: DLL_EXPORT is hardcoded in exports_generator.cpp,
// so any name changes here must be reflected there as well.
#define DLL_EXPORT(TYPE) extern "C" [[maybe_unused]] __declspec(dllexport) TYPE __cdecl
namespace smoke_api {
void init(HMODULE module_handle);
void shutdown();
AppId_t get_app_id();
}

View File

@@ -1,19 +0,0 @@
#include <koalabox/logger.hpp>
#include "steam_api/exports/steam_api.hpp"
#include "smoke_api.hpp"
#include "smoke_api/config.hpp"
DLL_EXPORT(bool) SteamAPI_RestartAppIfNecessary(const AppId_t unOwnAppID) {
LOG_INFO("{} -> unOwnAppID: {}", __func__, unOwnAppID);
// Restart can be suppressed if needed
AUTO_CALL(SteamAPI_RestartAppIfNecessary, unOwnAppID);
}
DLL_EXPORT(void) SteamAPI_Shutdown() {
LOG_INFO("{} -> Game requested shutdown", __func__);
AUTO_CALL(SteamAPI_Shutdown);
}

View File

@@ -1,8 +0,0 @@
#pragma once
#include "smoke_api.hpp"
#include "smoke_api/types.hpp"
DLL_EXPORT(bool) SteamAPI_RestartAppIfNecessary(AppId_t unOwnAppID);
DLL_EXPORT(void) SteamAPI_Shutdown();

View File

@@ -1,321 +0,0 @@
#include <koalabox/logger.hpp>
#include "smoke_api.hpp"
#include "smoke_api/interfaces/steam_apps.hpp"
#include "smoke_api/interfaces/steam_inventory.hpp"
#include "smoke_api/interfaces/steam_user.hpp"
#include "steam_api/steam_client.hpp"
#include "steam_api/steam_interface.hpp"
// ISteamApps
DLL_EXPORT(bool) SteamAPI_ISteamApps_BIsSubscribedApp(void* self, const AppId_t dlcID) {
try {
return smoke_api::steam_apps::IsDlcUnlocked(
__func__,
steam_interface::get_app_id(),
dlcID,
MODULE_CALL_CLOSURE(SteamAPI_ISteamApps_BIsSubscribedApp, self, dlcID)
);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return false;
}
}
DLL_EXPORT(bool) SteamAPI_ISteamApps_BIsDlcInstalled(void* self, const AppId_t dlcID) {
try {
return smoke_api::steam_apps::IsDlcUnlocked(
__func__,
steam_interface::get_app_id(),
dlcID,
MODULE_CALL_CLOSURE(SteamAPI_ISteamApps_BIsDlcInstalled, self, dlcID)
);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return false;
}
}
DLL_EXPORT(int) SteamAPI_ISteamApps_GetDLCCount(void* self) {
try {
return smoke_api::steam_apps::GetDLCCount(
__func__,
steam_interface::get_app_id(),
MODULE_CALL_CLOSURE(SteamAPI_ISteamApps_GetDLCCount, self)
);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return 0;
}
}
DLL_EXPORT(bool) SteamAPI_ISteamApps_BGetDLCDataByIndex(
void* self,
const int iDLC,
AppId_t* pDlcID,
bool* pbAvailable,
char* pchName,
const int cchNameBufferSize
) {
try {
return smoke_api::steam_apps::GetDLCDataByIndex(
__func__,
steam_interface::get_app_id(),
iDLC,
pDlcID,
pbAvailable,
pchName,
cchNameBufferSize,
MODULE_CALL_CLOSURE(
SteamAPI_ISteamApps_BGetDLCDataByIndex,
self,
iDLC,
pDlcID,
pbAvailable,
pchName,
cchNameBufferSize
),
[&](const AppId_t dlc_id) {
return SteamAPI_ISteamApps_BIsDlcInstalled(self, dlc_id);
}
);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return false;
}
}
// ISteamClient
DLL_EXPORT(void*) SteamAPI_ISteamClient_GetISteamGenericInterface(
void* self,
const HSteamUser hSteamUser,
const HSteamPipe hSteamPipe,
const char* pchVersion
) {
try {
return steam_client::GetGenericInterface(
__func__,
pchVersion,
MODULE_CALL_CLOSURE(
SteamAPI_ISteamClient_GetISteamGenericInterface,
self,
hSteamUser,
hSteamPipe,
pchVersion
)
);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return nullptr;
}
}
// ISteamInventory
DLL_EXPORT(EResult) SteamAPI_ISteamInventory_GetResultStatus(
void* self,
const SteamInventoryResult_t resultHandle
) {
return smoke_api::steam_inventory::GetResultStatus(
__func__,
resultHandle,
MODULE_CALL_CLOSURE(SteamAPI_ISteamInventory_GetResultStatus, self, resultHandle)
);
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetItemDefinitionIDs(
void* self,
SteamItemDef_t* pItemDefIDs,
uint32_t* punItemDefIDsArraySize
) {
return smoke_api::steam_inventory::GetItemDefinitionIDs(
__func__,
pItemDefIDs,
punItemDefIDsArraySize,
MODULE_CALL_CLOSURE(
SteamAPI_ISteamInventory_GetItemDefinitionIDs,
self,
pItemDefIDs,
punItemDefIDsArraySize
)
);
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetResultItems(
void* self,
const SteamInventoryResult_t resultHandle,
SteamItemDetails_t* pOutItemsArray,
uint32_t* punOutItemsArraySize
) {
return smoke_api::steam_inventory::GetResultItems(
__func__,
resultHandle,
pOutItemsArray,
punOutItemsArraySize,
MODULE_CALL_CLOSURE(
SteamAPI_ISteamInventory_GetResultItems,
self,
resultHandle,
pOutItemsArray,
punOutItemsArraySize
),
[&](SteamItemDef_t* pItemDefIDs, uint32_t* punItemDefIDsArraySize) {
MODULE_CALL(
SteamAPI_ISteamInventory_GetItemDefinitionIDs,
self,
pItemDefIDs,
punItemDefIDsArraySize
);
}
);
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetResultItemProperty(
void* self,
const SteamInventoryResult_t resultHandle,
const uint32_t unItemIndex,
const char* pchPropertyName,
char* pchValueBuffer,
uint32_t* punValueBufferSizeOut
) {
return smoke_api::steam_inventory::GetResultItemProperty(
__func__,
resultHandle,
unItemIndex,
pchPropertyName,
pchValueBuffer,
punValueBufferSizeOut,
MODULE_CALL_CLOSURE(
SteamAPI_ISteamInventory_GetResultItemProperty,
self,
resultHandle,
unItemIndex,
pchPropertyName,
pchValueBuffer,
punValueBufferSizeOut
)
);
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_CheckResultSteamID(
void* self,
const SteamInventoryResult_t resultHandle,
const CSteamID steamIDExpected
) {
return smoke_api::steam_inventory::CheckResultSteamID(
__func__,
resultHandle,
steamIDExpected,
MODULE_CALL_CLOSURE(
SteamAPI_ISteamInventory_CheckResultSteamID,
self,
resultHandle,
steamIDExpected
)
);
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetAllItems(
void* self,
SteamInventoryResult_t* pResultHandle
) {
return smoke_api::steam_inventory::GetAllItems(
__func__,
pResultHandle,
MODULE_CALL_CLOSURE(SteamAPI_ISteamInventory_GetAllItems, self, pResultHandle)
);
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetItemsByID(
void* self,
SteamInventoryResult_t* pResultHandle,
const SteamItemInstanceID_t* pInstanceIDs,
const uint32_t unCountInstanceIDs
) {
return smoke_api::steam_inventory::GetItemsByID(
__func__,
pResultHandle,
pInstanceIDs,
unCountInstanceIDs,
MODULE_CALL_CLOSURE(
SteamAPI_ISteamInventory_GetItemsByID,
self,
pResultHandle,
pInstanceIDs,
unCountInstanceIDs
)
);
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_SerializeResult(
void* self,
const SteamInventoryResult_t resultHandle,
void* pOutBuffer,
uint32_t* punOutBufferSize
) {
return smoke_api::steam_inventory::SerializeResult(
__func__,
resultHandle,
pOutBuffer,
punOutBufferSize,
MODULE_CALL_CLOSURE(
SteamAPI_ISteamInventory_SerializeResult,
self,
resultHandle,
pOutBuffer,
punOutBufferSize
)
);
}
// ISteamUser
DLL_EXPORT(EUserHasLicenseForAppResult) SteamAPI_ISteamUser_UserHasLicenseForApp(
void* self,
const CSteamID steamID,
const AppId_t dlcID
) {
try {
return smoke_api::steam_user::UserHasLicenseForApp(
__func__,
steam_interface::get_app_id(),
dlcID,
MODULE_CALL_CLOSURE(
SteamAPI_ISteamUser_UserHasLicenseForApp,
self,
steamID,
dlcID
)
);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return k_EUserHasLicenseResultDoesNotHaveLicense;
}
}
// ISteamGameServer
DLL_EXPORT(EUserHasLicenseForAppResult) SteamAPI_ISteamGameServer_UserHasLicenseForApp(
void* self,
const CSteamID steamID,
const AppId_t dlcID
) {
try {
return smoke_api::steam_user::UserHasLicenseForApp(
__func__,
steam_interface::get_app_id(),
dlcID,
MODULE_CALL_CLOSURE(
SteamAPI_ISteamGameServer_UserHasLicenseForApp,
self,
steamID,
dlcID
)
);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return k_EUserHasLicenseResultDoesNotHaveLicense;
}
}

View File

@@ -1,22 +0,0 @@
#include "smoke_api.hpp"
#include "smoke_api/types.hpp"
#include "steam_api/steam_client.hpp"
DLL_EXPORT(void*) SteamInternal_FindOrCreateUserInterface(
const HSteamUser hSteamUser,
const char* version
) {
return steam_client::GetGenericInterface(
__func__,
version,
MODULE_CALL_CLOSURE(SteamInternal_FindOrCreateUserInterface, hSteamUser, version)
);
}
DLL_EXPORT(void*) SteamInternal_CreateInterface(const char* version) {
return steam_client::GetGenericInterface(
__func__,
version,
MODULE_CALL_CLOSURE(SteamInternal_CreateInterface, version)
);
}

View File

@@ -1,91 +0,0 @@
#include <regex>
#include <map>
#include <koalabox/logger.hpp>
#include <koalabox/win.hpp>
#include "smoke_api.hpp"
#include "steam_api/steam_client.hpp"
namespace {
namespace kb = koalabox;
/**
* Searches the `.rdata` section of the original dll for the full interface version string
* Results are cached for performance.
*/
std::string get_versioned_interface(
const std::string& version_prefix,
const std::string& fallback
) {
static std::map<std::string, std::string> version_map;
if(not version_map.contains(version_prefix)) {
try {
const std::string rdata = kb::win::get_pe_section_data_or_throw(
smoke_api::steamapi_module,
".rdata"
);
const std::regex regex(version_prefix + "\\d{3}");
if(std::smatch match; std::regex_search(rdata, match, regex)) {
version_map[version_prefix] = match[0];
return version_map[version_prefix];
}
throw std::runtime_error(std::format("No match found for '{}'", version_prefix));
} catch(const std::exception& ex) {
LOG_ERROR(
"Failed to get versioned interface: {}."
"Falling back to version {}",
ex.what(),
fallback
);
version_map[version_prefix] = version_prefix + fallback;
}
}
return version_map[version_prefix];
}
}
DLL_EXPORT(void*) SteamClient() {
static auto version = get_versioned_interface(STEAM_CLIENT, "006");
return steam_client::GetGenericInterface(
__func__,
version,
MODULE_CALL_CLOSURE(SteamClient)
);
}
DLL_EXPORT(void*) SteamApps() {
static auto version = get_versioned_interface(STEAM_APPS, "002");
return steam_client::GetGenericInterface(
__func__,
version,
MODULE_CALL_CLOSURE(SteamApps)
);
}
DLL_EXPORT(void*) SteamUser() {
static auto version = get_versioned_interface(STEAM_USER, "012");
return steam_client::GetGenericInterface(
__func__,
version,
MODULE_CALL_CLOSURE(SteamUser)
);
}
DLL_EXPORT(void*) SteamInventory() {
static auto version = get_versioned_interface(STEAM_INVENTORY, "001");
return steam_client::GetGenericInterface(
__func__,
version,
MODULE_CALL_CLOSURE(SteamInventory)
);
}

View File

@@ -1,19 +1,25 @@
#include <koalabox/logger.hpp> #include <koalabox/logger.hpp>
#include "steam_api/steam_interface.hpp" #include "steam_client.hpp"
#include "steam_api/steam_interfaces.hpp"
namespace steam_client { namespace steam_client {
void* GetGenericInterface( void* GetGenericInterface(
const std::string& function_name, const std::string& function_name,
const std::string& interface_version, const std::string& interface_version,
const std::function<void*()>& original_function const std::function<void*()>& original_function
) { ) noexcept {
auto* const interface = original_function(); try {
auto* const interface = original_function();
LOG_DEBUG("{} -> '{}' @ {}", function_name, interface_version, interface); LOG_DEBUG("{} -> '{}' @ {}", function_name, interface_version, interface);
steam_interface::hook_virtuals(interface, interface_version); steam_interfaces::hook_virtuals(interface, interface_version);
return interface; return interface;
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: '{}' @ {}", function_name, interface_version, e.what());
return nullptr;
}
} }
} }

View File

@@ -7,5 +7,5 @@ namespace steam_client {
const std::string& function_name, const std::string& function_name,
const std::string& interface_version, const std::string& interface_version,
const std::function<void*()>& original_function const std::function<void*()>& original_function
); ) noexcept;
} }

View File

@@ -1,161 +0,0 @@
#include <ranges>
#include <set>
#include <battery/embed.hpp>
#include <koalabox/hook.hpp>
#include <koalabox/logger.hpp>
#include <koalabox/win.hpp>
#include "smoke_api.hpp"
#include "virtuals/steam_api_virtuals.hpp"
namespace {
struct interface_entry {
// function_name must match the function identifier to be able to call original functions
std::string function_name; // e.g. "ISteamClient_GetISteamApps"
uintptr_t function_address; // e.g. ISteamClient_GetISteamApps
};
struct interface_data {
std::string fallback_version; // e.g. "SteamClient021"
std::map<std::string, interface_entry> entry_map;
// e.g. {ENTRY(ISteamClient, GetISteamApps), ...}
};
std::map<std::string, interface_data> get_virtual_hook_map() {
#define ENTRY(INTERFACE, FUNC) \
{ \
#FUNC, { \
#INTERFACE "_" #FUNC, reinterpret_cast<uintptr_t>(INTERFACE##_##FUNC) \
} \
}
return {
{
STEAM_CLIENT,
interface_data{
.fallback_version = "SteamClient021",
.entry_map = {
ENTRY(ISteamClient, GetISteamApps),
ENTRY(ISteamClient, GetISteamUser),
ENTRY(ISteamClient, GetISteamGenericInterface),
ENTRY(ISteamClient, GetISteamInventory),
}
}
},
{
STEAM_APPS,
interface_data{
.fallback_version = "STEAMAPPS_INTERFACE_VERSION008",
.entry_map = {
ENTRY(ISteamApps, BIsSubscribedApp),
ENTRY(ISteamApps, BIsDlcInstalled),
ENTRY(ISteamApps, GetDLCCount),
ENTRY(ISteamApps, BGetDLCDataByIndex),
}
}
},
{
STEAM_USER,
interface_data{
.fallback_version = "SteamUser023",
.entry_map = {
ENTRY(ISteamUser, UserHasLicenseForApp),
}
}
},
{
STEAM_GAME_SERVER,
interface_data{
.fallback_version = "SteamGameServer015",
.entry_map = {
ENTRY(ISteamGameServer, UserHasLicenseForApp),
}
}
},
};
}
nlohmann::json read_interface_lookup() {
const auto lookup_str = b::embed<"res/interface_lookup.json">().str();
return nlohmann::json::parse(lookup_str);
}
const nlohmann::json& find_lookup(
const std::string& interface_version,
const std::string& fallback_version
) {
static const auto lookup = read_interface_lookup();
if(lookup.contains(interface_version)) {
return lookup[interface_version];
}
LOG_WARN(
"Interface version '{}' not found in lookup map. Using fallback: '{}'",
interface_version,
fallback_version
);
return lookup[fallback_version];
}
}
namespace steam_interface {
namespace kb = koalabox;
AppId_t get_app_id_or_throw() {
const auto app_id_str = kb::win::get_env_var("SteamAppId");
return std::stoi(app_id_str);
}
AppId_t get_app_id() {
try {
static const auto app_id = get_app_id_or_throw();
return app_id;
} catch(const std::exception& e) {
LOG_ERROR("Failed to get app id: {}", e.what());
return 0;
}
}
void hook_virtuals(void* interface, const std::string& version_string) {
if(interface == nullptr) {
// Game has tried to use an interface before initializing steam api
return;
}
static std::set<void*> processed_interfaces;
if(processed_interfaces.contains(interface)) {
LOG_DEBUG("Interface {} at {} has already been processed.", version_string, interface);
return;
}
static std::mutex section;
const std::lock_guard guard(section);
static const auto virtual_hook_map = get_virtual_hook_map();
for(const auto& [prefix, data] : virtual_hook_map) {
if(version_string.starts_with(prefix)) {
const auto& lookup = find_lookup(version_string, data.fallback_version);
for(const auto& [function, entry] : data.entry_map) {
if(lookup.contains(function)) {
kb::hook::swap_virtual_func(
interface,
entry.function_name,
lookup[function],
entry.function_address
);
}
}
break;
}
}
processed_interfaces.insert(interface);
}
}

View File

@@ -1,10 +0,0 @@
#pragma once
#include "smoke_api/types.hpp"
namespace steam_interface {
AppId_t get_app_id_or_throw();
AppId_t get_app_id();
void hook_virtuals(void* interface, const std::string& version_string);
}

View File

@@ -0,0 +1,252 @@
#include <ranges>
#include <set>
#include <battery/embed.hpp>
#include <koalabox/hook.hpp>
#include <koalabox/logger.hpp>
#include <koalabox/win.hpp>
#include "steam_api/steam_interfaces.hpp"
#include "smoke_api/smoke_api.hpp"
#include "smoke_api/steamclient/steamclient.hpp"
#include "virtuals/steam_api_virtuals.hpp"
namespace {
struct interface_entry {
// function_name must match the function identifier to be able to call original functions
std::string function_name; // e.g. "ISteamClient_GetISteamApps"
void* function_address; // e.g. ISteamClient_GetISteamApps
};
struct interface_data_t { // NOLINT(*-exception-escape)
std::string fallback_version; // e.g. "SteamClient021"
// Key is function name without interface prefix
std::map<std::string, interface_entry> entry_map;
// e.g. {ENTRY(ISteamClient, GetISteamApps), ...}
};
// Key is interface name, e.g. "SteamClient"
std::map<std::string, interface_data_t> get_virtual_hook_map() {
#define ENTRY(INTERFACE, FUNC) \
{ \
#FUNC, { \
#INTERFACE "_" #FUNC, reinterpret_cast<void*>(INTERFACE##_##FUNC) \
} \
}
return {
{
STEAM_APPS,
interface_data_t{
.fallback_version = "STEAMAPPS_INTERFACE_VERSION008",
.entry_map = {
ENTRY(ISteamApps, BIsSubscribedApp),
ENTRY(ISteamApps, BIsDlcInstalled),
ENTRY(ISteamApps, GetDLCCount),
ENTRY(ISteamApps, BGetDLCDataByIndex),
}
}
},
{
STEAM_CLIENT,
interface_data_t{
.fallback_version = "SteamClient021",
.entry_map = {
ENTRY(ISteamClient, GetISteamApps),
ENTRY(ISteamClient, GetISteamUser),
ENTRY(ISteamClient, GetISteamGenericInterface),
ENTRY(ISteamClient, GetISteamInventory),
}
}
},
{
STEAM_GAME_SERVER,
interface_data_t{
.fallback_version = "SteamGameServer015",
.entry_map = {
ENTRY(ISteamGameServer, UserHasLicenseForApp),
}
}
},
{
STEAM_HTTP,
interface_data_t{
.fallback_version = "STEAMHTTP_INTERFACE_VERSION003",
.entry_map = {
ENTRY(ISteamHTTP, GetHTTPResponseBodyData),
ENTRY(ISteamHTTP, GetHTTPStreamingResponseBodyData),
ENTRY(ISteamHTTP, SetHTTPRequestRawPostBody),
}
}
},
{
STEAM_INVENTORY,
interface_data_t{
.fallback_version = "STEAMINVENTORY_INTERFACE_V003",
.entry_map = {
ENTRY(ISteamInventory, GetResultStatus),
ENTRY(ISteamInventory, GetResultItems),
ENTRY(ISteamInventory, CheckResultSteamID),
ENTRY(ISteamInventory, GetAllItems),
ENTRY(ISteamInventory, GetItemsByID),
ENTRY(ISteamInventory, SerializeResult),
ENTRY(ISteamInventory, GetItemDefinitionIDs),
}
}
},
{
STEAM_USER,
interface_data_t{
.fallback_version = "SteamUser023",
.entry_map = {
ENTRY(ISteamUser, UserHasLicenseForApp),
}
}
},
};
}
// Key is function name, Value is ordinal
using ordinal_map_t = std::map<std::string, uint16_t>;
// Key is interface version string
using lookup_map_t = std::map<std::string, ordinal_map_t>;
lookup_map_t read_interface_lookup() {
lookup_map_t lookup_map;
const auto lookup_str = b::embed<"res/interface_lookup.json">().str();
const auto lookup_json = nlohmann::json::parse(lookup_str);
lookup_json.get_to(lookup_map);
return lookup_map;
}
const std::map<std::string, uint16_t>& find_lookup(
const std::string& interface_version,
const std::string& fallback_version
) {
static const auto lookup = read_interface_lookup();
if(lookup.contains(interface_version)) {
return lookup.at(interface_version);
}
LOG_WARN(
"Interface version '{}' not found in lookup map. Using fallback: '{}'",
interface_version,
fallback_version
);
return lookup.at(fallback_version);
}
}
namespace steam_interfaces {
namespace kb = koalabox;
/**
* @param interface_ptr Pointer to the interface
* @param version_string Example: 'SteamClient007'
*/
void hook_virtuals(const void* interface_ptr, const std::string& version_string) {
if(interface_ptr == nullptr) {
// Game has tried to use an interface before initializing steam api
// This does happen in practice.
return;
}
static std::mutex section;
const std::lock_guard guard(section);
static std::set<const void*> processed_interfaces;
if(processed_interfaces.contains(interface_ptr)) {
LOG_DEBUG("Interface '{}' @ {} has already been processed.", version_string, interface_ptr);
return;
}
processed_interfaces.insert(interface_ptr);
static const auto virtual_hook_map = get_virtual_hook_map();
for(const auto& [prefix, data] : virtual_hook_map) {
if(not version_string.starts_with(prefix)) {
continue;
}
LOG_INFO("Processing '{}' @ {} found in virtual hook map", version_string, interface_ptr);
const auto& lookup = find_lookup(version_string, data.fallback_version);
for(const auto& [function, entry] : data.entry_map) {
if(not lookup.contains(function)) {
continue;
}
kb::hook::swap_virtual_func(
interface_ptr,
entry.function_name,
lookup.at(function),
entry.function_address
);
}
break;
}
}
void hook_steamclient_interface(
const HMODULE steamclient_handle,
const std::string& steam_client_interface_version
) noexcept {
try {
// Create a copy for modification
auto virtual_hook_map = get_virtual_hook_map();
// Remove steam client map since we don't want to hook its methods
virtual_hook_map.erase(STEAM_CLIENT);
// Map remaining virtual hook map to a set of keys
const auto prefixes = std::views::keys(virtual_hook_map) | std::ranges::to<std::set>();
// Prepare HSteamPipe and HSteamUser
const auto CreateInterface$ = KB_WIN_GET_PROC(steamclient_handle, CreateInterface);
const auto* const THIS = CreateInterface$(steam_client_interface_version.c_str(), nullptr);
hook_virtuals(THIS, steam_client_interface_version);
const auto interface_lookup = read_interface_lookup();
for(const auto& interface_version : interface_lookup | std::views::keys) {
// SteamUser and SteamPipe handles must match the ones previously used by the game,
// otherwise SteamAPI will just create new instances of interfaces, instead of returning
// existing instances that are used by the game. Usually these handles default to 1,
// but if a game creates several of them, then we need to somehow find them out dynamically.
constexpr auto steam_pipe = 1;
constexpr auto steam_user = 1;
const bool should_hook = std::ranges::any_of(
prefixes,
[&](const auto& prefix) {
return std::ranges::starts_with(interface_version, prefix);
}
);
if(not should_hook) {
continue;
}
DECLARE_EDX();
const auto* const interface_ptr = ISteamClient_GetISteamGenericInterface(
ARGS(steam_user, steam_pipe, interface_version.c_str())
);
if(not interface_ptr) {
LOG_ERROR("Failed to get generic interface: '{}'", interface_version)
}
}
kb::hook::unhook_vt_all(THIS);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Unhandled exception: {}", __func__, e.what());
}
}
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include "smoke_api/types.hpp"
namespace steam_interfaces {
void hook_virtuals(const void* interface_ptr, const std::string& version_string);
/**
* A fallback mechanism used when SteamAPI has already been initialized.
* It will hook the SteamClient interface and hook its interface accessors.
* This allows us to hook interfaces that are no longer being created,
* such as in the case of late injection.
*/
void hook_steamclient_interface(
HMODULE steamclient_handle,
const std::string& steam_client_interface_version
) noexcept;
}

View File

@@ -1,48 +1,33 @@
#include <koalabox/logger.hpp> #include <koalabox/logger.hpp>
#include "smoke_api/smoke_api.hpp"
#include "smoke_api/interfaces/steam_apps.hpp" #include "smoke_api/interfaces/steam_apps.hpp"
#include "steam_api/steam_interface.hpp"
#include "steam_api/virtuals/steam_api_virtuals.hpp" #include "steam_api/virtuals/steam_api_virtuals.hpp"
VIRTUAL(bool) ISteamApps_BIsSubscribedApp(PARAMS(const AppId_t dlc_id)) { VIRTUAL(bool) ISteamApps_BIsSubscribedApp(PARAMS(const AppId_t dlc_id)) noexcept {
try { return smoke_api::steam_apps::IsDlcUnlocked(
return smoke_api::steam_apps::IsDlcUnlocked( __func__,
__func__, smoke_api::get_app_id(),
steam_interface::get_app_id(), dlc_id,
dlc_id, SWAPPED_CALL_CLOSURE(ISteamApps_BIsSubscribedApp, ARGS(dlc_id))
HOOKED_CALL_CLOSURE(ISteamApps_BIsSubscribedApp, ARGS(dlc_id)) );
);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return false;
}
} }
VIRTUAL(bool) ISteamApps_BIsDlcInstalled(PARAMS(const AppId_t dlc_id)) { VIRTUAL(bool) ISteamApps_BIsDlcInstalled(PARAMS(const AppId_t dlc_id)) noexcept {
try { return smoke_api::steam_apps::IsDlcUnlocked(
return smoke_api::steam_apps::IsDlcUnlocked( __func__,
__func__, smoke_api::get_app_id(),
steam_interface::get_app_id(), dlc_id,
dlc_id, SWAPPED_CALL_CLOSURE(ISteamApps_BIsDlcInstalled, ARGS(dlc_id))
HOOKED_CALL_CLOSURE(ISteamApps_BIsDlcInstalled, ARGS(dlc_id)) );
);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return false;
}
} }
VIRTUAL(int) ISteamApps_GetDLCCount(PARAMS()) { VIRTUAL(int) ISteamApps_GetDLCCount(PARAMS()) noexcept {
try { return smoke_api::steam_apps::GetDLCCount(
return smoke_api::steam_apps::GetDLCCount( __func__,
__func__, smoke_api::get_app_id(),
steam_interface::get_app_id(), SWAPPED_CALL_CLOSURE(ISteamApps_GetDLCCount, ARGS())
HOOKED_CALL_CLOSURE(ISteamApps_GetDLCCount, ARGS()) );
);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return 0;
}
} }
VIRTUAL(bool) ISteamApps_BGetDLCDataByIndex( VIRTUAL(bool) ISteamApps_BGetDLCDataByIndex(
@@ -53,26 +38,22 @@ VIRTUAL(bool) ISteamApps_BGetDLCDataByIndex(
char* pchName, char* pchName,
const int cchNameBufferSize const int cchNameBufferSize
) )
) { ) noexcept {
try { return smoke_api::steam_apps::GetDLCDataByIndex(
return smoke_api::steam_apps::GetDLCDataByIndex( __func__,
__func__, smoke_api::get_app_id(),
steam_interface::get_app_id(), iDLC,
iDLC, p_dlc_id,
p_dlc_id, pbAvailable,
pbAvailable, pchName,
pchName, cchNameBufferSize,
cchNameBufferSize, SWAPPED_CALL_CLOSURE(
HOOKED_CALL_CLOSURE( ISteamApps_BGetDLCDataByIndex,
ISteamApps_BGetDLCDataByIndex, ARGS(iDLC, p_dlc_id, pbAvailable, pchName, cchNameBufferSize)
ARGS(iDLC, p_dlc_id, pbAvailable, pchName, cchNameBufferSize) ),
), SWAPPED_CALL_CLOSURE(
[&](const AppId_t dlc_id) { ISteamApps_BIsSubscribedApp,
return ISteamApps_BIsDlcInstalled(ARGS(dlc_id)); ARGS(*p_dlc_id)
} )
); );
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return false;
}
} }

View File

@@ -7,60 +7,45 @@ VIRTUAL(void*) ISteamClient_GetISteamApps(
PARAMS( PARAMS(
const HSteamUser hSteamUser, const HSteamUser hSteamUser,
const HSteamPipe hSteamPipe, const HSteamPipe hSteamPipe,
const char* version const char* pchVersion
) )
) { ) noexcept {
try { return steam_client::GetGenericInterface(
return steam_client::GetGenericInterface( __func__,
__func__, pchVersion,
version, SWAPPED_CALL_CLOSURE(ISteamClient_GetISteamApps, ARGS(hSteamUser, hSteamPipe, pchVersion))
HOOKED_CALL_CLOSURE(ISteamClient_GetISteamApps, ARGS(hSteamUser, hSteamPipe, version)) );
);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return nullptr;
}
} }
VIRTUAL(void*) ISteamClient_GetISteamUser( VIRTUAL(void*) ISteamClient_GetISteamUser(
PARAMS( PARAMS(
const HSteamUser hSteamUser, const HSteamUser hSteamUser,
const HSteamPipe hSteamPipe, const HSteamPipe hSteamPipe,
const char* version const char* pchVersion
) )
) { ) noexcept {
try { return steam_client::GetGenericInterface(
return steam_client::GetGenericInterface( __func__,
__func__, pchVersion,
version, SWAPPED_CALL_CLOSURE(ISteamClient_GetISteamUser, ARGS(hSteamUser, hSteamPipe, pchVersion))
HOOKED_CALL_CLOSURE(ISteamClient_GetISteamUser, ARGS(hSteamUser, hSteamPipe, version)) );
);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return nullptr;
}
} }
VIRTUAL(void*) ISteamClient_GetISteamGenericInterface( VIRTUAL(void*) ISteamClient_GetISteamGenericInterface(
PARAMS( PARAMS(
HSteamUser hSteamUser, const HSteamUser hSteamUser,
HSteamPipe hSteamPipe, const HSteamPipe hSteamPipe,
const char* pchVersion const char* pchVersion
) )
) { ) noexcept {
try { return steam_client::GetGenericInterface(
return steam_client::GetGenericInterface( __func__,
__func__, pchVersion,
pchVersion, SWAPPED_CALL_CLOSURE(
HOOKED_CALL_CLOSURE( ISteamClient_GetISteamGenericInterface,
ISteamClient_GetISteamGenericInterface, ARGS(hSteamUser, hSteamPipe, pchVersion)
ARGS(hSteamUser, hSteamPipe, pchVersion) )
) );
);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return nullptr;
}
} }
VIRTUAL(void*) ISteamClient_GetISteamInventory( VIRTUAL(void*) ISteamClient_GetISteamInventory(
@@ -69,18 +54,13 @@ VIRTUAL(void*) ISteamClient_GetISteamInventory(
const HSteamPipe hSteamPipe, const HSteamPipe hSteamPipe,
const char* pchVersion const char* pchVersion
) )
) { ) noexcept {
try { return steam_client::GetGenericInterface(
return steam_client::GetGenericInterface( __func__,
__func__, pchVersion,
pchVersion, SWAPPED_CALL_CLOSURE(
HOOKED_CALL_CLOSURE( ISteamClient_GetISteamInventory,
ISteamClient_GetISteamInventory, ARGS(hSteamUser, hSteamPipe, pchVersion)
ARGS(hSteamUser, hSteamPipe, pchVersion) )
) );
);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return nullptr;
}
} }

View File

@@ -1,21 +1,16 @@
#include <koalabox/logger.hpp> #include <koalabox/logger.hpp>
#include "smoke_api/smoke_api.hpp"
#include "smoke_api/interfaces/steam_user.hpp" #include "smoke_api/interfaces/steam_user.hpp"
#include "steam_api/steam_interface.hpp"
#include "steam_api/virtuals/steam_api_virtuals.hpp" #include "steam_api/virtuals/steam_api_virtuals.hpp"
VIRTUAL(EUserHasLicenseForAppResult) ISteamGameServer_UserHasLicenseForApp( VIRTUAL(EUserHasLicenseForAppResult) ISteamGameServer_UserHasLicenseForApp(
PARAMS(const CSteamID steamID, const AppId_t dlc_id) PARAMS(const CSteamID steamID, const AppId_t dlc_id)
) { ) noexcept {
try { return smoke_api::steam_user::UserHasLicenseForApp(
return smoke_api::steam_user::UserHasLicenseForApp( __func__,
__func__, smoke_api::get_app_id(),
steam_interface::get_app_id(), dlc_id,
dlc_id, SWAPPED_CALL_CLOSURE(ISteamGameServer_UserHasLicenseForApp, ARGS(steamID, dlc_id))
HOOKED_CALL_CLOSURE(ISteamGameServer_UserHasLicenseForApp, ARGS(steamID, dlc_id)) );
);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return k_EUserHasLicenseResultDoesNotHaveLicense;
}
} }

View File

@@ -0,0 +1,65 @@
#include <koalabox/logger.hpp>
#include "smoke_api/interfaces/steam_http.hpp"
#include "steam_api/virtuals/steam_api_virtuals.hpp"
VIRTUAL(bool) ISteamHTTP_GetHTTPResponseBodyData(
PARAMS(
const HTTPRequestHandle hRequest,
const uint8_t* pBodyDataBuffer,
const uint32_t unBufferSize
)
) noexcept {
return smoke_api::steam_http::GetHTTPResponseBodyData(
__func__,
hRequest,
pBodyDataBuffer,
unBufferSize,
SWAPPED_CALL_CLOSURE(
ISteamHTTP_GetHTTPResponseBodyData,
ARGS(hRequest, pBodyDataBuffer, unBufferSize)
)
);
}
VIRTUAL(bool) ISteamHTTP_GetHTTPStreamingResponseBodyData(
PARAMS(
const HTTPRequestHandle hRequest,
const uint32_t cOffset,
const uint8_t* pBodyDataBuffer,
const uint32_t unBufferSize
)
) noexcept {
return smoke_api::steam_http::GetHTTPStreamingResponseBodyData(
__func__,
hRequest,
cOffset,
pBodyDataBuffer,
unBufferSize,
SWAPPED_CALL_CLOSURE(
ISteamHTTP_GetHTTPStreamingResponseBodyData,
ARGS(hRequest, cOffset, pBodyDataBuffer, unBufferSize)
)
);
}
VIRTUAL(bool) ISteamHTTP_SetHTTPRequestRawPostBody(
PARAMS(
const HTTPRequestHandle hRequest,
const char* pchContentType,
const uint8_t* pubBody,
const uint32_t unBodyLen
)
) noexcept {
return smoke_api::steam_http::SetHTTPRequestRawPostBody(
__func__,
hRequest,
pchContentType,
pubBody,
unBodyLen,
SWAPPED_CALL_CLOSURE(
ISteamHTTP_SetHTTPRequestRawPostBody,
ARGS(hRequest, pchContentType, pubBody, unBodyLen)
)
);
}

View File

@@ -3,11 +3,11 @@
VIRTUAL(EResult) ISteamInventory_GetResultStatus( VIRTUAL(EResult) ISteamInventory_GetResultStatus(
PARAMS(const SteamInventoryResult_t resultHandle) PARAMS(const SteamInventoryResult_t resultHandle)
) { ) noexcept {
return smoke_api::steam_inventory::GetResultStatus( return smoke_api::steam_inventory::GetResultStatus(
__func__, __func__,
resultHandle, resultHandle,
HOOKED_CALL_CLOSURE(ISteamInventory_GetResultStatus, ARGS(resultHandle)) SWAPPED_CALL_CLOSURE(ISteamInventory_GetResultStatus, ARGS(resultHandle))
); );
} }
@@ -17,18 +17,19 @@ VIRTUAL(bool) ISteamInventory_GetResultItems(
SteamItemDetails_t* pOutItemsArray, SteamItemDetails_t* pOutItemsArray,
uint32_t* punOutItemsArraySize uint32_t* punOutItemsArraySize
) )
) { ) noexcept {
return smoke_api::steam_inventory::GetResultItems( return smoke_api::steam_inventory::GetResultItems(
__func__, __func__,
resultHandle, resultHandle,
pOutItemsArray, pOutItemsArray,
punOutItemsArraySize, punOutItemsArraySize,
HOOKED_CALL_CLOSURE( SWAPPED_CALL_CLOSURE(
ISteamInventory_GetResultItems, ISteamInventory_GetResultItems,
ARGS(resultHandle, pOutItemsArray, punOutItemsArraySize) ARGS(resultHandle, pOutItemsArray, punOutItemsArraySize)
), ),
[&](SteamItemDef_t* pItemDefIDs, uint32_t* punItemDefIDsArraySize) { [&](SteamItemDef_t* pItemDefIDs, uint32_t* punItemDefIDsArraySize) {
HOOKED_CALL( SWAPPED_CALL(
THIS,
ISteamInventory_GetItemDefinitionIDs, ISteamInventory_GetItemDefinitionIDs,
ARGS(pItemDefIDs, punItemDefIDsArraySize) ARGS(pItemDefIDs, punItemDefIDsArraySize)
); );
@@ -44,7 +45,7 @@ VIRTUAL(bool) ISteamInventory_GetResultItemProperty(
char* pchValueBuffer, char* pchValueBuffer,
uint32_t* punValueBufferSizeOut uint32_t* punValueBufferSizeOut
) )
) { ) noexcept {
return smoke_api::steam_inventory::GetResultItemProperty( return smoke_api::steam_inventory::GetResultItemProperty(
__func__, __func__,
resultHandle, resultHandle,
@@ -52,7 +53,7 @@ VIRTUAL(bool) ISteamInventory_GetResultItemProperty(
pchPropertyName, pchPropertyName,
pchValueBuffer, pchValueBuffer,
punValueBufferSizeOut, punValueBufferSizeOut,
HOOKED_CALL_CLOSURE( SWAPPED_CALL_CLOSURE(
ISteamInventory_GetResultItemProperty, ISteamInventory_GetResultItemProperty,
ARGS( ARGS(
resultHandle, resultHandle,
@@ -65,11 +66,11 @@ VIRTUAL(bool) ISteamInventory_GetResultItemProperty(
); );
} }
VIRTUAL(bool) ISteamInventory_GetAllItems(PARAMS(SteamInventoryResult_t* pResultHandle)) { VIRTUAL(bool) ISteamInventory_GetAllItems(PARAMS(SteamInventoryResult_t* pResultHandle)) noexcept {
return smoke_api::steam_inventory::GetAllItems( return smoke_api::steam_inventory::GetAllItems(
__func__, __func__,
pResultHandle, pResultHandle,
HOOKED_CALL_CLOSURE(ISteamInventory_GetAllItems, ARGS(pResultHandle)) SWAPPED_CALL_CLOSURE(ISteamInventory_GetAllItems, ARGS(pResultHandle))
); );
} }
@@ -79,13 +80,13 @@ VIRTUAL(bool) ISteamInventory_GetItemsByID(
const SteamItemInstanceID_t* pInstanceIDs, const SteamItemInstanceID_t* pInstanceIDs,
const uint32_t unCountInstanceIDs const uint32_t unCountInstanceIDs
) )
) { ) noexcept {
return smoke_api::steam_inventory::GetItemsByID( return smoke_api::steam_inventory::GetItemsByID(
__func__, __func__,
pResultHandle, pResultHandle,
pInstanceIDs, pInstanceIDs,
unCountInstanceIDs, unCountInstanceIDs,
HOOKED_CALL_CLOSURE( SWAPPED_CALL_CLOSURE(
ISteamInventory_GetItemsByID, ISteamInventory_GetItemsByID,
ARGS(pResultHandle, pInstanceIDs, unCountInstanceIDs) ARGS(pResultHandle, pInstanceIDs, unCountInstanceIDs)
) )
@@ -98,13 +99,13 @@ VIRTUAL(bool) ISteamInventory_SerializeResult(
void* pOutBuffer, void* pOutBuffer,
uint32_t* punOutBufferSize uint32_t* punOutBufferSize
) )
) { ) noexcept {
return smoke_api::steam_inventory::SerializeResult( return smoke_api::steam_inventory::SerializeResult(
__func__, __func__,
resultHandle, resultHandle,
pOutBuffer, pOutBuffer,
punOutBufferSize, punOutBufferSize,
HOOKED_CALL_CLOSURE( SWAPPED_CALL_CLOSURE(
ISteamInventory_SerializeResult, ISteamInventory_SerializeResult,
ARGS(resultHandle, pOutBuffer, punOutBufferSize) ARGS(resultHandle, pOutBuffer, punOutBufferSize)
) )
@@ -116,12 +117,12 @@ VIRTUAL(bool) ISteamInventory_GetItemDefinitionIDs(
SteamItemDef_t*pItemDefIDs, SteamItemDef_t*pItemDefIDs,
uint32_t* punItemDefIDsArraySize uint32_t* punItemDefIDsArraySize
) )
) { ) noexcept {
return smoke_api::steam_inventory::GetItemDefinitionIDs( return smoke_api::steam_inventory::GetItemDefinitionIDs(
__func__, __func__,
pItemDefIDs, pItemDefIDs,
punItemDefIDsArraySize, punItemDefIDsArraySize,
HOOKED_CALL_CLOSURE( SWAPPED_CALL_CLOSURE(
ISteamInventory_GetItemDefinitionIDs, ISteamInventory_GetItemDefinitionIDs,
ARGS(pItemDefIDs, punItemDefIDsArraySize) ARGS(pItemDefIDs, punItemDefIDsArraySize)
) )
@@ -130,14 +131,11 @@ VIRTUAL(bool) ISteamInventory_GetItemDefinitionIDs(
VIRTUAL(bool) ISteamInventory_CheckResultSteamID( VIRTUAL(bool) ISteamInventory_CheckResultSteamID(
PARAMS(const SteamInventoryResult_t resultHandle, CSteamID steamIDExpected) PARAMS(const SteamInventoryResult_t resultHandle, CSteamID steamIDExpected)
) { ) noexcept {
return smoke_api::steam_inventory::CheckResultSteamID( return smoke_api::steam_inventory::CheckResultSteamID(
__func__, __func__,
resultHandle, resultHandle,
steamIDExpected, steamIDExpected,
HOOKED_CALL_CLOSURE( SWAPPED_CALL_CLOSURE(ISteamInventory_CheckResultSteamID, ARGS(resultHandle, steamIDExpected))
ISteamInventory_CheckResultSteamID,
ARGS(resultHandle, steamIDExpected)
)
); );
} }

View File

@@ -1,22 +1,16 @@
#include <koalabox/logger.hpp> #include <koalabox/logger.hpp>
#include "smoke_api/smoke_api.hpp"
#include "smoke_api/interfaces/steam_user.hpp" #include "smoke_api/interfaces/steam_user.hpp"
#include "steam_api/steam_interface.hpp"
#include "steam_api/virtuals/steam_api_virtuals.hpp" #include "steam_api/virtuals/steam_api_virtuals.hpp"
VIRTUAL(EUserHasLicenseForAppResult) ISteamUser_UserHasLicenseForApp( VIRTUAL(EUserHasLicenseForAppResult) ISteamUser_UserHasLicenseForApp(
PARAMS(const CSteamID steamID, const AppId_t dlc_id) PARAMS(const CSteamID steamID, const AppId_t dlc_id)
) { ) noexcept {
try { return smoke_api::steam_user::UserHasLicenseForApp(
static const auto app_id = steam_interface::get_app_id(); __func__,
return smoke_api::steam_user::UserHasLicenseForApp( smoke_api::get_app_id(),
__func__, dlc_id,
app_id, SWAPPED_CALL_CLOSURE(ISteamUser_UserHasLicenseForApp, ARGS(steamID, dlc_id))
dlc_id, );
HOOKED_CALL_CLOSURE(ISteamUser_UserHasLicenseForApp, ARGS(steamID, dlc_id))
);
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return k_EUserHasLicenseResultDoesNotHaveLicense;
}
} }

View File

@@ -3,38 +3,63 @@
#include "smoke_api/types.hpp" #include "smoke_api/types.hpp"
// ISteamApps // ISteamApps
VIRTUAL(bool) ISteamApps_BIsSubscribedApp(PARAMS(AppId_t)); VIRTUAL(bool) ISteamApps_BIsSubscribedApp(PARAMS(AppId_t)) noexcept;
VIRTUAL(bool) ISteamApps_BIsDlcInstalled(PARAMS(AppId_t)); VIRTUAL(bool) ISteamApps_BIsDlcInstalled(PARAMS(AppId_t)) noexcept;
VIRTUAL(int) ISteamApps_GetDLCCount(PARAMS()); VIRTUAL(int) ISteamApps_GetDLCCount(PARAMS()) noexcept;
VIRTUAL(bool) ISteamApps_BGetDLCDataByIndex(PARAMS(int, AppId_t*, bool*, char*, int)); VIRTUAL(bool) ISteamApps_BGetDLCDataByIndex(PARAMS(int, AppId_t*, bool*, char*, int)) noexcept;
// ISteamClient // ISteamClient
VIRTUAL(void*) ISteamClient_GetISteamApps(PARAMS(HSteamUser, HSteamPipe, const char*)); VIRTUAL(void*) ISteamClient_GetISteamApps(PARAMS(HSteamUser, HSteamPipe, const char*)) noexcept;
VIRTUAL(void*) ISteamClient_GetISteamUser(PARAMS(HSteamUser, HSteamPipe, const char*)); VIRTUAL(void*) ISteamClient_GetISteamUser(PARAMS(HSteamUser, HSteamPipe, const char*)) noexcept;
VIRTUAL(void*) ISteamClient_GetISteamGenericInterface(PARAMS(HSteamUser, HSteamPipe, const char*)); VIRTUAL(void*) ISteamClient_GetISteamGenericInterface(
VIRTUAL(void*) ISteamClient_GetISteamInventory(PARAMS(HSteamUser, HSteamPipe, const char*)); PARAMS(HSteamUser, HSteamPipe, const char*)
) noexcept;
VIRTUAL(void*) ISteamClient_GetISteamInventory(
PARAMS(HSteamUser, HSteamPipe, const char*)
) noexcept;
// ISteamHTTP
VIRTUAL(bool) ISteamHTTP_GetHTTPResponseBodyData(
PARAMS(HTTPRequestHandle, const uint8_t*, uint32_t)
) noexcept;
VIRTUAL(bool) ISteamHTTP_GetHTTPStreamingResponseBodyData(
PARAMS(HTTPRequestHandle, uint32_t, const uint8_t*, uint32_t)
) noexcept;
VIRTUAL(bool) ISteamHTTP_SetHTTPRequestRawPostBody(
PARAMS(HTTPRequestHandle, const char*, const uint8_t*, uint32_t)
) noexcept;
// ISteamInventory // ISteamInventory
VIRTUAL(EResult) ISteamInventory_GetResultStatus(PARAMS(SteamInventoryResult_t)); VIRTUAL(EResult) ISteamInventory_GetResultStatus(PARAMS(SteamInventoryResult_t)) noexcept;
VIRTUAL(bool) ISteamInventory_GetResultItems( VIRTUAL(bool) ISteamInventory_GetResultItems(
PARAMS(SteamInventoryResult_t, SteamItemDetails_t*, uint32_t*) // @formatter:off PARAMS(SteamInventoryResult_t, SteamItemDetails_t*, uint32_t*) // @formatter:off
); // @formatter:on ) noexcept; // @formatter:on
VIRTUAL(bool) ISteamInventory_GetResultItemProperty( VIRTUAL(bool) ISteamInventory_GetResultItemProperty(
PARAMS(SteamInventoryResult_t, uint32_t, const char*, char*, uint32_t*) // @formatter:off PARAMS(SteamInventoryResult_t, uint32_t, const char*, char*, uint32_t*) // @formatter:off
); // @formatter:on ) noexcept; // @formatter:on
VIRTUAL(bool) ISteamInventory_GetAllItems(PARAMS(SteamInventoryResult_t*)); VIRTUAL(bool) ISteamInventory_GetAllItems(PARAMS(SteamInventoryResult_t*)) noexcept;
VIRTUAL(bool) ISteamInventory_GetItemsByID( VIRTUAL(bool) ISteamInventory_GetItemsByID(
PARAMS(SteamInventoryResult_t*, const SteamItemInstanceID_t*, uint32_t) PARAMS(SteamInventoryResult_t*, const SteamItemInstanceID_t*, uint32_t)
); ) noexcept;
VIRTUAL(bool) ISteamInventory_SerializeResult(PARAMS(SteamInventoryResult_t, void*, uint32_t*)); VIRTUAL(bool) ISteamInventory_SerializeResult(
VIRTUAL(bool) ISteamInventory_GetItemDefinitionIDs(PARAMS(SteamItemDef_t*, uint32_t*)); PARAMS(SteamInventoryResult_t, void*, uint32_t*)
VIRTUAL(bool) ISteamInventory_CheckResultSteamID(PARAMS(SteamInventoryResult_t, CSteamID));
) noexcept;
VIRTUAL(bool) ISteamInventory_GetItemDefinitionIDs(PARAMS(SteamItemDef_t*, uint32_t*)) noexcept;
VIRTUAL(bool) ISteamInventory_CheckResultSteamID(PARAMS(SteamInventoryResult_t, CSteamID)) noexcept;
// ISteamUser // ISteamUser
VIRTUAL(EUserHasLicenseForAppResult) ISteamUser_UserHasLicenseForApp(PARAMS(CSteamID, AppId_t)); VIRTUAL(EUserHasLicenseForAppResult) ISteamUser_UserHasLicenseForApp(
PARAMS(CSteamID, AppId_t)
) noexcept;
// ISteamGameServer // ISteamGameServer
VIRTUAL(EUserHasLicenseForAppResult) ISteamGameServer_UserHasLicenseForApp( VIRTUAL(EUserHasLicenseForAppResult) ISteamGameServer_UserHasLicenseForApp(
PARAMS(CSteamID, AppId_t) PARAMS(CSteamID, AppId_t)
); ) noexcept;

View File

@@ -1,16 +0,0 @@
#include "smoke_api/steamclient/steamclient.hpp"
#include "smoke_api.hpp"
#include "smoke_api/types.hpp"
#include "steam_api/steam_client.hpp"
/**
* SmokeAPI implementation
*/
C_DECL(void*) CreateInterface(const char* interface_version, int* out_result) {
return steam_client::GetGenericInterface(
__func__,
interface_version,
HOOKED_CALL_CLOSURE(CreateInterface, interface_version, out_result)
);
}

View File

@@ -0,0 +1,27 @@
#include <mutex>
#include <koalabox/hook.hpp>
#include <koalabox/logger.hpp>
#include "smoke_api/steamclient/steamclient.hpp"
#include "smoke_api/types.hpp"
#include "steam_api/steam_client.hpp"
/**
* SmokeAPI implementation
*/
C_DECL(void*) CreateInterface(const char* interface_version, create_interface_result* out_result) {
// Mutex here helps us detect unintended recursion early on by throwing an exception.
static std::mutex section;
const std::lock_guard lock(section);
return steam_client::GetGenericInterface(
__func__,
interface_version,
[&] {
static const auto CreateInterface$ = KB_HOOK_GET_HOOKED_FN(CreateInterface);
return CreateInterface$(interface_version, out_result);
}
);
}

View File

@@ -10,21 +10,18 @@ namespace smoke_api::config {
LOCKED, LOCKED,
}; };
NLOHMANN_JSON_SERIALIZE_ENUM( // @formatter:off
AppStatus, NLOHMANN_JSON_SERIALIZE_ENUM(AppStatus, {
// @formatter:off { AppStatus::UNDEFINED, nullptr },
{ { AppStatus::ORIGINAL, "original" },
{ AppStatus::UNDEFINED, nullptr }, { AppStatus::UNLOCKED, "unlocked" },
{ AppStatus::ORIGINAL, "original" }, { AppStatus::LOCKED, "locked" },
{ AppStatus::UNLOCKED, "unlocked" }, }) // @formatter:on
{ AppStatus::LOCKED, "locked" },
}
// @formatter:on
)
struct Config { struct Config {
uint32_t $version = 3; uint32_t $version = 4;
bool logging = false; bool logging = false;
bool log_steam_http = false;
AppStatus default_app_status = AppStatus::UNLOCKED; AppStatus default_app_status = AppStatus::UNLOCKED;
std::map<std::string, AppStatus> override_app_status; std::map<std::string, AppStatus> override_app_status;
std::map<std::string, AppStatus> override_dlc_status; std::map<std::string, AppStatus> override_dlc_status;
@@ -32,10 +29,11 @@ namespace smoke_api::config {
bool auto_inject_inventory = true; bool auto_inject_inventory = true;
std::vector<uint32_t> extra_inventory_items; std::vector<uint32_t> extra_inventory_items;
NLOHMANN_DEFINE_TYPE_INTRUSIVE( NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(
Config, Config,
$version, $version,
logging, logging,
log_steam_http,
default_app_status, default_app_status,
override_app_status, override_app_status,
override_dlc_status, override_dlc_status,

View File

@@ -85,7 +85,7 @@ namespace smoke_api::steam_apps {
const AppId_t app_id, const AppId_t app_id,
const AppId_t dlc_id, const AppId_t dlc_id,
const std::function<bool()>& original_function const std::function<bool()>& original_function
) { ) noexcept {
try { try {
const auto unlocked = config::is_dlc_unlocked( const auto unlocked = config::is_dlc_unlocked(
app_id, app_id,
@@ -103,7 +103,7 @@ namespace smoke_api::steam_apps {
return unlocked; return unlocked;
} catch(const std::exception& e) { } catch(const std::exception& e) {
LOG_ERROR("Uncaught exception: {}", e.what()); LOG_ERROR("{} -> Uncaught exception: {}", function_name, e.what());
return false; return false;
} }
} }
@@ -112,7 +112,7 @@ namespace smoke_api::steam_apps {
const std::string& function_name, const std::string& function_name,
const AppId_t app_id, const AppId_t app_id,
const std::function<int()>& original_function const std::function<int()>& original_function
) { ) noexcept {
try { try {
const auto total_count = [&](int count) { const auto total_count = [&](int count) {
LOG_INFO("{} -> Responding with DLC count: {}", function_name, count); LOG_INFO("{} -> Responding with DLC count: {}", function_name, count);
@@ -139,7 +139,7 @@ namespace smoke_api::steam_apps {
return total_count(static_cast<int>(app_dlcs[app_id].size())); return total_count(static_cast<int>(app_dlcs[app_id].size()));
} catch(const std::exception& e) { } catch(const std::exception& e) {
LOG_ERROR("Uncaught exception: {}", function_name, e.what()); LOG_ERROR("{} -> Uncaught exception: {}", function_name, e.what());
return 0; return 0;
} }
} }
@@ -153,8 +153,8 @@ namespace smoke_api::steam_apps {
char* pchName, char* pchName,
const int cchNameBufferSize, const int cchNameBufferSize,
const std::function<bool()>& original_function, const std::function<bool()>& original_function,
const std::function<bool(AppId_t)>& is_originally_unlocked const std::function<bool()>& is_originally_unlocked
) { ) noexcept {
try { try {
LOG_DEBUG("{} -> {}index: {:>3}", function_name, get_app_id_log(app_id), iDLC); LOG_DEBUG("{} -> {}index: {:>3}", function_name, get_app_id_log(app_id), iDLC);
@@ -174,13 +174,7 @@ namespace smoke_api::steam_apps {
const auto output_dlc = [&](const DLC& dlc) { const auto output_dlc = [&](const DLC& dlc) {
// Fill the output pointers // Fill the output pointers
*pDlcId = dlc.get_id(); *pDlcId = dlc.get_id();
*pbAvailable = config::is_dlc_unlocked( *pbAvailable = config::is_dlc_unlocked(app_id, *pDlcId, is_originally_unlocked);
app_id,
*pDlcId,
[&] {
return is_originally_unlocked(*pDlcId);
}
);
auto name = dlc.get_name(); auto name = dlc.get_name();
name = name.substr(0, cchNameBufferSize + 1); name = name.substr(0, cchNameBufferSize + 1);

View File

@@ -8,13 +8,13 @@ namespace smoke_api::steam_apps {
AppId_t app_id, AppId_t app_id,
AppId_t dlc_id, AppId_t dlc_id,
const std::function<bool()>& original_function const std::function<bool()>& original_function
); ) noexcept;
int GetDLCCount( int GetDLCCount(
const std::string& function_name, const std::string& function_name,
AppId_t app_id, AppId_t app_id,
const std::function<int()>& original_function const std::function<int()>& original_function
); ) noexcept;
bool GetDLCDataByIndex( bool GetDLCDataByIndex(
const std::string& function_name, const std::string& function_name,
@@ -25,6 +25,6 @@ namespace smoke_api::steam_apps {
char* pchName, char* pchName,
int cchNameBufferSize, int cchNameBufferSize,
const std::function<bool()>& original_function, const std::function<bool()>& original_function,
const std::function<bool(AppId_t)>& is_originally_unlocked const std::function<bool()>& is_originally_unlocked
); ) noexcept;
} }

View File

@@ -0,0 +1,115 @@
#include <koalabox/logger.hpp>
#include "smoke_api/interfaces/steam_http.hpp"
#include "smoke_api/config.hpp"
namespace smoke_api::steam_http {
bool GetHTTPResponseBodyData(
const std::string& function_name,
const HTTPRequestHandle hRequest,
const uint8_t* pBodyDataBuffer,
const uint32_t unBufferSize,
const std::function<bool()>& original_function
) noexcept {
try {
const auto result = original_function();
if(config::instance.log_steam_http) {
const std::string_view buffer =
pBodyDataBuffer && unBufferSize
? std::string_view(
reinterpret_cast<const char*>(pBodyDataBuffer),
unBufferSize
)
: "";
LOG_INFO(
"{} -> handle: {}, size: {}, buffer:\n{}",
function_name,
hRequest,
unBufferSize,
buffer
);
}
return result;
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return false;
}
}
bool GetHTTPStreamingResponseBodyData(
const std::string& function_name,
const HTTPRequestHandle hRequest,
const uint32_t cOffset,
const uint8_t* pBodyDataBuffer,
const uint32_t unBufferSize,
const std::function<bool()>& original_function
) noexcept {
try {
const auto result = original_function();
if(config::instance.log_steam_http) {
const std::string_view buffer =
pBodyDataBuffer && unBufferSize
? std::string_view(
reinterpret_cast<const char*>(pBodyDataBuffer),
unBufferSize
)
: "";
LOG_INFO(
"{} -> handle: {}, offset: {}, size: {}, buffer:\n{}",
function_name,
hRequest,
cOffset,
unBufferSize,
buffer
);
}
return result;
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return false;
}
}
bool SetHTTPRequestRawPostBody(
const std::string& function_name,
const HTTPRequestHandle hRequest,
const char* pchContentType,
const uint8_t* pubBody,
const uint32_t unBodyLen,
const std::function<bool()>& original_function
) noexcept {
try {
const auto result = original_function();
if(config::instance.log_steam_http) {
const std::string_view content_type =
pchContentType ? pchContentType : "smoke_api::N/A";
const std::string_view buffer =
pubBody && unBodyLen
? std::string_view(reinterpret_cast<const char*>(pubBody), unBodyLen)
: "";
LOG_INFO(
"{} -> handle: {}, content-type: {}, size: {}, buffer:\n{}",
function_name,
hRequest,
content_type,
unBodyLen,
buffer
);
}
return result;
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", __func__, e.what());
return false;
}
}
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include "smoke_api/types.hpp"
namespace smoke_api::steam_http {
bool GetHTTPResponseBodyData(
const std::string& function_name,
HTTPRequestHandle hRequest,
const uint8_t* pBodyDataBuffer,
uint32_t unBufferSize,
const std::function<bool()>& original_function
) noexcept;
bool GetHTTPStreamingResponseBodyData(
const std::string& function_name,
HTTPRequestHandle hRequest,
uint32_t cOffset,
const uint8_t* pBodyDataBuffer,
uint32_t unBufferSize,
const std::function<bool()>& original_function
) noexcept;
bool SetHTTPRequestRawPostBody(
const std::string& function_name,
HTTPRequestHandle hRequest,
const char* pchContentType,
const uint8_t* pubBody,
uint32_t unBodyLen,
const std::function<bool()>& original_function
) noexcept;
}

View File

@@ -8,17 +8,22 @@ namespace smoke_api::steam_inventory {
const std::string& function_name, const std::string& function_name,
const SteamInventoryResult_t resultHandle, const SteamInventoryResult_t resultHandle,
const std::function<EResult()>& original_function const std::function<EResult()>& original_function
) { ) noexcept {
const auto status = original_function(); try {
const auto status = original_function();
LOG_DEBUG( LOG_DEBUG(
"{} -> handle: {}, status: {}", "{} -> handle: {}, status: {}",
function_name, function_name,
resultHandle, resultHandle,
static_cast<int>(status) static_cast<int>(status)
); );
return status; return status;
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", function_name, e.what());
return EResult::k_EResultFail;
}
} }
bool GetResultItems( bool GetResultItems(
@@ -28,107 +33,112 @@ namespace smoke_api::steam_inventory {
uint32_t* punOutItemsArraySize, uint32_t* punOutItemsArraySize,
const std::function<bool()>& original_function, const std::function<bool()>& original_function,
const std::function<bool(SteamItemDef_t*, uint32_t*)>& get_item_definition_ids const std::function<bool(SteamItemDef_t*, uint32_t*)>& get_item_definition_ids
) { ) noexcept {
static std::mutex section; try {
const std::lock_guard guard(section); static std::mutex section;
const std::lock_guard guard(section);
const auto success = original_function(); const auto success = original_function();
auto print_item = [](const std::string& tag, const SteamItemDetails_t& item) { auto print_item = [](const std::string& tag, const SteamItemDetails_t& item) {
LOG_DEBUG( LOG_DEBUG(
" [{}] definitionId: {}, itemId: {}, quantity: {}, flags: {}", " [{}] definitionId: {}, itemId: {}, quantity: {}, flags: {}",
tag, tag,
item.m_iDefinition, item.m_iDefinition,
item.m_itemId, item.m_itemId,
item.m_unQuantity, item.m_unQuantity,
item.m_unFlags item.m_unFlags
); );
};
if(not success) {
LOG_DEBUG("{} -> original result is false", function_name);
return success;
}
if(punOutItemsArraySize == nullptr) {
LOG_ERROR("{} -> arraySize pointer is null", function_name);
return success;
}
LOG_DEBUG(
"{} -> handle: {}, pOutItemsArray: {}, arraySize: {}",
function_name,
resultHandle,
reinterpret_cast<void*>(pOutItemsArray),
*punOutItemsArraySize
);
static uint32_t original_count = 0;
const auto injected_count = config::instance.extra_inventory_items.size();
// Automatically get inventory items from steam
static std::vector<SteamItemDef_t> auto_inventory_items;
if(config::instance.auto_inject_inventory) {
static std::once_flag inventory_inject_flag;
std::call_once(
inventory_inject_flag,
[&] {
uint32_t count = 0;
if(get_item_definition_ids(nullptr, &count)) {
auto_inventory_items.resize(count);
get_item_definition_ids(auto_inventory_items.data(), &count);
}
}
);
}
const auto auto_injected_count = auto_inventory_items.size();
if(not pOutItemsArray) {
// If pOutItemsArray is NULL then we must set the array size.
original_count = *punOutItemsArraySize;
*punOutItemsArraySize += auto_injected_count + injected_count;
LOG_DEBUG(
"{} -> Original count: {}, Total count: {}",
function_name,
original_count,
*punOutItemsArraySize
);
} else {
// Otherwise, we modify the array
for(int i = 0; i < original_count; i++) {
print_item("original", pOutItemsArray[i]);
}
static auto new_item = [](SteamItemDef_t id) {
return SteamItemDetails_t{
.m_itemId = id,
.m_iDefinition = id,
.m_unQuantity = 1,
.m_unFlags = 0,
};
}; };
for(int i = 0; i < auto_injected_count; i++) { if(not success) {
auto& item = pOutItemsArray[original_count + i]; LOG_DEBUG("{} -> original result is false", function_name);
const auto item_def_id = auto_inventory_items[i]; return success;
item = new_item(item_def_id);
print_item("auto-injected", item);
} }
for(int i = 0; i < injected_count; i++) { if(punOutItemsArraySize == nullptr) {
auto& item = pOutItemsArray[original_count + auto_injected_count + i]; LOG_ERROR("{} -> arraySize pointer is null", function_name);
const auto item_def_id = config::instance.extra_inventory_items[i]; return success;
item = new_item(item_def_id);
print_item("injected", item);
} }
LOG_DEBUG(
"{} -> handle: {}, pOutItemsArray: {}, arraySize: {}",
function_name,
resultHandle,
reinterpret_cast<void*>(pOutItemsArray),
*punOutItemsArraySize
);
static uint32_t original_count = 0;
const auto injected_count = config::instance.extra_inventory_items.size();
// Automatically get inventory items from steam
static std::vector<SteamItemDef_t> auto_inventory_items;
if(config::instance.auto_inject_inventory) {
static std::once_flag inventory_inject_flag;
std::call_once(
inventory_inject_flag,
[&] {
uint32_t count = 0;
if(get_item_definition_ids(nullptr, &count)) {
auto_inventory_items.resize(count);
get_item_definition_ids(auto_inventory_items.data(), &count);
}
}
);
}
const auto auto_injected_count = auto_inventory_items.size();
if(not pOutItemsArray) {
// If pOutItemsArray is NULL then we must set the array size.
original_count = *punOutItemsArraySize;
*punOutItemsArraySize += auto_injected_count + injected_count;
LOG_DEBUG(
"{} -> Original count: {}, Total count: {}",
function_name,
original_count,
*punOutItemsArraySize
);
} else {
// Otherwise, we modify the array
for(int i = 0; i < original_count; i++) {
print_item("original", pOutItemsArray[i]);
}
static auto new_item = [](SteamItemDef_t id) {
return SteamItemDetails_t{
.m_itemId = id,
.m_iDefinition = id,
.m_unQuantity = 1,
.m_unFlags = 0,
};
};
for(int i = 0; i < auto_injected_count; i++) {
auto& item = pOutItemsArray[original_count + i];
const auto item_def_id = auto_inventory_items[i];
item = new_item(item_def_id);
print_item("auto-injected", item);
}
for(int i = 0; i < injected_count; i++) {
auto& item = pOutItemsArray[original_count + auto_injected_count + i];
const auto item_def_id = config::instance.extra_inventory_items[i];
item = new_item(item_def_id);
print_item("injected", item);
}
}
return success;
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", function_name, e.what());
return false;
} }
return success;
} }
bool GetResultItemProperty( bool GetResultItemProperty(
@@ -139,47 +149,61 @@ namespace smoke_api::steam_inventory {
const char* pchValueBuffer, const char* pchValueBuffer,
const uint32_t* punValueBufferSizeOut, const uint32_t* punValueBufferSizeOut,
const std::function<bool()>& original_function const std::function<bool()>& original_function
) { ) noexcept {
LOG_DEBUG( try {
"{} -> Handle: {}, Index: {}, Name: '{}'", LOG_DEBUG(
function_name, "{} -> Handle: {}, Index: {}, Name: '{}'",
resultHandle, function_name,
unItemIndex, resultHandle,
// can be empty, in which case steam responds with property list in csv format unItemIndex,
pchPropertyName ? pchPropertyName : "nullptr" // can be empty, in which case steam responds with property list in csv format
); pchPropertyName ? pchPropertyName : "nullptr"
);
const auto success = original_function(); const auto success = original_function();
if(!success) { if(!success) {
LOG_WARN("{} -> Result is false", function_name); LOG_WARN("{} -> Result is false", function_name);
return false;
}
if(
pchValueBuffer && *pchValueBuffer &&
punValueBufferSizeOut && *punValueBufferSizeOut > 0
) {
LOG_DEBUG(
R"('{}' -> Buffer: "{}")",
function_name,
std::string(pchValueBuffer, *punValueBufferSizeOut - 1)
);
}
return success;
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", function_name, e.what());
return false; return false;
} }
if(
pchValueBuffer && *pchValueBuffer &&
punValueBufferSizeOut && *punValueBufferSizeOut > 0
) {
LOG_DEBUG(
R"({} -> Buffer: "{}")",
function_name,
std::string(pchValueBuffer, *punValueBufferSizeOut - 1)
);
}
return success;
} }
bool GetAllItems( bool GetAllItems(
const std::string& function_name, const std::string& function_name,
const SteamInventoryResult_t* pResultHandle, const SteamInventoryResult_t* pResultHandle,
const std::function<bool()>& original_function const std::function<bool()>& original_function
) { ) noexcept {
const auto success = original_function(); try {
const auto success = original_function();
LOG_DEBUG("{} -> Handle: {}", function_name, fmt::ptr(pResultHandle)); LOG_DEBUG(
"{} -> Handle: {}",
function_name,
reinterpret_cast<const void*>(pResultHandle)
);
return success; return success;
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", function_name, e.what());
return false;
}
} }
bool GetItemsByID( bool GetItemsByID(
@@ -188,18 +212,23 @@ namespace smoke_api::steam_inventory {
const SteamItemInstanceID_t* pInstanceIDs, const SteamItemInstanceID_t* pInstanceIDs,
const uint32_t unCountInstanceIDs, const uint32_t unCountInstanceIDs,
const std::function<bool()>& original_function const std::function<bool()>& original_function
) { ) noexcept {
const auto success = original_function(); try {
const auto success = original_function();
LOG_DEBUG("{} -> Handle: {}", function_name, fmt::ptr(pResultHandle)); LOG_DEBUG("{} -> Handle: {}", function_name, fmt::ptr(pResultHandle));
if(success && pInstanceIDs != nullptr) { if(success && pInstanceIDs != nullptr) {
for(int i = 0; i < unCountInstanceIDs; i++) { for(int i = 0; i < unCountInstanceIDs; i++) {
LOG_DEBUG(" Index: {}, ItemId: {}", i, pInstanceIDs[i]); LOG_DEBUG(" Index: {}, ItemId: {}", i, pInstanceIDs[i]);
}
} }
}
return success; return success;
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", function_name, e.what());
return false;
}
} }
bool SerializeResult( bool SerializeResult(
@@ -208,22 +237,27 @@ namespace smoke_api::steam_inventory {
void* pOutBuffer, void* pOutBuffer,
uint32_t* punOutBufferSize, uint32_t* punOutBufferSize,
const std::function<bool()>& original_function const std::function<bool()>& original_function
) { ) noexcept {
const auto success = original_function(); try {
const auto success = original_function();
if(pOutBuffer != nullptr) { if(pOutBuffer != nullptr) {
std::string buffer(static_cast<char*>(pOutBuffer), *punOutBufferSize); std::string buffer(static_cast<char*>(pOutBuffer), *punOutBufferSize);
LOG_DEBUG("{} -> Handle: {}, Buffer: '{}'", function_name, resultHandle, buffer); LOG_DEBUG("{} -> Handle: {}, Buffer: '{}'", function_name, resultHandle, buffer);
} else { } else {
LOG_DEBUG( LOG_DEBUG(
"{} -> Handle: {}, Size: '{}'", "{} -> Handle: {}, Size: '{}'",
function_name, function_name,
resultHandle, resultHandle,
*punOutBufferSize *punOutBufferSize
); );
}
return success;
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", function_name, e.what());
return false;
} }
return success;
} }
bool GetItemDefinitionIDs( bool GetItemDefinitionIDs(
@@ -231,28 +265,33 @@ namespace smoke_api::steam_inventory {
const SteamItemDef_t* pItemDefIDs, const SteamItemDef_t* pItemDefIDs,
uint32_t* punItemDefIDsArraySize, uint32_t* punItemDefIDsArraySize,
const std::function<bool()>& original_function const std::function<bool()>& original_function
) { ) noexcept {
const auto success = original_function(); try {
const auto success = original_function();
if(!success) { if(!success) {
LOG_WARN("{} -> Result is false", function_name); LOG_WARN("{} -> Result is false", function_name);
return false;
}
if(punItemDefIDsArraySize) {
LOG_DEBUG("{} -> Size: {}", function_name, *punItemDefIDsArraySize);
} else {
return success;
}
if(pItemDefIDs) { // Definitions were copied
for(int i = 0; i < *punItemDefIDsArraySize; i++) {
const auto& def = pItemDefIDs[i];
LOG_DEBUG(" Index: {}, ID: {}", i, def);
}
}
return success;
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", function_name, e.what());
return false; return false;
} }
if(punItemDefIDsArraySize) {
LOG_DEBUG("{} -> Size: {}", function_name, *punItemDefIDsArraySize);
} else {
return success;
}
if(pItemDefIDs) { // Definitions were copied
for(int i = 0; i < *punItemDefIDsArraySize; i++) {
const auto& def = pItemDefIDs[i];
LOG_DEBUG(" Index: {}, ID: {}", i, def);
}
}
return success;
} }
bool CheckResultSteamID( bool CheckResultSteamID(
@@ -260,17 +299,22 @@ namespace smoke_api::steam_inventory {
SteamInventoryResult_t resultHandle, SteamInventoryResult_t resultHandle,
CSteamID steamIDExpected, CSteamID steamIDExpected,
const std::function<bool()>& original_function const std::function<bool()>& original_function
) { ) noexcept {
const auto result = original_function(); try {
const auto result = original_function();
LOG_DEBUG( LOG_DEBUG(
"{} -> handle: {}, steamID: {}, original result: {}", "{} -> handle: {}, steamID: {}, original result: {}",
function_name, function_name,
resultHandle, resultHandle,
steamIDExpected, steamIDExpected,
result result
); );
return true; return true;
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", function_name, e.what());
return false;
}
} }
} }

View File

@@ -7,7 +7,7 @@ namespace smoke_api::steam_inventory {
const std::string& function_name, const std::string& function_name,
SteamInventoryResult_t resultHandle, SteamInventoryResult_t resultHandle,
const std::function<EResult()>& original_function const std::function<EResult()>& original_function
); ) noexcept;
bool GetResultItems( bool GetResultItems(
const std::string& function_name, const std::string& function_name,
@@ -16,7 +16,7 @@ namespace smoke_api::steam_inventory {
uint32_t* punOutItemsArraySize, uint32_t* punOutItemsArraySize,
const std::function<bool()>& original_function, const std::function<bool()>& original_function,
const std::function<bool(SteamItemDef_t*, uint32_t*)>& get_item_definition_ids const std::function<bool(SteamItemDef_t*, uint32_t*)>& get_item_definition_ids
); ) noexcept;
bool GetResultItemProperty( bool GetResultItemProperty(
const std::string& function_name, const std::string& function_name,
@@ -26,13 +26,13 @@ namespace smoke_api::steam_inventory {
const char* pchValueBuffer, const char* pchValueBuffer,
const uint32_t* punValueBufferSizeOut, const uint32_t* punValueBufferSizeOut,
const std::function<bool()>& original_function const std::function<bool()>& original_function
); ) noexcept;
bool GetAllItems( bool GetAllItems(
const std::string& function_name, const std::string& function_name,
const SteamInventoryResult_t* pResultHandle, const SteamInventoryResult_t* pResultHandle,
const std::function<bool()>& original_function const std::function<bool()>& original_function
); ) noexcept;
bool GetItemsByID( bool GetItemsByID(
const std::string& function_name, const std::string& function_name,
@@ -40,7 +40,7 @@ namespace smoke_api::steam_inventory {
const SteamItemInstanceID_t* pInstanceIDs, const SteamItemInstanceID_t* pInstanceIDs,
uint32_t unCountInstanceIDs, uint32_t unCountInstanceIDs,
const std::function<bool()>& original_function const std::function<bool()>& original_function
); ) noexcept;
bool SerializeResult( bool SerializeResult(
const std::string& function_name, const std::string& function_name,
@@ -48,19 +48,19 @@ namespace smoke_api::steam_inventory {
void* pOutBuffer, void* pOutBuffer,
uint32_t* punOutBufferSize, uint32_t* punOutBufferSize,
const std::function<bool()>& original_function const std::function<bool()>& original_function
); ) noexcept;
bool GetItemDefinitionIDs( bool GetItemDefinitionIDs(
const std::string& function_name, const std::string& function_name,
const SteamItemDef_t* pItemDefIDs, const SteamItemDef_t* pItemDefIDs,
uint32_t* punItemDefIDsArraySize, uint32_t* punItemDefIDsArraySize,
const std::function<bool()>& original_function const std::function<bool()>& original_function
); ) noexcept;
bool CheckResultSteamID( bool CheckResultSteamID(
const std::string& function_name, const std::string& function_name,
SteamInventoryResult_t resultHandle, SteamInventoryResult_t resultHandle,
CSteamID steamIDExpected, CSteamID steamIDExpected,
const std::function<bool()>& original_function const std::function<bool()>& original_function
); ) noexcept;
} }

View File

@@ -9,26 +9,31 @@ namespace smoke_api::steam_user {
const AppId_t appId, const AppId_t appId,
const AppId_t dlcId, const AppId_t dlcId,
const std::function<EUserHasLicenseForAppResult()>& original_function const std::function<EUserHasLicenseForAppResult()>& original_function
) { ) noexcept {
const auto result = original_function(); try {
const auto result = original_function();
if(result == k_EUserHasLicenseResultNoAuth) { if(result == k_EUserHasLicenseResultNoAuth) {
LOG_WARN("{} -> App ID: {:>8}, Result: NoAuth", function_name, dlcId); LOG_WARN("{} -> App ID: {:>8}, Result: NoAuth", function_name, dlcId);
return result; return result;
}
const auto has_license = config::is_dlc_unlocked(
appId,
dlcId,
[&] {
return result == k_EUserHasLicenseResultHasLicense;
} }
);
LOG_INFO("{} -> App ID: {:>8}, HasLicense: {}", function_name, dlcId, has_license); const auto has_license = config::is_dlc_unlocked(
appId,
dlcId,
[&] {
return result == k_EUserHasLicenseResultHasLicense;
}
);
return has_license LOG_INFO("{} -> App ID: {:>8}, HasLicense: {}", function_name, dlcId, has_license);
? k_EUserHasLicenseResultHasLicense
: k_EUserHasLicenseResultDoesNotHaveLicense; return has_license
? k_EUserHasLicenseResultHasLicense
: k_EUserHasLicenseResultDoesNotHaveLicense;
} catch(const std::exception& e) {
LOG_ERROR("{} -> Error: {}", function_name, e.what());
return k_EUserHasLicenseResultDoesNotHaveLicense;
}
} }
} }

View File

@@ -8,5 +8,5 @@ namespace smoke_api::steam_user {
AppId_t appId, AppId_t appId,
AppId_t dlcId, AppId_t dlcId,
const std::function<EUserHasLicenseForAppResult()>& original_function const std::function<EUserHasLicenseForAppResult()>& original_function
); ) noexcept;
} }

View File

@@ -2,4 +2,13 @@
#include "smoke_api/types.hpp" #include "smoke_api/types.hpp"
C_DECL(void*) CreateInterface(const char* interface_version, int* out_result); enum class create_interface_result {
Success = 0,
Error = 1,
};
/**
* @param interface_version Example: STEAMAPPS_INTERFACE_VERSION008
* @param out_result Pointer to the result enum that will be written to. Can be nullptr.
*/
C_DECL(void*) CreateInterface(const char* interface_version, create_interface_result* out_result);

View File

@@ -4,9 +4,9 @@ std::vector<DLC> DLC::get_dlcs_from_apps(const AppDlcNameMap& apps, const AppId_
std::vector<DLC> dlcs; std::vector<DLC> dlcs;
if(const auto app_id_str = std::to_string(app_id); apps.contains(app_id_str)) { if(const auto app_id_str = std::to_string(app_id); apps.contains(app_id_str)) {
const auto& app = apps.at(app_id_str); const auto& [app_dlcs] = apps.at(app_id_str);
for(auto const& [id, name] : app.dlcs) { for(auto const& [id, name] : app_dlcs) {
dlcs.emplace_back(id, name); dlcs.emplace_back(id, name);
} }
} }

View File

@@ -13,12 +13,12 @@
// These macros are meant to be used for callbacks that should return original result // These macros are meant to be used for callbacks that should return original result
#define HOOKED_CALL(FUNC, ...) \ #define SWAPPED_CALL(CLASS, FUNC, ...) \
static const auto _##FUNC = KB_HOOK_GET_HOOKED_FN(FUNC); \ const auto _##FUNC = KB_HOOK_GET_SWAPPED_FN(CLASS, FUNC); \
return _##FUNC(__VA_ARGS__) return _##FUNC(__VA_ARGS__)
#define HOOKED_CALL_CLOSURE(FUNC, ...) \ #define SWAPPED_CALL_CLOSURE(FUNC, ...) \
[&] { HOOKED_CALL(FUNC, __VA_ARGS__); } [&] { SWAPPED_CALL(THIS, FUNC, __VA_ARGS__); }
/** /**
* By default, virtual functions are declared with __thiscall * By default, virtual functions are declared with __thiscall
@@ -44,23 +44,32 @@ return _##FUNC(__VA_ARGS__)
* The macros below implement the above-mentioned considerations. * The macros below implement the above-mentioned considerations.
*/ */
#ifdef _WIN64 #ifdef _WIN64
#define PARAMS(...) const void *RCX, __VA_ARGS__ #define PARAMS(...) const void* RCX, __VA_ARGS__
#define ARGS(...) RCX, __VA_ARGS__ #define ARGS(...) RCX, __VA_ARGS__
#define THIS RCX #define THIS RCX
#define DECLARE_EDX()
#else #else
#define PARAMS(...) const void *ECX, const void *EDX, __VA_ARGS__ #define PARAMS(...) const void* ECX, const void* EDX, __VA_ARGS__
#define ARGS(...) ECX, EDX, __VA_ARGS__ #define ARGS(...) ECX, EDX, __VA_ARGS__
#define THIS ECX #define THIS ECX
#define DECLARE_EDX() const void* EDX = nullptr;
#endif #endif
using AppId_t = uint32_t; using AppId_t = uint32_t;
using HSteamPipe = uint32_t; using HSteamPipe = uint32_t;
using EResult = uint32_t;
using HSteamUser = uint32_t; using HSteamUser = uint32_t;
using SteamInventoryResult_t = uint32_t; using SteamInventoryResult_t = uint32_t;
using SteamItemInstanceID_t = uint64_t; using SteamItemInstanceID_t = uint64_t;
using SteamItemDef_t = uint32_t; using SteamItemDef_t = uint32_t;
using CSteamID = uint64_t; using CSteamID = uint64_t;
using HTTPRequestHandle = uint32_t;
enum class EResult {
k_EResultNone = 0,
k_EResultOK = 1,
k_EResultFail = 2,
// See all at steamclientpublic.h
};
struct SteamItemDetails_t { struct SteamItemDetails_t {
SteamItemInstanceID_t m_itemId; SteamItemInstanceID_t m_itemId;
@@ -71,10 +80,12 @@ struct SteamItemDetails_t {
// results from UserHasLicenseForApp // results from UserHasLicenseForApp
enum EUserHasLicenseForAppResult { enum EUserHasLicenseForAppResult {
k_EUserHasLicenseResultHasLicense = 0, // User has a license for specified app // User has a license for specified app
k_EUserHasLicenseResultDoesNotHaveLicense = 1, k_EUserHasLicenseResultHasLicense = 0,
// User does not have a license for the specified app // User does not have a license for the specified app
k_EUserHasLicenseResultNoAuth = 2, // User has not been authenticated k_EUserHasLicenseResultDoesNotHaveLicense = 1,
// User has not been authenticated
k_EUserHasLicenseResultNoAuth = 2,
}; };
// These aliases exist solely to increase code readability // These aliases exist solely to increase code readability
@@ -87,7 +98,7 @@ using DlcNameMap = std::map<DlcIdKey, DlcNameValue>;
struct App { struct App {
DlcNameMap dlcs; DlcNameMap dlcs;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(App, dlcs) // NOLINT(misc-const-correctness) NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(App, dlcs)
}; };
using AppDlcNameMap = std::map<AppIdKey, App>; using AppDlcNameMap = std::map<AppIdKey, App>;
@@ -98,6 +109,8 @@ class DLC {
std::string name; std::string name;
public: public:
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(DLC, appid, name)
explicit DLC() = default; explicit DLC() = default;
explicit DLC(std::string appid, std::string name) : appid{std::move(appid)}, explicit DLC(std::string appid, std::string name) : appid{std::move(appid)},
@@ -115,8 +128,6 @@ public:
return name; return name;
} }
NLOHMANN_DEFINE_TYPE_INTRUSIVE(DLC, appid, name)
static std::vector<DLC> get_dlcs_from_apps(const AppDlcNameMap& apps, AppId_t app_id); static std::vector<DLC> get_dlcs_from_apps(const AppDlcNameMap& apps, AppId_t app_id);
static DlcNameMap get_dlc_map_from_vector(const std::vector<DLC>& dlcs); static DlcNameMap get_dlc_map_from_vector(const std::vector<DLC>& dlcs);