Reworked hooking

This commit is contained in:
acidicoala
2025-08-28 22:25:11 +05:00
parent 6432eb3ec9
commit 09e1187ab0
19 changed files with 129 additions and 120 deletions

View File

@@ -35,6 +35,8 @@ set(SMOKE_API_STATIC_SOURCES
set(SMOKE_API_SOURCES
${SMOKE_API_STATIC_SOURCES}
src/smoke_api/smoke_api.cpp
src/smoke_api/smoke_api.hpp
src/steam_api/exports/steam_api.cpp
src/steam_api/exports/steam_api.hpp
src/steam_api/exports/steam_api_unversioned.cpp
@@ -51,29 +53,30 @@ set(SMOKE_API_SOURCES
src/steam_api/steam_interface.hpp
src/steamclient/steamclient.cpp
src/main.cpp
src/smoke_api.cpp
src/smoke_api.hpp
)
### 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>"
)
target_link_libraries(SmokeAPI_interface INTERFACE KoalaBox $<TARGET_OBJECTS:KoalaBox>)
target_link_libraries(SmokeAPI_common INTERFACE KoalaBox $<TARGET_OBJECTS:KoalaBox>)
### Static SmokeAPI
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
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})
configure_version_resource(
TARGET SmokeAPI

View File

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

View File

@@ -79,11 +79,9 @@ namespace smoke_api {
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__);
LOG_INFO("{} v{} | Built at '{}'", PROJECT_NAME, PROJECT_VERSION, __TIMESTAMP__);
LOG_DEBUG("Parsed config:\n{}", nlohmann::json(config::instance).dump(2));
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());

View File

@@ -1,6 +1,6 @@
#pragma once
#include <koalabox/hook.hpp>
#include <koalabox/win.hpp>
constexpr auto STEAM_APPS = "STEAMAPPS_INTERFACE_VERSION";
constexpr auto STEAM_CLIENT = "SteamClient";
@@ -13,21 +13,6 @@ constexpr auto STEAM_GAME_SERVER = "SteamGameServer";
// 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;

View File

@@ -1,8 +1,14 @@
#include <koalabox/logger.hpp>
#include "steam_api/exports/steam_api.hpp"
#include "smoke_api.hpp"
#include "smoke_api/config.hpp"
#include "smoke_api/smoke_api.hpp"
#define AUTO_CALL(FUNC, ...) \
static const auto _##FUNC = smoke_api::hook_mode \
? KB_HOOK_GET_HOOKED_FN(FUNC) \
: KB_WIN_GET_PROC(smoke_api::steamapi_module, FUNC); \
return _##FUNC(__VA_ARGS__)
DLL_EXPORT(bool) SteamAPI_RestartAppIfNecessary(const AppId_t unOwnAppID) {
LOG_INFO("{} -> unOwnAppID: {}", __func__, unOwnAppID);

View File

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

View File

@@ -1,10 +1,10 @@
#include <regex>
#include <map>
#include <regex>
#include <koalabox/logger.hpp>
#include <koalabox/win.hpp>
#include "smoke_api.hpp"
#include "smoke_api/smoke_api.hpp"
#include "steam_api/steam_client.hpp"
namespace {
@@ -20,7 +20,10 @@ namespace {
) {
static std::map<std::string, std::string> version_map;
if(not version_map.contains(version_prefix)) {
if(version_map.contains(version_prefix)) {
return version_map.at(version_prefix);
}
try {
const auto section = kb::win::get_pe_section_or_throw(
smoke_api::steamapi_module,
@@ -34,10 +37,9 @@ namespace {
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];
}
} else {
throw std::runtime_error(std::format("No match found for '{}'", version_prefix));
}
} catch(const std::exception& ex) {
LOG_ERROR(
"Failed to get versioned interface: {}."
@@ -48,14 +50,19 @@ namespace {
version_map[version_prefix] = version_prefix + fallback;
}
}
return version_map[version_prefix];
return version_map.at(version_prefix);
}
}
// TODO: Do we really need to proxy them?
#define MODULE_CALL_CLOSURE(FUNC, ...) \
[&] { \
static const auto _##FUNC = KB_WIN_GET_PROC(smoke_api::steamapi_module, FUNC); \
return _##FUNC(__VA_ARGS__); \
}
DLL_EXPORT(void*) SteamApps() {
static auto version = get_versioned_interface(STEAM_APPS, "002");

View File

@@ -8,17 +8,17 @@
#include <koalabox/win.hpp>
#include "steam_api/steam_interface.hpp"
#include "smoke_api.hpp"
#include "smoke_api/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
void* function_address; // e.g. ISteamClient_GetISteamApps
};
struct interface_data {
struct interface_data { // NOLINT(*-exception-escape)
std::string fallback_version; // e.g. "SteamClient021"
std::map<std::string, interface_entry> entry_map;
// e.g. {ENTRY(ISteamClient, GetISteamApps), ...}
@@ -28,7 +28,7 @@ namespace {
#define ENTRY(INTERFACE, FUNC) \
{ \
#FUNC, { \
#INTERFACE "_" #FUNC, reinterpret_cast<uintptr_t>(INTERFACE##_##FUNC) \
#INTERFACE "_" #FUNC, reinterpret_cast<void*>(INTERFACE##_##FUNC) \
} \
}
@@ -137,6 +137,10 @@ namespace steam_interface {
}
}
/**
* @param interface Pointer to the interface
* @param version_string Example: 'SteamClient020'
*/
void hook_virtuals(void* interface, const std::string& version_string) {
if(interface == nullptr) {
// Game has tried to use an interface before initializing steam api
@@ -160,11 +164,16 @@ namespace steam_interface {
static const auto virtual_hook_map = get_virtual_hook_map();
for(const auto& [prefix, data] : virtual_hook_map) {
if(version_string.starts_with(prefix)) {
if(not version_string.starts_with(prefix)) {
continue;
}
const auto& lookup = find_lookup(version_string, data.fallback_version);
for(const auto& [function, entry] : data.entry_map) {
if(lookup.contains(function)) {
if(not lookup.contains(function)) {
continue;
}
kb::hook::swap_virtual_func(
interface,
entry.function_name,
@@ -172,10 +181,8 @@ namespace steam_interface {
entry.function_address
);
}
}
break;
}
}
}
}

View File

@@ -9,7 +9,7 @@ VIRTUAL(bool) ISteamApps_BIsSubscribedApp(PARAMS(const AppId_t dlc_id)) noexcept
__func__,
steam_interface::get_app_id(),
dlc_id,
HOOKED_CALL_CLOSURE(ISteamApps_BIsSubscribedApp, ARGS(dlc_id))
SWAPPED_CALL_CLOSURE(ISteamApps_BIsSubscribedApp, ARGS(dlc_id))
);
}
@@ -18,7 +18,7 @@ VIRTUAL(bool) ISteamApps_BIsDlcInstalled(PARAMS(const AppId_t dlc_id)) noexcept
__func__,
steam_interface::get_app_id(),
dlc_id,
HOOKED_CALL_CLOSURE(ISteamApps_BIsDlcInstalled, ARGS(dlc_id))
SWAPPED_CALL_CLOSURE(ISteamApps_BIsDlcInstalled, ARGS(dlc_id))
);
}
@@ -26,7 +26,7 @@ VIRTUAL(int) ISteamApps_GetDLCCount(PARAMS()) noexcept {
return smoke_api::steam_apps::GetDLCCount(
__func__,
steam_interface::get_app_id(),
HOOKED_CALL_CLOSURE(ISteamApps_GetDLCCount, ARGS())
SWAPPED_CALL_CLOSURE(ISteamApps_GetDLCCount, ARGS())
);
}
@@ -47,11 +47,11 @@ VIRTUAL(bool) ISteamApps_BGetDLCDataByIndex(
pbAvailable,
pchName,
cchNameBufferSize,
HOOKED_CALL_CLOSURE(
SWAPPED_CALL_CLOSURE(
ISteamApps_BGetDLCDataByIndex,
ARGS(iDLC, p_dlc_id, pbAvailable, pchName, cchNameBufferSize)
),
HOOKED_CALL_CLOSURE(
SWAPPED_CALL_CLOSURE(
ISteamApps_BIsSubscribedApp,
ARGS(*p_dlc_id)
)

View File

@@ -13,7 +13,7 @@ VIRTUAL(void*) ISteamClient_GetISteamApps(
return steam_client::GetGenericInterface(
__func__,
version,
HOOKED_CALL_CLOSURE(ISteamClient_GetISteamApps, ARGS(hSteamUser, hSteamPipe, version))
SWAPPED_CALL_CLOSURE(ISteamClient_GetISteamApps, ARGS(hSteamUser, hSteamPipe, version))
);
}
@@ -27,7 +27,7 @@ VIRTUAL(void*) ISteamClient_GetISteamUser(
return steam_client::GetGenericInterface(
__func__,
version,
HOOKED_CALL_CLOSURE(ISteamClient_GetISteamUser, ARGS(hSteamUser, hSteamPipe, version))
SWAPPED_CALL_CLOSURE(ISteamClient_GetISteamUser, ARGS(hSteamUser, hSteamPipe, version))
);
}
@@ -41,7 +41,7 @@ VIRTUAL(void*) ISteamClient_GetISteamGenericInterface(
return steam_client::GetGenericInterface(
__func__,
pchVersion,
HOOKED_CALL_CLOSURE(
SWAPPED_CALL_CLOSURE(
ISteamClient_GetISteamGenericInterface,
ARGS(hSteamUser, hSteamPipe, pchVersion)
)
@@ -58,7 +58,7 @@ VIRTUAL(void*) ISteamClient_GetISteamInventory(
return steam_client::GetGenericInterface(
__func__,
pchVersion,
HOOKED_CALL_CLOSURE(
SWAPPED_CALL_CLOSURE(
ISteamClient_GetISteamInventory,
ARGS(hSteamUser, hSteamPipe, pchVersion)
)

View File

@@ -11,6 +11,6 @@ VIRTUAL(EUserHasLicenseForAppResult) ISteamGameServer_UserHasLicenseForApp(
__func__,
steam_interface::get_app_id(),
dlc_id,
HOOKED_CALL_CLOSURE(ISteamGameServer_UserHasLicenseForApp, ARGS(steamID, dlc_id))
SWAPPED_CALL_CLOSURE(ISteamGameServer_UserHasLicenseForApp, ARGS(steamID, dlc_id))
);
}

View File

@@ -16,7 +16,7 @@ VIRTUAL(bool) ISteamHTTP_GetHTTPResponseBodyData(
hRequest,
pBodyDataBuffer,
unBufferSize,
HOOKED_CALL_CLOSURE(
SWAPPED_CALL_CLOSURE(
ISteamHTTP_GetHTTPResponseBodyData,
ARGS(hRequest, pBodyDataBuffer, unBufferSize)
)
@@ -37,7 +37,7 @@ VIRTUAL(bool) ISteamHTTP_GetHTTPStreamingResponseBodyData(
cOffset,
pBodyDataBuffer,
unBufferSize,
HOOKED_CALL_CLOSURE(
SWAPPED_CALL_CLOSURE(
ISteamHTTP_GetHTTPStreamingResponseBodyData,
ARGS(hRequest, cOffset, pBodyDataBuffer, unBufferSize)
)
@@ -58,7 +58,7 @@ VIRTUAL(bool) ISteamHTTP_SetHTTPRequestRawPostBody(
pchContentType,
pubBody,
unBodyLen,
HOOKED_CALL_CLOSURE(
SWAPPED_CALL_CLOSURE(
ISteamHTTP_SetHTTPRequestRawPostBody,
ARGS(hRequest, pchContentType, pubBody, unBodyLen)
)

View File

@@ -7,7 +7,7 @@ VIRTUAL(EResult) ISteamInventory_GetResultStatus(
return smoke_api::steam_inventory::GetResultStatus(
__func__,
resultHandle,
HOOKED_CALL_CLOSURE(ISteamInventory_GetResultStatus, ARGS(resultHandle))
SWAPPED_CALL_CLOSURE(ISteamInventory_GetResultStatus, ARGS(resultHandle))
);
}
@@ -23,12 +23,13 @@ VIRTUAL(bool) ISteamInventory_GetResultItems(
resultHandle,
pOutItemsArray,
punOutItemsArraySize,
HOOKED_CALL_CLOSURE(
SWAPPED_CALL_CLOSURE(
ISteamInventory_GetResultItems,
ARGS(resultHandle, pOutItemsArray, punOutItemsArraySize)
),
[&](SteamItemDef_t* pItemDefIDs, uint32_t* punItemDefIDsArraySize) {
HOOKED_CALL(
SWAPPED_CALL(
THIS,
ISteamInventory_GetItemDefinitionIDs,
ARGS(pItemDefIDs, punItemDefIDsArraySize)
);
@@ -52,7 +53,7 @@ VIRTUAL(bool) ISteamInventory_GetResultItemProperty(
pchPropertyName,
pchValueBuffer,
punValueBufferSizeOut,
HOOKED_CALL_CLOSURE(
SWAPPED_CALL_CLOSURE(
ISteamInventory_GetResultItemProperty,
ARGS(
resultHandle,
@@ -69,7 +70,7 @@ VIRTUAL(bool) ISteamInventory_GetAllItems(PARAMS(SteamInventoryResult_t* pResult
return smoke_api::steam_inventory::GetAllItems(
__func__,
pResultHandle,
HOOKED_CALL_CLOSURE(ISteamInventory_GetAllItems, ARGS(pResultHandle))
SWAPPED_CALL_CLOSURE(ISteamInventory_GetAllItems, ARGS(pResultHandle))
);
}
@@ -85,7 +86,7 @@ VIRTUAL(bool) ISteamInventory_GetItemsByID(
pResultHandle,
pInstanceIDs,
unCountInstanceIDs,
HOOKED_CALL_CLOSURE(
SWAPPED_CALL_CLOSURE(
ISteamInventory_GetItemsByID,
ARGS(pResultHandle, pInstanceIDs, unCountInstanceIDs)
)
@@ -104,7 +105,7 @@ VIRTUAL(bool) ISteamInventory_SerializeResult(
resultHandle,
pOutBuffer,
punOutBufferSize,
HOOKED_CALL_CLOSURE(
SWAPPED_CALL_CLOSURE(
ISteamInventory_SerializeResult,
ARGS(resultHandle, pOutBuffer, punOutBufferSize)
)
@@ -121,7 +122,7 @@ VIRTUAL(bool) ISteamInventory_GetItemDefinitionIDs(
__func__,
pItemDefIDs,
punItemDefIDsArraySize,
HOOKED_CALL_CLOSURE(
SWAPPED_CALL_CLOSURE(
ISteamInventory_GetItemDefinitionIDs,
ARGS(pItemDefIDs, punItemDefIDsArraySize)
)
@@ -135,6 +136,6 @@ VIRTUAL(bool) ISteamInventory_CheckResultSteamID(
__func__,
resultHandle,
steamIDExpected,
HOOKED_CALL_CLOSURE(ISteamInventory_CheckResultSteamID, ARGS(resultHandle, steamIDExpected))
SWAPPED_CALL_CLOSURE(ISteamInventory_CheckResultSteamID, ARGS(resultHandle, steamIDExpected))
);
}

View File

@@ -11,6 +11,6 @@ VIRTUAL(EUserHasLicenseForAppResult) ISteamUser_UserHasLicenseForApp(
__func__,
steam_interface::get_app_id(),
dlc_id,
HOOKED_CALL_CLOSURE(ISteamUser_UserHasLicenseForApp, ARGS(steamID, dlc_id))
SWAPPED_CALL_CLOSURE(ISteamUser_UserHasLicenseForApp, ARGS(steamID, dlc_id))
);
}

View File

@@ -1,8 +1,10 @@
#include "smoke_api/steamclient/steamclient.hpp"
#include "../smoke_api.hpp"
#include "smoke_api/types.hpp"
#include <koalabox/hook.hpp>
#include "../steam_api/steam_client.hpp"
#include "smoke_api/steamclient/steamclient.hpp"
#include "smoke_api/smoke_api.hpp"
#include "smoke_api/types.hpp"
#include "steam_api/steam_client.hpp"
/**
* SmokeAPI implementation
@@ -11,6 +13,9 @@ 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)
[&] {
static const auto CreateInterface$ = KB_HOOK_GET_HOOKED_FN(CreateInterface);
return CreateInterface$(interface_version, out_result);
}
);
}

View File

@@ -37,6 +37,7 @@ namespace smoke_api::config {
Config,
$version,
logging,
log_steam_http,
default_app_status,
override_app_status,
override_dlc_status,

View File

@@ -4,9 +4,9 @@ std::vector<DLC> DLC::get_dlcs_from_apps(const AppDlcNameMap& apps, const AppId_
std::vector<DLC> dlcs;
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);
}
}

View File

@@ -13,16 +13,12 @@
// These macros are meant to be used for callbacks that should return original result
#define HOOKED_CALL(FUNC, ...) \
static const auto _##FUNC = KB_HOOK_GET_HOOKED_FN(FUNC); \
#define SWAPPED_CALL(CLASS, FUNC, ...) \
const auto _##FUNC = KB_HOOK_GET_SWAPPED_FN(CLASS, FUNC); \
return _##FUNC(__VA_ARGS__)
#define HOOKED_CALL_CLOSURE(FUNC, ...) \
[&] { HOOKED_CALL(FUNC, __VA_ARGS__); }
#define HOOKED_CALL_RESULT(FUNC, ...) \
static const auto _##FUNC = KB_HOOK_GET_HOOKED_FN(FUNC); \
const auto result = _##FUNC(__VA_ARGS__)
#define SWAPPED_CALL_CLOSURE(FUNC, ...) \
[&] { SWAPPED_CALL(THIS, FUNC, __VA_ARGS__); }
/**
* By default, virtual functions are declared with __thiscall