diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b7af63..3656b3b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,8 @@ add_subdirectory(tools) set_32_and_64(STEAMAPI_DLL steam_api) 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) @@ -74,11 +75,11 @@ target_link_libraries(SmokeAPI_static PUBLIC SmokeAPI::common) add_library(SmokeAPI SHARED ${SMOKE_API_SOURCES}) 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( TARGET SmokeAPI FILE_DESC "Steamworks DLC unlocker" - ORIG_NAME SmokeAPI + ORIG_NAME ${SMOKEAPI_DLL} ) target_include_directories(SmokeAPI PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src" @@ -97,7 +98,7 @@ configure_linker_exports( HEADER_NAME "linker_exports_for_steam_api" FORWARDED_DLL "${STEAMAPI_DLL}_o" 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( diff --git a/KoalaBox b/KoalaBox index 1fd92cf..25d3d0a 160000 --- a/KoalaBox +++ b/KoalaBox @@ -1 +1 @@ -Subproject commit 1fd92cf817d2aeef0055a5c206b8384b3245c17c +Subproject commit 25d3d0a883ac7f55656b7758c25be7322c52ab22 diff --git a/README.adoc b/README.adoc index 8326a41..3a23039 100644 --- a/README.adoc +++ b/README.adoc @@ -79,7 +79,9 @@ If it doesn't work, try installing it in proxy mode. === 🪝 Hook mode . 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 installation) @@ -92,7 +94,8 @@ 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]. . 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}. -. 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. @@ -102,12 +105,13 @@ In such cases, it might be worth trying {special_k}, which can inject SmokeAPI a . 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}. -. 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 <> ---- +''' 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}. diff --git a/src/smoke_api/smoke_api.cpp b/src/smoke_api/smoke_api.cpp index 69f47b2..439ad1d 100644 --- a/src/smoke_api/smoke_api.cpp +++ b/src/smoke_api/smoke_api.cpp @@ -1,3 +1,6 @@ +#include +#include + #include #include #include @@ -9,11 +12,13 @@ #include #include -#include "build_config.h" - #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: @@ -38,11 +43,59 @@ namespace { HMODULE original_steamapi_handle = nullptr; + std::set find_steamclient_versions(const HMODULE steamapi_handle) noexcept { + try { + std::set 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; + } catch(const std::exception& e) { + LOG_ERROR("{} -> insert error: {}", __func__, e.what()); + return {}; + } + } + + // 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}, - [&](const HMODULE& module_handle, auto&) { - KB_HOOK_DETOUR_MODULE(CreateInterface, module_handle); + { + {STEAMCLIENT_DLL, on_steamclient_loaded} } ); } @@ -82,6 +135,7 @@ namespace smoke_api { self_path, STEAMAPI_DLL ); + start_dll_listener(); } diff --git a/src/steam_api/steam_client.cpp b/src/steam_api/steam_client.cpp index f6a3959..19bea30 100644 --- a/src/steam_api/steam_client.cpp +++ b/src/steam_api/steam_client.cpp @@ -1,7 +1,7 @@ #include -#include "steam_api/steam_interfaces.hpp" #include "steam_client.hpp" +#include "steam_api/steam_interfaces.hpp" namespace steam_client { void* GetGenericInterface( diff --git a/src/steam_api/steam_interfaces.cpp b/src/steam_api/steam_interfaces.cpp index 0e2aae3..8ae0d5c 100644 --- a/src/steam_api/steam_interfaces.cpp +++ b/src/steam_api/steam_interfaces.cpp @@ -9,6 +9,7 @@ #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 { @@ -18,14 +19,15 @@ namespace { void* function_address; // e.g. ISteamClient_GetISteamApps }; - // TODO: Split fallback into low and high versions - struct interface_data { // NOLINT(*-exception-escape) + struct interface_data_t { // NOLINT(*-exception-escape) std::string fallback_version; // e.g. "SteamClient021" + // Key is function name without interface prefix std::map entry_map; // e.g. {ENTRY(ISteamClient, GetISteamApps), ...} }; - std::map get_virtual_hook_map() { + // Key is interface name, e.g. "SteamClient" + std::map get_virtual_hook_map() { #define ENTRY(INTERFACE, FUNC) \ { \ #FUNC, { \ @@ -36,7 +38,7 @@ namespace { return { { STEAM_APPS, - interface_data{ + interface_data_t{ .fallback_version = "STEAMAPPS_INTERFACE_VERSION008", .entry_map = { ENTRY(ISteamApps, BIsSubscribedApp), @@ -48,7 +50,7 @@ namespace { }, { STEAM_CLIENT, - interface_data{ + interface_data_t{ .fallback_version = "SteamClient021", .entry_map = { ENTRY(ISteamClient, GetISteamApps), @@ -60,7 +62,7 @@ namespace { }, { STEAM_GAME_SERVER, - interface_data{ + interface_data_t{ .fallback_version = "SteamGameServer015", .entry_map = { ENTRY(ISteamGameServer, UserHasLicenseForApp), @@ -69,7 +71,7 @@ namespace { }, { STEAM_HTTP, - interface_data{ + interface_data_t{ .fallback_version = "STEAMHTTP_INTERFACE_VERSION003", .entry_map = { ENTRY(ISteamHTTP, GetHTTPResponseBodyData), @@ -80,7 +82,7 @@ namespace { }, { STEAM_INVENTORY, - interface_data{ + interface_data_t{ .fallback_version = "STEAMINVENTORY_INTERFACE_V003", .entry_map = { ENTRY(ISteamInventory, GetResultStatus), @@ -95,7 +97,7 @@ namespace { }, { STEAM_USER, - interface_data{ + interface_data_t{ .fallback_version = "SteamUser023", .entry_map = { ENTRY(ISteamUser, UserHasLicenseForApp), @@ -105,8 +107,14 @@ namespace { }; } - auto read_interface_lookup() { - std::map> lookup_map; + // Key is function name, Value is ordinal + using ordinal_map_t = std::map; + + // Key is interface version string + using lookup_map_t = std::map; + + 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); @@ -140,9 +148,9 @@ namespace steam_interfaces { /** * @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) { // Game has tried to use an interface before initializing steam api // This does happen in practice. @@ -152,14 +160,10 @@ namespace steam_interfaces { static std::mutex section; const std::lock_guard guard(section); - static std::set processed_interfaces; + static std::set processed_interfaces; if(processed_interfaces.contains(interface_ptr)) { - LOG_DEBUG( - "Interface '{}' @ {} has already been processed.", - version_string, - interface_ptr - ); + LOG_DEBUG("Interface '{}' @ {} has already been processed.", version_string, interface_ptr); return; } processed_interfaces.insert(interface_ptr); @@ -170,11 +174,7 @@ namespace steam_interfaces { continue; } - LOG_INFO( - "Processing '{}' @ {} found in virtual hook map", - version_string, - interface_ptr - ); + LOG_INFO("Processing '{}' @ {} found in virtual hook map", version_string, interface_ptr); const auto& lookup = find_lookup(version_string, data.fallback_version); @@ -194,4 +194,63 @@ namespace steam_interfaces { 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(); + + // 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; + } + + 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) + } + } + + // ISteamClient_ReleaseUser(ARGS(steam_pipe, steam_user)); + // ISteamClient_BReleaseSteamPipe(ARGS(steam_pipe)); + + kb::hook::unhook_vt_all(THIS); + } catch(const std::exception& e) { + LOG_ERROR("{} -> Unhandled exception: {}", __func__, e.what()); + } + } } diff --git a/src/steam_api/steam_interfaces.hpp b/src/steam_api/steam_interfaces.hpp index 2140b13..d1a9dde 100644 --- a/src/steam_api/steam_interfaces.hpp +++ b/src/steam_api/steam_interfaces.hpp @@ -3,7 +3,16 @@ #include "smoke_api/types.hpp" namespace steam_interfaces { + void hook_virtuals(const void* interface_ptr, const std::string& version_string); - void hook_virtuals(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; } diff --git a/src/steam_api/virtuals/isteamclient.cpp b/src/steam_api/virtuals/isteamclient.cpp index b046144..668dd76 100644 --- a/src/steam_api/virtuals/isteamclient.cpp +++ b/src/steam_api/virtuals/isteamclient.cpp @@ -7,13 +7,13 @@ VIRTUAL(void*) ISteamClient_GetISteamApps( PARAMS( const HSteamUser hSteamUser, const HSteamPipe hSteamPipe, - const char* version + const char* pchVersion ) ) noexcept { return steam_client::GetGenericInterface( __func__, - version, - SWAPPED_CALL_CLOSURE(ISteamClient_GetISteamApps, ARGS(hSteamUser, hSteamPipe, version)) + pchVersion, + SWAPPED_CALL_CLOSURE(ISteamClient_GetISteamApps, ARGS(hSteamUser, hSteamPipe, pchVersion)) ); } @@ -21,13 +21,13 @@ VIRTUAL(void*) ISteamClient_GetISteamUser( PARAMS( const HSteamUser hSteamUser, const HSteamPipe hSteamPipe, - const char* version + const char* pchVersion ) ) noexcept { return steam_client::GetGenericInterface( __func__, - version, - SWAPPED_CALL_CLOSURE(ISteamClient_GetISteamUser, ARGS(hSteamUser, hSteamPipe, version)) + pchVersion, + SWAPPED_CALL_CLOSURE(ISteamClient_GetISteamUser, ARGS(hSteamUser, hSteamPipe, pchVersion)) ); } diff --git a/src/steam_api/virtuals/steam_api_virtuals.hpp b/src/steam_api/virtuals/steam_api_virtuals.hpp index a764ff7..b481475 100644 --- a/src/steam_api/virtuals/steam_api_virtuals.hpp +++ b/src/steam_api/virtuals/steam_api_virtuals.hpp @@ -14,10 +14,12 @@ VIRTUAL(void*) ISteamClient_GetISteamUser(PARAMS(HSteamUser, HSteamPipe, const c VIRTUAL(void*) ISteamClient_GetISteamGenericInterface( PARAMS(HSteamUser, HSteamPipe, const char*) + ) noexcept; VIRTUAL(void*) ISteamClient_GetISteamInventory( PARAMS(HSteamUser, HSteamPipe, const char*) + ) noexcept; // ISteamHTTP @@ -47,6 +49,7 @@ VIRTUAL(bool) ISteamInventory_GetItemsByID( VIRTUAL(bool) ISteamInventory_SerializeResult( PARAMS(SteamInventoryResult_t, void*, uint32_t*) + ) noexcept; VIRTUAL(bool) ISteamInventory_GetItemDefinitionIDs(PARAMS(SteamItemDef_t*, uint32_t*)) noexcept; VIRTUAL(bool) ISteamInventory_CheckResultSteamID(PARAMS(SteamInventoryResult_t, CSteamID)) noexcept; diff --git a/src/steamclient/steamclient.cpp b/src/steamclient/steamclient.cpp index b5112d8..e381d3f 100644 --- a/src/steamclient/steamclient.cpp +++ b/src/steamclient/steamclient.cpp @@ -1,14 +1,21 @@ +#include + #include +#include #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, 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( __func__, interface_version, diff --git a/static/smoke_api/steamclient/steamclient.hpp b/static/smoke_api/steamclient/steamclient.hpp index 68d0afa..8d6994d 100644 --- a/static/smoke_api/steamclient/steamclient.hpp +++ b/static/smoke_api/steamclient/steamclient.hpp @@ -2,4 +2,13 @@ #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);