7 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
23 changed files with 268 additions and 374 deletions

View File

@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.24) cmake_minimum_required(VERSION 3.24)
project(SmokeAPI VERSION 3.1.0) project(SmokeAPI VERSION 3.1.1)
include(KoalaBox/cmake/KoalaBox.cmake) include(KoalaBox/cmake/KoalaBox.cmake)
@@ -9,7 +9,8 @@ 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)
@@ -37,9 +38,6 @@ set(SMOKE_API_SOURCES
${SMOKE_API_STATIC_SOURCES} ${SMOKE_API_STATIC_SOURCES}
src/smoke_api/smoke_api.cpp src/smoke_api/smoke_api.cpp
src/smoke_api/smoke_api.hpp 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
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
@@ -49,8 +47,8 @@ set(SMOKE_API_SOURCES
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/steamclient.cpp src/steamclient/steamclient.cpp
src/main.cpp src/main.cpp
) )
@@ -77,11 +75,11 @@ target_link_libraries(SmokeAPI_static PUBLIC SmokeAPI::common)
add_library(SmokeAPI SHARED ${SMOKE_API_SOURCES}) add_library(SmokeAPI SHARED ${SMOKE_API_SOURCES})
target_link_libraries(SmokeAPI PUBLIC SmokeAPI::common) 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 SmokeAPI ORIG_NAME ${SMOKEAPI_DLL}
) )
target_include_directories(SmokeAPI PRIVATE target_include_directories(SmokeAPI PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src" "${CMAKE_CURRENT_SOURCE_DIR}/src"
@@ -99,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.]

View File

@@ -1,3 +1,12 @@
Project page: https://github.com/acidicoala/SmokeAPI?tab=readme-ov-file#smokeapi Project page: https://github.com/acidicoala/SmokeAPI#readme
Forum topic: https://cs.rin.ru/forum/viewtopic.php?p=2597932#p2597932 Forum topic: https://cs.rin.ru/forum/viewtopic.php?p=2597932#p2597932
DLC Database: https://steamdb.info/ 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

@@ -115,7 +115,7 @@
}, },
"examples": [ "examples": [
{ {
"$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": 4, "$version": 4,
"logging": true, "logging": true,
"log_steam_http": true, "log_steam_http": true,

View File

@@ -1,3 +1,6 @@
#include <regex>
#include <set>
#include <koalabox/config.hpp> #include <koalabox/config.hpp>
#include <koalabox/dll_monitor.hpp> #include <koalabox/dll_monitor.hpp>
#include <koalabox/globals.hpp> #include <koalabox/globals.hpp>
@@ -6,16 +9,16 @@
#include <koalabox/logger.hpp> #include <koalabox/logger.hpp>
#include <koalabox/path.hpp> #include <koalabox/path.hpp>
#include <koalabox/paths.hpp> #include <koalabox/paths.hpp>
#include <koalabox/str.hpp>
#include <koalabox/util.hpp> #include <koalabox/util.hpp>
#include <koalabox/win.hpp> #include <koalabox/win.hpp>
#include "build_config.h"
#include "smoke_api.hpp" #include "smoke_api.hpp"
#include "smoke_api/config.hpp" #include "smoke_api/config.hpp"
#include "smoke_api/steamclient/steamclient.hpp" #include "smoke_api/steamclient/steamclient.hpp"
#include "steam_api/exports/steam_api.hpp" #include "steam_api/steam_interfaces.hpp"
#include "build_config.h"
// Hooking steam_api has shown itself to be less desirable than steamclient // Hooking steam_api has shown itself to be less desirable than steamclient
// for the reasons outlined below: // for the reasons outlined below:
@@ -38,46 +41,59 @@
namespace { namespace {
namespace kb = koalabox; namespace kb = koalabox;
void init_proxy_mode() { HMODULE original_steamapi_handle = nullptr;
LOG_INFO("Detected proxy mode");
const auto self_path = kb::paths::get_self_dir(); std::set<std::string> find_steamclient_versions(const HMODULE steamapi_handle) {
smoke_api::steamapi_module = kb::loader::load_original_library(self_path, STEAMAPI_DLL); 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;
} }
void init_hook_mode() { // ReSharper disable once CppDFAConstantFunctionResult
LOG_INFO("Detected hook mode"); 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;
}
kb::dll_monitor::init_listener( static const auto CreateInterface$ = KB_WIN_GET_PROC(steamclient_handle, CreateInterface);
{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)) {
// TODO: SteamAPI_Init will be too small to hook on x64.
// Ideally, we should inspect the address it jumps to and hook that instead.
// Moreover, SteamAPI_InitSafe calls the same address,
// so it could be used as a sanity check
KB_HOOK_DETOUR_MODULE(SteamAPI_Init, module_handle); const auto steamclient_versions = find_steamclient_versions(steamapi_handle);
KB_HOOK_DETOUR_MODULE(SteamAPI_InitSafe, module_handle); for(const auto& steamclient_version : steamclient_versions) {
KB_HOOK_DETOUR_MODULE(SteamAPI_InitFlat, module_handle); if(CreateInterface$(steamclient_version.c_str(), nullptr)) {
KB_HOOK_DETOUR_MODULE(SteamInternal_SteamAPI_Init, module_handle); LOG_WARN("'{}' was already initialized. SmokeAPI might not work as expected.", steamclient_version);
KB_HOOK_DETOUR_MODULE(SteamAPI_RestartAppIfNecessary, module_handle); LOG_WARN("Probable cause: SmokeAPI was injected too late. If possible, try injecting it earlier.");
KB_HOOK_DETOUR_MODULE(SteamAPI_Shutdown, module_handle);
// Note: It is not necessary to hook flat functions or interface accessors steam_interfaces::hook_steamclient_interface(steamclient_handle, steamclient_version);
// since the underlying interfaces will be hooked through steamclient. } 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 { namespace smoke_api {
HMODULE steamapi_module = nullptr;
bool hook_mode = false;
void init(const HMODULE module_handle) { void init(const HMODULE module_handle) {
try { try {
kb::globals::init_globals(module_handle, PROJECT_NAME); kb::globals::init_globals(module_handle, PROJECT_NAME);
@@ -100,10 +116,19 @@ namespace smoke_api {
kb::hook::init(true); kb::hook::init(true);
if(kb::hook::is_hook_mode(module_handle, STEAMAPI_DLL)) { if(kb::hook::is_hook_mode(module_handle, STEAMAPI_DLL)) {
hook_mode = true; LOG_INFO("Detected hook mode");
init_hook_mode();
start_dll_listener();
} else { } else {
init_proxy_mode(); 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"); LOG_INFO("Initialization complete");
@@ -114,11 +139,13 @@ namespace smoke_api {
void shutdown() { void shutdown() {
try { try {
if(steamapi_module != nullptr) { if(original_steamapi_handle != nullptr) {
kb::win::free_library(steamapi_module); kb::win::free_library(original_steamapi_handle);
steamapi_module = nullptr; original_steamapi_handle = nullptr;
} }
// TODO: Unhook everything
LOG_INFO("Shutdown complete"); LOG_INFO("Shutdown complete");
} catch(const std::exception& e) { } catch(const std::exception& e) {
const auto msg = std::format("Shutdown error: {}", e.what()); const auto msg = std::format("Shutdown error: {}", e.what());
@@ -127,4 +154,15 @@ namespace smoke_api {
kb::logger::shutdown(); 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

@@ -1,6 +1,6 @@
#pragma once #pragma once
#include <koalabox/win.hpp> #include "smoke_api/types.hpp"
constexpr auto STEAM_APPS = "STEAMAPPS_INTERFACE_VERSION"; constexpr auto STEAM_APPS = "STEAMAPPS_INTERFACE_VERSION";
constexpr auto STEAM_CLIENT = "SteamClient"; constexpr auto STEAM_CLIENT = "SteamClient";
@@ -14,11 +14,9 @@ constexpr auto STEAM_GAME_SERVER = "SteamGameServer";
#define DLL_EXPORT(TYPE) extern "C" [[maybe_unused]] __declspec(dllexport) TYPE __cdecl #define DLL_EXPORT(TYPE) extern "C" [[maybe_unused]] __declspec(dllexport) TYPE __cdecl
namespace smoke_api { namespace smoke_api {
extern HMODULE steamapi_module;
extern bool hook_mode;
void init(HMODULE module_handle); void init(HMODULE module_handle);
void shutdown(); void shutdown();
AppId_t get_app_id();
} }

View File

@@ -1,96 +0,0 @@
#include <koalabox/logger.hpp>
#include "steam_api/exports/steam_api.hpp"
#include "smoke_api/config.hpp"
#include "smoke_api/smoke_api.hpp"
#define AUTO_CALL_RETURN(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__)
#define AUTO_CALL_RESULT(FUNC, ...) \
static const auto _##FUNC = smoke_api::hook_mode \
? KB_HOOK_GET_HOOKED_FN(FUNC) \
: KB_WIN_GET_PROC(smoke_api::steamapi_module, FUNC); \
const auto result = _##FUNC(__VA_ARGS__)
DLL_EXPORT(bool) SteamAPI_Init() {
LOG_INFO(__func__);
AUTO_CALL_RESULT(SteamAPI_Init);
LOG_INFO("{} -> result: {}", __func__, result);
return result;
}
DLL_EXPORT(bool) SteamAPI_InitSafe() {
LOG_INFO(__func__);
AUTO_CALL_RESULT(SteamAPI_InitSafe);
LOG_INFO("{} -> result: {}", __func__, result);
return result;
}
DLL_EXPORT(ESteamAPIInitResult) SteamAPI_InitFlat(const SteamErrMsg* pOutErrMsg) {
LOG_INFO(__func__);
AUTO_CALL_RESULT(SteamAPI_InitFlat, pOutErrMsg);
const auto error_message = pOutErrMsg && *pOutErrMsg
? std::string_view(*pOutErrMsg)
: "";
LOG_INFO(
"{} -> result: {}, error_message: {}",
__func__,
result,
error_message
);
return result;
}
DLL_EXPORT(ESteamAPIInitResult) SteamInternal_SteamAPI_Init(
const char* pszInternalCheckInterfaceVersions,
const SteamErrMsg* pOutErrMsg
) {
LOG_INFO(__func__);
AUTO_CALL_RESULT(SteamInternal_SteamAPI_Init, pszInternalCheckInterfaceVersions, pOutErrMsg);
const auto error_message = pOutErrMsg && *pOutErrMsg
? std::string_view(*pOutErrMsg)
: "";
LOG_INFO(
"{} -> pszInternalCheckInterfaceVersions: {}, result: {}, error_message: {}",
__func__,
pszInternalCheckInterfaceVersions,
result,
error_message
);
return result;
}
DLL_EXPORT(bool) SteamAPI_RestartAppIfNecessary(const AppId_t unOwnAppID) {
LOG_INFO(__func__);
AUTO_CALL_RESULT(SteamAPI_RestartAppIfNecessary, unOwnAppID);
LOG_INFO("{} -> unOwnAppID: {}, result: {}", __func__, unOwnAppID, result);
// Restart can be suppressed if needed
return result;
}
DLL_EXPORT(void) SteamAPI_Shutdown() {
LOG_INFO("{} -> Game requested shutdown", __func__);
AUTO_CALL_RETURN(SteamAPI_Shutdown);
}

View File

@@ -1,22 +0,0 @@
#pragma once
#include "smoke_api/smoke_api.hpp"
#include "smoke_api/types.hpp"
using ESteamAPIInitResult = uint32_t;
using SteamErrMsg = char[1024];
DLL_EXPORT(bool) SteamAPI_Init();
DLL_EXPORT(bool) SteamAPI_InitSafe();
DLL_EXPORT(ESteamAPIInitResult) SteamAPI_InitFlat(const SteamErrMsg* pOutErrMsg);
DLL_EXPORT(ESteamAPIInitResult) SteamInternal_SteamAPI_Init(
const char* pszInternalCheckInterfaceVersions,
const SteamErrMsg* pOutErrMsg
);
DLL_EXPORT(bool) SteamAPI_RestartAppIfNecessary(AppId_t unOwnAppID);
DLL_EXPORT(void) SteamAPI_Shutdown();

View File

@@ -1,114 +0,0 @@
#include <map>
#include <regex>
#include <koalabox/logger.hpp>
#include <koalabox/win.hpp>
#include "smoke_api/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(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,
".rdata"
);
const auto rdata = std::string(
reinterpret_cast<const char*>(section.start_address),
section.size
);
const std::regex regex(version_prefix + "\\d{3}");
if(std::smatch match; std::regex_search(rdata, match, regex)) {
version_map[version_prefix] = match[0];
} 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: {}."
"Falling back to version {}",
ex.what(),
fallback
);
version_map[version_prefix] = version_prefix + fallback;
}
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");
return steam_client::GetGenericInterface(
__func__,
version,
MODULE_CALL_CLOSURE(SteamApps)
);
}
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*) SteamHTTP() {
static auto version = get_versioned_interface(STEAM_HTTP, "003");
return steam_client::GetGenericInterface(
__func__,
version,
MODULE_CALL_CLOSURE(SteamHTTP)
);
}
DLL_EXPORT(void*) SteamInventory() {
static auto version = get_versioned_interface(STEAM_INVENTORY, "001");
return steam_client::GetGenericInterface(
__func__,
version,
MODULE_CALL_CLOSURE(SteamInventory)
);
}
DLL_EXPORT(void*) SteamUser() {
static auto version = get_versioned_interface(STEAM_USER, "012");
return steam_client::GetGenericInterface(
__func__,
version,
MODULE_CALL_CLOSURE(SteamUser)
);
}

View File

@@ -1,7 +1,7 @@
#include <koalabox/logger.hpp> #include <koalabox/logger.hpp>
#include "steam_api/steam_interface.hpp"
#include "steam_client.hpp" #include "steam_client.hpp"
#include "steam_api/steam_interfaces.hpp"
namespace steam_client { namespace steam_client {
void* GetGenericInterface( void* GetGenericInterface(
@@ -14,7 +14,7 @@ namespace steam_client {
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) { } catch(const std::exception& e) {

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_ptr, const std::string& version_string);
}

View File

@@ -7,8 +7,9 @@
#include <koalabox/logger.hpp> #include <koalabox/logger.hpp>
#include <koalabox/win.hpp> #include <koalabox/win.hpp>
#include "steam_api/steam_interface.hpp" #include "steam_api/steam_interfaces.hpp"
#include "smoke_api/smoke_api.hpp" #include "smoke_api/smoke_api.hpp"
#include "smoke_api/steamclient/steamclient.hpp"
#include "virtuals/steam_api_virtuals.hpp" #include "virtuals/steam_api_virtuals.hpp"
namespace { namespace {
@@ -18,14 +19,15 @@ namespace {
void* function_address; // e.g. ISteamClient_GetISteamApps void* function_address; // e.g. ISteamClient_GetISteamApps
}; };
// TODO: Split fallback into low and high versions struct interface_data_t { // NOLINT(*-exception-escape)
struct interface_data { // NOLINT(*-exception-escape)
std::string fallback_version; // e.g. "SteamClient021" std::string fallback_version; // e.g. "SteamClient021"
// Key is function name without interface prefix
std::map<std::string, interface_entry> entry_map; std::map<std::string, interface_entry> entry_map;
// e.g. {ENTRY(ISteamClient, GetISteamApps), ...} // e.g. {ENTRY(ISteamClient, GetISteamApps), ...}
}; };
std::map<std::string, interface_data> get_virtual_hook_map() { // Key is interface name, e.g. "SteamClient"
std::map<std::string, interface_data_t> get_virtual_hook_map() {
#define ENTRY(INTERFACE, FUNC) \ #define ENTRY(INTERFACE, FUNC) \
{ \ { \
#FUNC, { \ #FUNC, { \
@@ -36,7 +38,7 @@ namespace {
return { return {
{ {
STEAM_APPS, STEAM_APPS,
interface_data{ interface_data_t{
.fallback_version = "STEAMAPPS_INTERFACE_VERSION008", .fallback_version = "STEAMAPPS_INTERFACE_VERSION008",
.entry_map = { .entry_map = {
ENTRY(ISteamApps, BIsSubscribedApp), ENTRY(ISteamApps, BIsSubscribedApp),
@@ -48,7 +50,7 @@ namespace {
}, },
{ {
STEAM_CLIENT, STEAM_CLIENT,
interface_data{ interface_data_t{
.fallback_version = "SteamClient021", .fallback_version = "SteamClient021",
.entry_map = { .entry_map = {
ENTRY(ISteamClient, GetISteamApps), ENTRY(ISteamClient, GetISteamApps),
@@ -60,7 +62,7 @@ namespace {
}, },
{ {
STEAM_GAME_SERVER, STEAM_GAME_SERVER,
interface_data{ interface_data_t{
.fallback_version = "SteamGameServer015", .fallback_version = "SteamGameServer015",
.entry_map = { .entry_map = {
ENTRY(ISteamGameServer, UserHasLicenseForApp), ENTRY(ISteamGameServer, UserHasLicenseForApp),
@@ -69,7 +71,7 @@ namespace {
}, },
{ {
STEAM_HTTP, STEAM_HTTP,
interface_data{ interface_data_t{
.fallback_version = "STEAMHTTP_INTERFACE_VERSION003", .fallback_version = "STEAMHTTP_INTERFACE_VERSION003",
.entry_map = { .entry_map = {
ENTRY(ISteamHTTP, GetHTTPResponseBodyData), ENTRY(ISteamHTTP, GetHTTPResponseBodyData),
@@ -80,7 +82,7 @@ namespace {
}, },
{ {
STEAM_INVENTORY, STEAM_INVENTORY,
interface_data{ interface_data_t{
.fallback_version = "STEAMINVENTORY_INTERFACE_V003", .fallback_version = "STEAMINVENTORY_INTERFACE_V003",
.entry_map = { .entry_map = {
ENTRY(ISteamInventory, GetResultStatus), ENTRY(ISteamInventory, GetResultStatus),
@@ -95,7 +97,7 @@ namespace {
}, },
{ {
STEAM_USER, STEAM_USER,
interface_data{ interface_data_t{
.fallback_version = "SteamUser023", .fallback_version = "SteamUser023",
.entry_map = { .entry_map = {
ENTRY(ISteamUser, UserHasLicenseForApp), ENTRY(ISteamUser, UserHasLicenseForApp),
@@ -105,8 +107,14 @@ namespace {
}; };
} }
auto read_interface_lookup() { // Key is function name, Value is ordinal
std::map<std::string, std::map<std::string, uint16_t>> lookup_map; 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_str = b::embed<"res/interface_lookup.json">().str();
const auto lookup_json = nlohmann::json::parse(lookup_str); const auto lookup_json = nlohmann::json::parse(lookup_str);
@@ -135,29 +143,14 @@ namespace {
} }
} }
namespace steam_interface { namespace steam_interfaces {
namespace kb = koalabox; 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;
}
}
/** /**
* @param interface_ptr Pointer to the interface * @param interface_ptr Pointer to the interface
* @param version_string Example: 'SteamClient020' * @param version_string Example: 'SteamClient007'
*/ */
void hook_virtuals(void* interface_ptr, const std::string& version_string) { void hook_virtuals(const void* interface_ptr, const std::string& version_string) {
if(interface_ptr == nullptr) { if(interface_ptr == nullptr) {
// Game has tried to use an interface before initializing steam api // Game has tried to use an interface before initializing steam api
// This does happen in practice. // This does happen in practice.
@@ -167,14 +160,10 @@ namespace steam_interface {
static std::mutex section; static std::mutex section;
const std::lock_guard guard(section); const std::lock_guard guard(section);
static std::set<void*> processed_interfaces; static std::set<const void*> processed_interfaces;
if(processed_interfaces.contains(interface_ptr)) { if(processed_interfaces.contains(interface_ptr)) {
LOG_DEBUG( LOG_DEBUG("Interface '{}' @ {} has already been processed.", version_string, interface_ptr);
"Interface '{}' @ {} has already been processed.",
version_string,
interface_ptr
);
return; return;
} }
processed_interfaces.insert(interface_ptr); processed_interfaces.insert(interface_ptr);
@@ -185,11 +174,7 @@ namespace steam_interface {
continue; continue;
} }
LOG_INFO( LOG_INFO("Processing '{}' @ {} found in virtual hook map", version_string, interface_ptr);
"Processing '{}' @ {} found in virtual hook map",
version_string,
interface_ptr
);
const auto& lookup = find_lookup(version_string, data.fallback_version); const auto& lookup = find_lookup(version_string, data.fallback_version);
@@ -209,4 +194,59 @@ namespace steam_interface {
break; 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,13 +1,13 @@
#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)) noexcept { VIRTUAL(bool) ISteamApps_BIsSubscribedApp(PARAMS(const AppId_t dlc_id)) noexcept {
return smoke_api::steam_apps::IsDlcUnlocked( return smoke_api::steam_apps::IsDlcUnlocked(
__func__, __func__,
steam_interface::get_app_id(), smoke_api::get_app_id(),
dlc_id, dlc_id,
SWAPPED_CALL_CLOSURE(ISteamApps_BIsSubscribedApp, ARGS(dlc_id)) SWAPPED_CALL_CLOSURE(ISteamApps_BIsSubscribedApp, ARGS(dlc_id))
); );
@@ -16,7 +16,7 @@ VIRTUAL(bool) ISteamApps_BIsSubscribedApp(PARAMS(const AppId_t dlc_id)) noexcept
VIRTUAL(bool) ISteamApps_BIsDlcInstalled(PARAMS(const AppId_t dlc_id)) noexcept { VIRTUAL(bool) ISteamApps_BIsDlcInstalled(PARAMS(const AppId_t dlc_id)) noexcept {
return smoke_api::steam_apps::IsDlcUnlocked( return smoke_api::steam_apps::IsDlcUnlocked(
__func__, __func__,
steam_interface::get_app_id(), smoke_api::get_app_id(),
dlc_id, dlc_id,
SWAPPED_CALL_CLOSURE(ISteamApps_BIsDlcInstalled, ARGS(dlc_id)) SWAPPED_CALL_CLOSURE(ISteamApps_BIsDlcInstalled, ARGS(dlc_id))
); );
@@ -25,7 +25,7 @@ VIRTUAL(bool) ISteamApps_BIsDlcInstalled(PARAMS(const AppId_t dlc_id)) noexcept
VIRTUAL(int) ISteamApps_GetDLCCount(PARAMS()) noexcept { VIRTUAL(int) ISteamApps_GetDLCCount(PARAMS()) noexcept {
return smoke_api::steam_apps::GetDLCCount( return smoke_api::steam_apps::GetDLCCount(
__func__, __func__,
steam_interface::get_app_id(), smoke_api::get_app_id(),
SWAPPED_CALL_CLOSURE(ISteamApps_GetDLCCount, ARGS()) SWAPPED_CALL_CLOSURE(ISteamApps_GetDLCCount, ARGS())
); );
} }
@@ -41,7 +41,7 @@ VIRTUAL(bool) ISteamApps_BGetDLCDataByIndex(
) noexcept { ) noexcept {
return smoke_api::steam_apps::GetDLCDataByIndex( return smoke_api::steam_apps::GetDLCDataByIndex(
__func__, __func__,
steam_interface::get_app_id(), smoke_api::get_app_id(),
iDLC, iDLC,
p_dlc_id, p_dlc_id,
pbAvailable, pbAvailable,
@@ -52,8 +52,8 @@ VIRTUAL(bool) ISteamApps_BGetDLCDataByIndex(
ARGS(iDLC, p_dlc_id, pbAvailable, pchName, cchNameBufferSize) ARGS(iDLC, p_dlc_id, pbAvailable, pchName, cchNameBufferSize)
), ),
SWAPPED_CALL_CLOSURE( SWAPPED_CALL_CLOSURE(
ISteamApps_BIsSubscribedApp, ISteamApps_BIsSubscribedApp,
ARGS(*p_dlc_id) ARGS(*p_dlc_id)
) )
); );
} }

View File

@@ -7,13 +7,13 @@ 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 { ) noexcept {
return steam_client::GetGenericInterface( return steam_client::GetGenericInterface(
__func__, __func__,
version, pchVersion,
SWAPPED_CALL_CLOSURE(ISteamClient_GetISteamApps, ARGS(hSteamUser, hSteamPipe, version)) SWAPPED_CALL_CLOSURE(ISteamClient_GetISteamApps, ARGS(hSteamUser, hSteamPipe, pchVersion))
); );
} }
@@ -21,13 +21,13 @@ 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 { ) noexcept {
return steam_client::GetGenericInterface( return steam_client::GetGenericInterface(
__func__, __func__,
version, pchVersion,
SWAPPED_CALL_CLOSURE(ISteamClient_GetISteamUser, ARGS(hSteamUser, hSteamPipe, version)) SWAPPED_CALL_CLOSURE(ISteamClient_GetISteamUser, ARGS(hSteamUser, hSteamPipe, pchVersion))
); );
} }

View File

@@ -1,7 +1,7 @@
#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(
@@ -9,7 +9,7 @@ VIRTUAL(EUserHasLicenseForAppResult) ISteamGameServer_UserHasLicenseForApp(
) noexcept { ) noexcept {
return smoke_api::steam_user::UserHasLicenseForApp( return smoke_api::steam_user::UserHasLicenseForApp(
__func__, __func__,
steam_interface::get_app_id(), smoke_api::get_app_id(),
dlc_id, dlc_id,
SWAPPED_CALL_CLOSURE(ISteamGameServer_UserHasLicenseForApp, ARGS(steamID, dlc_id)) SWAPPED_CALL_CLOSURE(ISteamGameServer_UserHasLicenseForApp, ARGS(steamID, dlc_id))
); );

View File

@@ -1,7 +1,6 @@
#include <koalabox/logger.hpp> #include <koalabox/logger.hpp>
#include "smoke_api/interfaces/steam_http.hpp" #include "smoke_api/interfaces/steam_http.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) ISteamHTTP_GetHTTPResponseBodyData( VIRTUAL(bool) ISteamHTTP_GetHTTPResponseBodyData(

View File

@@ -1,7 +1,7 @@
#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(
@@ -9,7 +9,7 @@ VIRTUAL(EUserHasLicenseForAppResult) ISteamUser_UserHasLicenseForApp(
) noexcept { ) noexcept {
return smoke_api::steam_user::UserHasLicenseForApp( return smoke_api::steam_user::UserHasLicenseForApp(
__func__, __func__,
steam_interface::get_app_id(), smoke_api::get_app_id(),
dlc_id, dlc_id,
SWAPPED_CALL_CLOSURE(ISteamUser_UserHasLicenseForApp, ARGS(steamID, dlc_id)) SWAPPED_CALL_CLOSURE(ISteamUser_UserHasLicenseForApp, ARGS(steamID, dlc_id))
); );

View File

@@ -14,10 +14,12 @@ VIRTUAL(void*) ISteamClient_GetISteamUser(PARAMS(HSteamUser, HSteamPipe, const c
VIRTUAL(void*) ISteamClient_GetISteamGenericInterface( VIRTUAL(void*) ISteamClient_GetISteamGenericInterface(
PARAMS(HSteamUser, HSteamPipe, const char*) PARAMS(HSteamUser, HSteamPipe, const char*)
) noexcept; ) noexcept;
VIRTUAL(void*) ISteamClient_GetISteamInventory( VIRTUAL(void*) ISteamClient_GetISteamInventory(
PARAMS(HSteamUser, HSteamPipe, const char*) PARAMS(HSteamUser, HSteamPipe, const char*)
) noexcept; ) noexcept;
// ISteamHTTP // ISteamHTTP
@@ -47,6 +49,7 @@ VIRTUAL(bool) ISteamInventory_GetItemsByID(
VIRTUAL(bool) ISteamInventory_SerializeResult( VIRTUAL(bool) ISteamInventory_SerializeResult(
PARAMS(SteamInventoryResult_t, void*, uint32_t*) PARAMS(SteamInventoryResult_t, void*, uint32_t*)
) noexcept; ) noexcept;
VIRTUAL(bool) ISteamInventory_GetItemDefinitionIDs(PARAMS(SteamItemDef_t*, uint32_t*)) noexcept; VIRTUAL(bool) ISteamInventory_GetItemDefinitionIDs(PARAMS(SteamItemDef_t*, uint32_t*)) noexcept;
VIRTUAL(bool) ISteamInventory_CheckResultSteamID(PARAMS(SteamInventoryResult_t, CSteamID)) noexcept; VIRTUAL(bool) ISteamInventory_CheckResultSteamID(PARAMS(SteamInventoryResult_t, CSteamID)) noexcept;

View File

@@ -1,15 +1,21 @@
#include <mutex>
#include <koalabox/hook.hpp> #include <koalabox/hook.hpp>
#include <koalabox/logger.hpp>
#include "smoke_api/steamclient/steamclient.hpp" #include "smoke_api/steamclient/steamclient.hpp"
#include "smoke_api/smoke_api.hpp"
#include "smoke_api/types.hpp" #include "smoke_api/types.hpp"
#include "steam_api/steam_client.hpp" #include "steam_api/steam_client.hpp"
/** /**
* SmokeAPI implementation * SmokeAPI implementation
*/ */
C_DECL(void*) CreateInterface(const char* interface_version, int* out_result) { 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( return steam_client::GetGenericInterface(
__func__, __func__,
interface_version, interface_version,

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

@@ -47,10 +47,12 @@
#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;