39 Commits

Author SHA1 Message Date
acidicoala
95ceac3d47 Fixed steam url 2023-01-30 14:43:25 +03:00
acidicoala
aa23be373d Updated README 2023-01-22 18:31:54 +03:00
acidicoala
6288aa149c Changed pipe name 2023-01-21 20:25:59 +03:00
acidicoala
6606c01fda Updated koaloader config file name [skip ci] 2023-01-13 19:07:01 +03:00
acidicoala
caf9ef2225 Updated README [skip-ci] 2023-01-13 15:51:24 +03:00
acidicoala
4f93515bac Renamed koalageddon mode to store mode 2023-01-13 03:49:07 +03:00
acidicoala
dfbce391a7 Added config reload via IPC pipes 2023-01-12 00:25:36 +03:00
acidicoala
d52e752723 Refactored code visitor function 2023-01-11 12:49:43 +03:00
acidicoala
3e40e4607f Improved koalageddon config init 2023-01-11 10:58:11 +03:00
acidicoala
edd785cfcf Added ReloadConfig 2023-01-11 01:35:14 +03:00
acidicoala
30f1076261 Updated app id status 2023-01-10 03:17:37 +03:00
acidicoala
59fa68a7b4 Updated KoalaBox 2023-01-09 09:12:08 +03:00
acidicoala
a7e912a98a Added Release config 2023-01-09 08:44:30 +03:00
acidicoala
3f595f4f88 Removed Debug config 2023-01-09 08:38:38 +03:00
acidicoala
ad1578f556 Refactor 2023-01-09 08:02:57 +03:00
acidicoala
6347e3f148 Inventory logic re-write 2023-01-09 03:26:42 +03:00
acidicoala
55ada47bef Updated logging 2023-01-08 11:52:23 +03:00
acidicoala
a2cbf55819 Refactor 2023-01-08 01:00:32 +03:00
acidicoala
b1680fe3d7 Fix 64-bit build 2023-01-07 11:36:12 +03:00
acidicoala
b077212d10 Refactored for latest Koalabox 2023-01-07 11:25:37 +03:00
acidicoala
1d36cfb3be Implemented function ordinal detection algorithm 2023-01-07 03:41:10 +03:00
acidicoala
eb888b91b5 Refactored function selector hooking 2023-01-06 11:50:40 +03:00
acidicoala
f29fc96dcd Added address map args 2023-01-06 11:09:09 +03:00
acidicoala
eaca0bec34 Refactored hook calls 2023-01-06 09:54:51 +03:00
acidicoala
011f3fac5d Refactored DLC unlocking logic 2023-01-06 05:18:13 +03:00
acidicoala
b04c96a36d Improved koalageddon mode detection 2023-01-05 21:46:56 +03:00
acidicoala
6f43e5ee9b bump to v2, koalageddon mode improvements 2023-01-05 17:13:34 +03:00
acidicoala
89fa851943 Added remote koalageddon config source 2023-01-05 07:08:45 +03:00
acidicoala
c654f9cbfd Improved DLC ID cache resolution 2023-01-05 04:05:07 +03:00
acidicoala
7a628c1315 Added steamworks sdk 155 2023-01-02 02:51:26 +03:00
acidicoala
297861ba88 Added family sharing toggle 2023-01-02 02:51:12 +03:00
acidicoala
71506bd03c Fixed koalageddon mode 2023-01-02 00:54:28 +03:00
acidicoala
0d1ae0fd29 Refactor 2022-12-31 18:32:12 +03:00
acidicoala
1ba84753aa Added IClientUser_IsSubscribedApp 2022-12-30 05:51:07 +03:00
acidicoala
d6828e3bfb Refactor 2022-12-30 02:54:19 +03:00
acidicoala
636f9186a3 Added koalageddon config 2022-12-30 02:17:48 +03:00
acidicoala
941d5d7d8c Removed steam_api hooking 2022-12-29 21:44:02 +03:00
acidicoala
8cba428c0f Updated KoalaBox 2022-12-29 16:56:38 +03:00
acidicoala
da43de4065 [WIP] deadlock fix 2022-06-23 10:35:03 +03:00
81 changed files with 2823 additions and 1658 deletions

View File

@@ -4,7 +4,7 @@ on: push
jobs: jobs:
ci: ci:
name: CI name: CI
uses: acidicoala/KoalaBox/.github/workflows/build-and-package.yml@d9b0d1a00beb065a9a931a60ef615ce8d1fc7164 uses: acidicoala/KoalaBox/.github/workflows/build-and-package.yml@15d5cfc2e515bc72e47da6c0c563820cff98551f
permissions: permissions:
contents: write contents: write
with: with:
@@ -14,5 +14,6 @@ jobs:
zip_command: > zip_command: >
zip -j $ZIP_NAME zip -j $ZIP_NAME
artifacts/*/*.dll artifacts/*/*.dll
res/SmokeAPI.config.json
config: Debug config: Release

9
.idea/cmake.xml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CMakeSharedSettings">
<configurations>
<configuration PROFILE_NAME="Debug [32] (Template: copy and set the 32-bit toolchain)" ENABLED="false" GENERATION_DIR="build/32" CONFIG_NAME="Debug" GENERATION_OPTIONS="-G &quot;Visual Studio 17 2022&quot; -A Win32" />
<configuration PROFILE_NAME="Debug [64] (Template: copy and set the 64-bit toolchain)" ENABLED="false" GENERATION_DIR="build/64" CONFIG_NAME="Debug" GENERATION_OPTIONS="-G &quot;Visual Studio 17 2022&quot; -A x64" />
</configurations>
</component>
</project>

View File

@@ -8,6 +8,8 @@
<option name="WRAP_TEXT_INSIDE_BLOCKQUOTES" value="false" /> <option name="WRAP_TEXT_INSIDE_BLOCKQUOTES" value="false" />
</Markdown> </Markdown>
<Objective-C> <Objective-C>
<option name="KEEP_STRUCTURES_IN_ONE_LINE" value="true" />
<option name="FUNCTION_CALL_ARGUMENTS_ALIGN_MULTILINE" value="false" />
<option name="SPACE_BEFORE_POINTER_IN_DECLARATION" value="false" /> <option name="SPACE_BEFORE_POINTER_IN_DECLARATION" value="false" />
<option name="SPACE_AFTER_POINTER_IN_DECLARATION" value="true" /> <option name="SPACE_AFTER_POINTER_IN_DECLARATION" value="true" />
<option name="SPACE_BEFORE_REFERENCE_IN_DECLARATION" value="false" /> <option name="SPACE_BEFORE_REFERENCE_IN_DECLARATION" value="false" />
@@ -17,15 +19,26 @@
<editorconfig> <editorconfig>
<option name="ENABLED" value="false" /> <option name="ENABLED" value="false" />
</editorconfig> </editorconfig>
<files>
<extensions>
<pair source="cpp" header="hpp" fileNamingConvention="SNAKE_CASE" />
<pair source="c" header="h" fileNamingConvention="NONE" />
<pair source="cu" header="cuh" fileNamingConvention="NONE" />
</extensions>
</files>
<codeStyleSettings language="CMake"> <codeStyleSettings language="CMake">
<indentOptions> <indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" /> <option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions> </indentOptions>
</codeStyleSettings> </codeStyleSettings>
<codeStyleSettings language="ObjectiveC"> <codeStyleSettings language="ObjectiveC">
<option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
<option name="BLANK_LINES_BEFORE_IMPORTS" value="0" /> <option name="BLANK_LINES_BEFORE_IMPORTS" value="0" />
<option name="BLANK_LINES_AFTER_IMPORTS" value="0" /> <option name="BLANK_LINES_AFTER_IMPORTS" value="0" />
<option name="BLANK_LINES_AROUND_CLASS" value="0" />
<option name="BLANK_LINES_AROUND_METHOD" value="0" />
<option name="BLANK_LINES_AROUND_METHOD_IN_INTERFACE" value="0" /> <option name="BLANK_LINES_AROUND_METHOD_IN_INTERFACE" value="0" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="SOFT_MARGINS" value="120" /> <option name="SOFT_MARGINS" value="120" />
<indentOptions> <indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" /> <option name="CONTINUATION_INDENT_SIZE" value="4" />

6
.idea/encodings.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/include/sdk/steamtypes.h" charset="windows-1252" />
</component>
</project>

View File

@@ -4,5 +4,6 @@
<inspection_tool class="ClangTidy" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="ClangTidy" enabled="true" level="WARNING" enabled_by_default="true">
<option name="clangTidyChecks" value="-*,cppcoreguidelines-interfaces-global-init,cppcoreguidelines-narrowing-conversions,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-pro-type-static-cast-downcast,cppcoreguidelines-slicing,google-default-arguments,google-explicit-constructor,google-runtime-operator,hicpp-exception-baseclass,hicpp-multiway-paths-covered,modernize-avoid-bind,modernize-concat-nested-namespaces,modernize-deprecated-headers,modernize-deprecated-ios-base-aliases,modernize-loop-convert,modernize-make-shared,modernize-make-unique,modernize-pass-by-value,modernize-raw-string-literal,modernize-redundant-void-arg,modernize-replace-auto-ptr,modernize-replace-disallow-copy-and-assign-macro,modernize-replace-random-shuffle,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-auto,modernize-use-bool-literals,modernize-use-emplace,modernize-use-equals-default,modernize-use-equals-delete,modernize-use-nodiscard,modernize-use-noexcept,modernize-use-nullptr,modernize-use-override,modernize-use-transparent-functors,modernize-use-uncaught-exceptions,mpi-buffer-deref,mpi-type-mismatch,openmp-use-default-none,performance-faster-string-find,performance-for-range-copy,performance-implicit-conversion-in-loop,performance-inefficient-algorithm,performance-inefficient-string-concatenation,performance-inefficient-vector-operation,performance-move-const-arg,performance-move-constructor-init,performance-no-automatic-move,performance-noexcept-move-constructor,performance-trivially-destructible,performance-type-promotion-in-math-fn,performance-unnecessary-copy-initialization,performance-unnecessary-value-param,portability-simd-intrinsics,bugprone-*,abseil-*,cert-*,objc-*,readability-*,clang-analyzer-*,misc-*,-readability-magic-numbers,-bugprone-easily-swappable-parameters,-readability-implicit-bool-conversion,-readability-identifier-length,-readability-named-parameter,-readability-function-cognitive-complexity" /> <option name="clangTidyChecks" value="-*,cppcoreguidelines-interfaces-global-init,cppcoreguidelines-narrowing-conversions,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-pro-type-static-cast-downcast,cppcoreguidelines-slicing,google-default-arguments,google-explicit-constructor,google-runtime-operator,hicpp-exception-baseclass,hicpp-multiway-paths-covered,modernize-avoid-bind,modernize-concat-nested-namespaces,modernize-deprecated-headers,modernize-deprecated-ios-base-aliases,modernize-loop-convert,modernize-make-shared,modernize-make-unique,modernize-pass-by-value,modernize-raw-string-literal,modernize-redundant-void-arg,modernize-replace-auto-ptr,modernize-replace-disallow-copy-and-assign-macro,modernize-replace-random-shuffle,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-auto,modernize-use-bool-literals,modernize-use-emplace,modernize-use-equals-default,modernize-use-equals-delete,modernize-use-nodiscard,modernize-use-noexcept,modernize-use-nullptr,modernize-use-override,modernize-use-transparent-functors,modernize-use-uncaught-exceptions,mpi-buffer-deref,mpi-type-mismatch,openmp-use-default-none,performance-faster-string-find,performance-for-range-copy,performance-implicit-conversion-in-loop,performance-inefficient-algorithm,performance-inefficient-string-concatenation,performance-inefficient-vector-operation,performance-move-const-arg,performance-move-constructor-init,performance-no-automatic-move,performance-noexcept-move-constructor,performance-trivially-destructible,performance-type-promotion-in-math-fn,performance-unnecessary-copy-initialization,performance-unnecessary-value-param,portability-simd-intrinsics,bugprone-*,abseil-*,cert-*,objc-*,readability-*,clang-analyzer-*,misc-*,-readability-magic-numbers,-bugprone-easily-swappable-parameters,-readability-implicit-bool-conversion,-readability-identifier-length,-readability-named-parameter,-readability-function-cognitive-complexity" />
</inspection_tool> </inspection_tool>
<inspection_tool class="ConstantFunctionResult" enabled="true" level="INFORMATION" enabled_by_default="true" editorAttributes="INFORMATION_ATTRIBUTES" />
</profile> </profile>
</component> </component>

3
.idea/misc.xml generated
View File

@@ -2,6 +2,9 @@
<project version="4"> <project version="4">
<component name="CMakeWorkspace" PROJECT_DIR="$PROJECT_DIR$" /> <component name="CMakeWorkspace" PROJECT_DIR="$PROJECT_DIR$" />
<component name="CidrRootsConfiguration"> <component name="CidrRootsConfiguration">
<libraryRoots>
<file path="$PROJECT_DIR$/KoalaBox/dependencies" />
</libraryRoots>
<excludeRoots> <excludeRoots>
<file path="$PROJECT_DIR$/build" /> <file path="$PROJECT_DIR$/build" />
<file path="$PROJECT_DIR$/sdk" /> <file path="$PROJECT_DIR$/sdk" />

View File

@@ -1,12 +1,12 @@
cmake_minimum_required(VERSION 3.22) cmake_minimum_required(VERSION 3.24)
project(SmokeAPI VERSION 1.0.3) project(SmokeAPI VERSION 2.0.0)
include(KoalaBox/cmake/KoalaBox.cmake) include(KoalaBox/cmake/KoalaBox.cmake)
add_subdirectory(KoalaBox EXCLUDE_FROM_ALL) add_subdirectory(KoalaBox EXCLUDE_FROM_ALL)
set_32_and_64(ORIGINAL_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(VSTDLIB_DLL vstdlib_s) set_32_and_64(VSTDLIB_DLL vstdlib_s)
@@ -19,61 +19,86 @@ file(GLOB DLL_INPUT "res/dll/*/sdk/redistributable_bin/${DLL_SUFFIX}.dll")
set( set(
STEAM_API_EXPORTS STEAM_API_EXPORTS
"${CMAKE_CURRENT_SOURCE_DIR}/src/steam_api_exports/steam_api_flat.cpp" "src/game_mode/exports/steam_api_flat.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/steam_api_exports/steam_api_internal.cpp" "src/game_mode/exports/steam_api_internal.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/steam_api_exports/steam_api_unversioned.cpp" "src/game_mode/exports/steam_api_unversioned.cpp"
) )
configure_linker_exports( configure_linker_exports(
FORWARDED_DLL "${ORIGINAL_DLL}_o" FORWARDED_DLL "${STEAMAPI_DLL}_o"
INPUT_SOURCES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/steam_api_exports" INPUT_SOURCES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/game_mode/exports"
INPUT_DLLS "${DLL_INPUT}" INPUT_DLLS "${DLL_INPUT}"
DEP_SOURCES "${STEAM_API_EXPORTS}" DEP_SOURCES "${STEAM_API_EXPORTS}" # Is this redundant?
) )
configure_build_config(extra_build_config) configure_build_config(extra_build_config)
set( set(
SMOKE_API_SOURCES SMOKE_API_SOURCES
src/common/app_cache.cpp
src/common/app_cache.hpp
src/common/steamclient_exports.cpp
src/common/steamclient_exports.hpp
src/core/api.cpp
src/core/api.hpp
src/core/globals.cpp
src/core/globals.hpp
src/core/paths.cpp
src/core/paths.hpp
src/core/types.cpp
src/core/types.hpp
src/game_mode/exports/steam_api_flat.cpp
src/game_mode/exports/steam_api_internal.cpp
src/game_mode/exports/steam_api_unversioned.cpp
src/game_mode/virtuals/isteamapps.cpp
src/game_mode/virtuals/isteamclient.cpp
src/game_mode/virtuals/isteaminventory.cpp
src/game_mode/virtuals/isteamuser.cpp
src/game_mode/virtuals/steam_api_virtuals.hpp
src/smoke_api/config.cpp
src/smoke_api/config.hpp
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_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/isteamclient.cpp
src/steam_api_virtuals/isteaminventory.cpp
src/steam_api_virtuals/isteamuser.cpp
src/steam_impl/steam_apps.cpp src/steam_impl/steam_apps.cpp
src/steam_impl/steam_apps.hpp
src/steam_impl/steam_client.cpp src/steam_impl/steam_client.cpp
src/steam_impl/steam_client.hpp
src/steam_impl/steam_impl.cpp
src/steam_impl/steam_impl.hpp src/steam_impl/steam_impl.hpp
src/steam_impl/steam_inventory.cpp src/steam_impl/steam_inventory.cpp
src/steam_impl/steam_inventory.hpp
src/steam_impl/steam_user.cpp src/steam_impl/steam_user.cpp
src/steam_functions/steam_functions.cpp src/steam_impl/steam_user.hpp
src/steam_functions/steam_functions.hpp
src/steam_types/steam_types.hpp
src/steamclient_exports/steamclient.cpp
src/main.cpp src/main.cpp
${GENERATED_LINKER_EXPORTS} ${GENERATED_LINKER_EXPORTS}
) )
# Include store_mode mode sources only in 32-bit builds
if (CMAKE_SIZEOF_VOID_P EQUAL 4) if (CMAKE_SIZEOF_VOID_P EQUAL 4)
set( set(
SMOKE_API_SOURCES ${SMOKE_API_SOURCES} SMOKE_API_SOURCES ${SMOKE_API_SOURCES}
src/koalageddon/vstdlib.cpp src/store_mode/steamclient/client_app_manager.cpp
src/koalageddon/steamclient.cpp src/store_mode/steamclient/client_apps.cpp
src/steamclient_virtuals/client_app_manager.cpp src/store_mode/steamclient/client_inventory.cpp
src/steamclient_virtuals/client_apps.cpp src/store_mode/steamclient/client_user.cpp
src/steamclient_virtuals/client_inventory.cpp src/store_mode/steamclient/client_utils.cpp
src/steamclient_virtuals/client_user.cpp src/store_mode/steamclient/steamclient.cpp
src/store_mode/steamclient/steamclient.hpp
src/store_mode/vstdlib/vstdlib.cpp
src/store_mode/vstdlib/vstdlib.hpp
src/store_mode/store.cpp
src/store_mode/store.hpp
src/store_mode/store_api.cpp
src/store_mode/store_api.hpp
src/store_mode/store_cache.cpp
src/store_mode/store_cache.hpp
) )
endif () endif ()
add_library(SmokeAPI SHARED ${SMOKE_API_SOURCES} ${VERSION_RESOURCE}) add_library(SmokeAPI SHARED ${SMOKE_API_SOURCES} ${VERSION_RESOURCE})
configure_output_name(${ORIGINAL_DLL}) configure_output_name(${STEAMAPI_DLL})
configure_include_directories() configure_include_directories()
target_link_libraries(SmokeAPI PRIVATE KoalaBox) target_link_libraries(SmokeAPI PRIVATE KoalaBox)

View File

@@ -1,12 +0,0 @@
Copyright (c) 2022 by acidicoala
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.

275
README.adoc Normal file
View File

@@ -0,0 +1,275 @@
= 🐨 SmokeAPI ♨
_Legit DLC Unlocker for Steamworks_
image::https://www.gstatic.com/android/keyboard/emojikitchen/20201001/u1f428/u1f428_u2615.png[,256,align="center"]
== ✨ Features
* 🔓 Legit DLC Unlocking
* 🛅 Inventory emulation
* 📝 Config-less operation
* Multiple installation methods:
** 🛍️ Store mode
** 🎮 Game mode
*** 🪝 Hook mode
*** 🔀 Proxy mode
== 🔗 Links
:forum-topic: https://cs.rin.ru/forum/viewtopic.php?p=2597932#p2597932[SmokeAPI forum topic]
* 📥 https://github.com/acidicoala/SmokeAPI/releases/latest[Download the latest release]
* 💬 {forum-topic}
== 📖 Introduction
=== What is SmokeAPI?
SmokeAPI is a DLC unlocker for the games that are legitimately owned in your Steam account.
It attempts to spoof games that use Steamworks SDK into believing that you own desired DLCs.
However, SmokeAPI does not modify the rest of the Steamworks SDK, hence features like multiplayer, achievements, etc. remain fully functional.
.Supported versions
[%collapsible]
====
SmokeAPI aims to support all released SteamAPI versions.
When it encountered a new, unsupported interface version, it will fall back on the latest supported version.
Below is a list of supported interface versions:
* ISteamClient v6 — v20. (Versions before 6 did not contain any DLC related interfaces)
* ISteamApps v2 — v8. (Version 1 did not contain any DLC related functions)
* ISteamUser v12 — v21. (Versions before 12 did not contain any DLC related functions)
* ISteamInventory v1 — v3.
Steam inventory does not work in all games with steam inventory because of custom implementation, and online checks.
A list of games where inventory emulation has been shown to work is as follows:
* Project Winter
* Euro Truck Simulator 2
* Hero Siege (if you bypass EAC)
====
== 🛠 Installation Instructions
WARNING: Please proceed with installation at your own risk.
Usage of this unlocker entails breaking one or more terms of service, which might result in a permanent loss of your account.
:smokeapi_release: https://github.com/acidicoala/SmokeAPI/releases/latest[SmokeAPI Releases]
SmokeAPI supports 2 main modes of installation: *Store* mode and *Game* mode, which are described in the next section.
NOTE: It is worth noting that the following instructions describe a _manual_ installation method.
You can benefit from _automatic_ installation and GUI configuration by using https://github.com/acidicoala/Koalageddon2#readme[Koalageddon v2].
=== 🛍️ Store mode
In this installation mode, SmokeAPI is loaded into the Steam process, which makes it able to affect all Steam games.
:steam-dir: the Steam directoryfootnote:fn-steam-dir[The root directory where Steam is installed]
. Download the latest Koaloader release zip from https://github.com/acidicoala/Koaloader/releases/latest[Koaloader Releases].
. From Koaloader archive unpack `version.dll` from `version-32`, and place it in {steam-dir}.
. In {steam-dir} create the following Koaloader configuration file:
+
.`Koaloader.config.json`
[%collapsible]
====
[source,json]
----
{
"auto_load": false,
"targets": [
"Steam.exe"
],
"modules": [
{
"path": "SmokeAPI.dll",
"required": true
}
]
}
----
====
. Download the latest SmokeAPI release zip from {smokeapi_release}.
. From SmokeAPI archive unpack `steam_api.dll`, rename it to `SmokeAPI.dll`, and place it in {steam-dir}.
=== 🎮 Game mode
In this installation mode, SmokeAPI is loaded into a game process, which limits it to that particular game only.
This mode itself supports 2 modes: hook mode and proxy mode.
Try installing the unlocker in hook mode first.
If it doesn't work, try installing it in proxy mode.
==== 🪝 Hook mode
. Download the latest Koaloader release zip from https://github.com/acidicoala/Koaloader/releases/latest[Koaloader Releases].
. From Koaloader archive unpack `version.dll` from version-32/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`/`steam_api64.dll`, depending on the game bitness, rename it to `SmokeAPI.dll`, and place it next to the game exe file.
==== 🔀 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`.
. 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.
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
NOTE: This document describes configuration for version 2 of SmokeAPI.
You can find the version 1 documentation https://github.com/acidicoala/SmokeAPI/blob/v1.0.3/README.md#-configuration[here].
:steam_config: https://github.com/acidicoala/public-entitlements/blob/main/steam/v2/store_config.json
:fn-app-id: footnote:fn-app-id[App/DLC IDs can be obtained from https://steamdb.info. 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.]
SmokeAPI does not require any manual configuration.
By default, it uses the most reasonable options and tries to unlock all DLCs that it can.
However, there might be circumstances in which you need more custom-tailored behaviour, such as disabling certain DLCs, or selectively enabling just a few of them.
In this case you can use a configuration file link:res/SmokeAPI.config.json[SmokeAPI.config.json] that you can find here in this repository or in the release zip.
To use it, simply place it next to the SmokeAPI DLL.
It will be read upon each launch of a game or the store.
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.
`logging`:: Toggles generation of a `SmokeAPI.log.log` file.
+
[horizontal]
Type::: Boolean
Default::: `false`
`unlock_family_sharing`:: Toggles Family Sharing bypass, which enables the borrower of a shared library to start and continue playing games when library owner is playing as well.
+
[horizontal]
Type::: Boolean
Default::: `true`
`default_app_status`:: This option sets the default DLC unlocking behaviour.
+
[horizontal]
Possible values:::
+
[horizontal]
`original`:::: Leaves DLC unlock status unmodified, unless specified otherwise.
`unlocked`:::: Unlocks all DLCs in all games, unless specified otherwise.
Type::: String
Default::: `unlocked`
`override_app_status`:: This option overrides the status of all DLCs that belong to a specified app ID{fn-app-id}.
+
[horizontal]
Possible values::: An object with key-value pairs, where the key corresponds to the app ID, and value to the app status.
Possible app status values are defined in the `default_app_status` option.
Type::: Object
Default::: `{}`
`override_dlc_status`:: This option overrides the status of individual DLCs, regardless of the corresponding app status.
+
[horizontal]
Possible values::: An object with key-value pairs, where the key corresponds to the app ID, and value to the app status.
Possible app status values are defined in the `default_app_status` option.
Furthermore, it is possible to lock even the legitimately locked DLCs by setting the corresponding app status value to `locked`.
Type::: Object (Map of String to String)
Default::: `{}`
`auto_inject_inventory`:: Toggles whether SmokeAPI should automatically inject a list of all registered inventory items, when a game queries user inventory
+
[horizontal]
Type::: Boolean
Default::: `true`
`extra_inventory_items`:: A list of inventory items IDs{fn-app-id} that will be added in addition to the automatically injected items.
+
[horizontal]
Type::: Array (of Integers)
Default::: `[]`
=== Advanced options
`$version`:: A technical field reserved for use by tools like GUI config editors.
Do not modify this value.
+
[horizontal]
Type::: Integer
Default::: `2`
`extra_dlcs`:: See <<How SmokeAPI works in games with large number of DLCs>> to understand the use case for this option.
+
[horizontal]
Possible values::: An object with key-value pairs, where the key corresponds to the app ID, and value to the object that contains DLC IDs.
The format is the same as in the aforementioned GitHub config.
Type::: Object (Map of String to Object)
Default::: `{}`
`store_config`:: An object that specifies offsets required for store mode operation.
It will override the config fetched from {steam_config}[remote source] or local cache.
Do not modify this value unless you know what you are doing.
+
[horizontal]
Type::: Object (Map of String to Integer)
Default::: See {steam_config}[online config]
== Extra info
=== How SmokeAPI works in games with large number of DLCs
Some games that have a large number of DLCs begin ownership verification by querying the Steamworks API for a list of all available DLCs.
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.
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.
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.
You can specify those missing DLC IDs there, and SmokeAPI will make them available to the game.
However, this introduces the need for manual configuration, which goes against the ideals of this project.
To remedy this issue SmokeAPI will also fetch a https://github.com/acidicoala/public-entitlements/blob/main/steam/v2/dlc.json[manually maintained list of extra DLCs] stored in a GitHub repository.
The purpose of this document is to contain all the DLC IDs that are lacking a Steam store page.
This enables SmokeAPI to unlock all DLCs without any config file at all.
Feel free to report in the {forum-topic} games that have more than 64 DLCs,
_and_ have DLCs without a dedicated store page.
They will be added to the list of missing DLC IDs to facilitate config-less operation.
== 🏗️ Building from source
=== 🚦 Requirements
:fn-lower-ver: footnote:lower-versions[Older versions may be supported as well.]
* CMake v3.24 (Make sure that cmake is available from powershell)
* Visual Studio 2022{fn-lower-ver}.
* Tested on Windows 11 SDK (10.0.22621.0){fn-lower-ver}.
=== 👨‍💻 Commands
Build the project
----
.\build.ps1 <arch> <config>
----
where
[horizontal]
arch::: `32` or `64`
config::: `Debug` or `Release`
For example:
----
.\build.ps1 32 Debug
----
== 👋 Acknowledgements
SmokeAPI makes use of the following open source projects:
* https://github.com/libcpr/cpr[C++ Requests]
* https://github.com/nlohmann/json[JSON for Modern C++]
* https://github.com/stevemk14ebr/PolyHook_2_0[PolyHook 2]
* https://github.com/gabime/spdlog[spdlog]
== 📄 License
This software is licensed under the https://unlicense.org/[Unlicense], terms of which are available in link:UNLICENSE.txt[UNLICENSE.txt]

113
README.md
View File

@@ -1,113 +0,0 @@
# 🐨 SmokeAPI ♨
**Legit DLC Unlocker for Steamworks**
___
**Features**
- 🔓 Legit DLC Unlocking
- 🪝 Hook mode and Proxy mode installation
- 📝 Configless operation
- 🛅 Inventory emulation
📥 [Download the latest release](https://github.com/acidicoala/SmokeAPI/releases/latest)
💬 [Official forum topic](https://cs.rin.ru/forum/viewtopic.php?p=2597932#p2597932)
## Introduction
<details><summary>What is SmokeAPI?</summary>
SmokeAPI is a DLC unlocker for the games that are legitimately owned in your Steam account. It attempts to fool games that use Steamworks SDK into thinking that you own the desired DLCs. However, SmokeAPI does not modify the rest of the Steamworks SDK, hence features like multiplayer, achievements, etc. remain fully functional.
</details>
<details><summary>Supported versions</summary>
SmokeAPI aims to support all released SteamAPI versions. When it encountered a new, unsupported interface version, it will fall back on the latest supported version. Below is a list of supported interface versions:
- ISteamClient v6—v20. (Versions before 6 did not contain any DLC related interfaces)
- ISteamApps v2—v8. (Version 1 did not contain any DLC related functions)
- ISteamUser v12—v21. (Versions before 12 did not contain any DLC related functions)
- ISteamInventory v1—v3.
Steam inventory does not work in all games with steam inventory because of custom implementation, and online checks.
The list of games where inventory emulation has been shown to work is as follows:
- Hero Siege
- Project Winter
- Euro Truck Simulator 2
- Bloons TD 6
</details>
## 🛠 Installation Instructions
This unlocker supports 2 modes of installation: *Hook* mode and *Proxy* mode.
Try installing the unlocker in hook mode first. If it doesn't work, try installing it in proxy mode.
#### 🪝 Hook mode
1. Download the latest Koaloader release zip from [Koaloader Releases].
2. From Koaloader archive unpack `version.dll` from version-32/64, depending on the game bitness, and place it next to the game exe file.
3. Download the latest SmokeAPI release zip from [SmokeAPI Releases].
4. From SmokeAPI archive unpack `steam_api.dll`/`steam_api64.dll`, depending on the game bitness, rename it to `SmokeAPI.dll`, and place it next to the game exe file.
#### 🔀 Proxy mode
1. Find `steam_api.dll`/`steam_api64.dll` file in game directory, and add `_o` to it's name, e.g. `steam_api64_o.dll`.
2. Download the latest SmokeAPI release zip from [SmokeAPI Releases].
3. 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.
If the unlocker is not working as expected, then please fully read the [Generic Unlocker Installation Instructions] before seeking help in the support forum.
[Koaloader Releases]: https://github.com/acidicoala/Koaloader/releases/latest
[SmokeAPI Releases]: https://github.com/acidicoala/SmokeAPI/releases/latest
[Generic Unlocker Installation Instructions]: https://gist.github.com/acidicoala/2c131cb90e251f97c0c1dbeaf2c174dc
## ⚙ Configuration
SmokeAPI does not require any manual configuration. By default, it uses the most reasonable options and tries to unlock all DLCs that it can. However, there might be circumstances in which you need more custom-tailored behaviour. In this case you can use a configuration file [SmokeAPI.json] that you can find here in this repository. To use it, simply place it next to the SmokeAPI DLL. It will be read upon each launch of a game. In the absence of the config file, default value specified below will be used.
| Option | Description | Type | Default |
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------|:-------:|
| `$version` | A technical field reserved for future use by tools like GUI config editors | Integer | `1` |
| `logging` | Toggles generation of `*.log` file | Boolean | `false` |
| `hook_steamclient` | When installed in hook mode, this option toggles between hooking steamclient(64).dll and steam_api(64).dll | Boolean | `true` |
| `unlock_all` | Toggles whether all DLCs should be unlocked by default | Boolean | `true` |
| `override` | When `unlock_all` is `true`, this option serves as a blacklist of DLC IDs, which should remain locked. When `unlock_all` is `false`, this option serves as a whitelist of DLC IDs, which should become unlocked | List of Integers | `[]` |
| `dlc_ids` | When game requests list of all DLCs from Steam and the number of registered DLCs is greater than 64, Steam may not return all of them. In this case, SmokeAPI will fetch all released DLCs from Web API. In some games, however (like Monster Hunter: World), web api also doesn't return all possible DLCs. To address this issue, you can specify the missing DLC IDs¹ in this option. For some games (including MH:W), however, it is not necessary because SmokeAPI will also automatically fetch a [manually maintained list of DLC IDs] that are missing from web api | List of Integers | `[]` |
| `auto_inject_inventory` | Toggles whether SmokeAPI should automatically inject a list of all registered inventory items, when a game queries user inventory | Boolean | `true` |
| `inventory_items` | A list of inventory items IDs¹ that will be added in addition to the automatically injected items | List of Integers | `[]` |
¹ DLC/Item IDs can be obtained from https://steamdb.info. You need to be logged in with your steam account in order to see accurate inventory item IDs.
[SmokeAPI.json]: res/SmokeAPI.json
[manually maintained list of DLC IDs]: https://github.com/acidicoala/public-entitlements/blob/main/steam/v1/dlc.json
## Extra info
### How SmokeAPI works in games with large number of DLCs
Some games that have a lot of DLCs begin ownership verification by querying the Steamworks API for a list of all available DLCs. 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 DLC 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. Unfortunately, even the web API does not solve all of our problems, because it will only return 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. That's where the `dlc_ids` config option comes into play. You can specify those missing DLC IDs there, and SmokeAPI will make them available to the game. However, this introduces the need for manual configuration, which goes against the ideals of this project. To remedy this issue SmokeAPI will also fetch [this document] stored in a GitHub repository. It contains all the DLC IDs missing from Steam store. The document is hand-crafted using data from https://steamdb.com. This enables SmokeAPI to unlock all DLCs without any config file at all. Feel free to report games that have more than 64 DLCs, *and* have DLCs without a dedicated store page. They will be added to the list of missing DLC IDs to facilitate configless operation.
[this document]: https://github.com/acidicoala/public-entitlements/blob/main/steam/v1/dlc.json
## 👋 Acknowledgements
SmokeAPI makes use of the following open source projects:
- [C++ Requests](https://github.com/libcpr/cpr)
- [JSON for Modern C++](https://github.com/nlohmann/json)
- [PolyHook 2](https://github.com/stevemk14ebr/PolyHook_2_0)
- [spdlog](https://github.com/gabime/spdlog)
## 📄 License
This software is licensed under [BSD Zero Clause License], terms of which are available in [LICENSE.txt]
[BSD Zero Clause License]: https://choosealicense.com/licenses/0bsd/
[LICENSE.txt]: LICENSE.txt

24
UNLICENSE.txt Normal file
View File

@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

View File

@@ -3,6 +3,6 @@
Set-Location (Get-Item $PSScriptRoot) Set-Location (Get-Item $PSScriptRoot)
. ./KoalaBox/build.ps1 @args . ./KoalaBox/build.ps1 SmokeAPI @args
Build-Project Build-Project

30
res/SmokeAPI.config.json Normal file
View File

@@ -0,0 +1,30 @@
{
"$version": 2,
"logging": true,
"unlock_family_sharing": true,
"default_app_status": "unlocked",
"override_app_status": {
"1234": "original",
"4321": "unlocked"
},
"override_dlc_status": {
"1234": "original",
"4321": "unlocked",
"5678": "locked"
},
"auto_inject_inventory": true,
"extra_inventory_items": [],
"extra_dlcs": {
"1234": {
"dlcs": {
"56789": "Example DLC 1"
}
},
"4321": {
"dlcs": {
"98765": "Example DLC 2"
}
}
},
"store_config": null
}

View File

@@ -1,10 +0,0 @@
{
"$version": 1,
"logging": true,
"hook_steamclient": true,
"unlock_all": true,
"override": [],
"dlc_ids": [],
"auto_inject_inventory": true,
"inventory_items": []
}

View File

@@ -1,5 +1,5 @@
#pragma once #pragma once
#define ORIGINAL_DLL "${ORIGINAL_DLL}" #define STEAMAPI_DLL "${STEAMAPI_DLL}"
#define STEAMCLIENT_DLL "${STEAMCLIENT_DLL}" #define STEAMCLIENT_DLL "${STEAMCLIENT_DLL}"
#define VSTDLIB_DLL "${VSTDLIB_DLL}" #define VSTDLIB_DLL "${VSTDLIB_DLL}"

52
src/common/app_cache.cpp Normal file
View File

@@ -0,0 +1,52 @@
#include <common/app_cache.hpp>
#include <core/paths.hpp>
#include <koalabox/cache.hpp>
#include <koalabox/logger.hpp>
constexpr auto KEY_APPS = "apps";
AppDlcNameMap get_cached_apps() noexcept {
try {
const auto cache = koalabox::cache::read_from_cache(KEY_APPS);
return cache.get<AppDlcNameMap>();
} catch (const Exception& e) {
LOG_WARN("Failed to get cached apps: {}", e.what())
return {};
}
}
namespace smoke_api::app_cache {
Vector<DLC> get_dlcs(AppId_t app_id) noexcept {
try {
LOG_DEBUG("Reading cached DLC IDs for the app: {}", app_id)
const auto apps = get_cached_apps();
return DLC::get_dlcs_from_apps(apps, app_id);
} catch (const Exception& e) {
LOG_ERROR("Error reading DLCs from disk cache: ", e.what())
return {};
}
}
bool save_dlcs(AppId_t app_id, const Vector<DLC>& dlcs) noexcept {
try {
LOG_DEBUG("Caching DLC IDs for the app: {}", app_id)
auto apps = get_cached_apps();
apps[std::to_string(app_id)] = App{.dlcs=DLC::get_dlc_map_from_vector(dlcs)};
return koalabox::cache::save_to_cache(KEY_APPS, Json(apps));
} catch (const Exception& e) {
LOG_ERROR("Error saving DLCs to disk cache: {}", e.what())
return false;
}
}
}

11
src/common/app_cache.hpp Normal file
View File

@@ -0,0 +1,11 @@
#pragma once
#include <core/types.hpp>
namespace smoke_api::app_cache {
Vector<DLC> get_dlcs(AppId_t app_id) noexcept;
bool save_dlcs(AppId_t app_id, const Vector<DLC>& dlcs) noexcept;
}

View File

@@ -0,0 +1,12 @@
#include <common/steamclient_exports.hpp>
#include <steam_impl/steam_client.hpp>
DLL_EXPORT(void*) CreateInterface(const char* interface_string, int* out_result) {
return steam_client::GetGenericInterface(
__func__, interface_string, [&]() {
GET_ORIGINAL_HOOKED_FUNCTION(CreateInterface)
return CreateInterface_o(interface_string, out_result);
}
);
}

View File

@@ -0,0 +1,3 @@
#include <core/types.hpp>
DLL_EXPORT(void*) CreateInterface(const char* interface_string, int* out_result);

46
src/core/api.cpp Normal file
View File

@@ -0,0 +1,46 @@
#include <core/api.hpp>
#include <koalabox/logger.hpp>
#include <koalabox/http_client.hpp>
namespace api {
struct SteamResponse {
uint32_t success = 0;
Vector<DLC> dlcs;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(SteamResponse, success, dlcs) // NOLINT(misc-const-correctness)
};
std::optional<Vector<DLC>> fetch_dlcs_from_github(AppId_t app_id) noexcept {
try {
const auto* url =
"https://raw.githubusercontent.com/acidicoala/public-entitlements/main/steam/v2/dlc.json";
const auto json = koalabox::http_client::fetch_json(url);
const auto response = json.get<AppDlcNameMap>();
return DLC::get_dlcs_from_apps(response, app_id);
} catch (const Json::exception& e) {
LOG_ERROR("Failed to fetch dlc list from GitHub: {}", e.what())
return std::nullopt;
}
}
std::optional<Vector<DLC>> fetch_dlcs_from_steam(AppId_t app_id) noexcept {
try {
const auto url = fmt::format("https://store.steampowered.com/dlc/{}/ajaxgetdlclist", app_id);
const auto json = koalabox::http_client::fetch_json(url);
const auto response = json.get<SteamResponse>();
if (response.success != 1) {
throw std::runtime_error("Web API responded with 'success' != 1");
}
return response.dlcs;
} catch (const Exception& e) {
LOG_ERROR("Failed to fetch dlc list from Steam: {}", e.what())
return std::nullopt;
}
}
}

11
src/core/api.hpp Normal file
View File

@@ -0,0 +1,11 @@
#pragma once
#include <core/types.hpp>
namespace api {
std::optional<Vector<DLC>> fetch_dlcs_from_github(AppId_t app_id) noexcept;
std::optional<Vector<DLC>> fetch_dlcs_from_steam(AppId_t app_id) noexcept;
}

11
src/core/globals.cpp Normal file
View File

@@ -0,0 +1,11 @@
#include <core/globals.hpp>
namespace globals {
// TODO: Refactor to koalabox?
HMODULE smokeapi_handle = nullptr;
HMODULE steamapi_module = nullptr;
HMODULE vstdlib_module = nullptr;
HMODULE steamclient_module = nullptr;
Map<String, uintptr_t> address_map; // NOLINT(cert-err58-cpp)
}

13
src/core/globals.hpp Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include <koalabox/core.hpp>
namespace globals {
extern HMODULE smokeapi_handle;
extern HMODULE steamclient_module;
extern HMODULE steamapi_module;
extern HMODULE vstdlib_module;
extern Map<String, uintptr_t> address_map;
}

28
src/core/paths.cpp Normal file
View File

@@ -0,0 +1,28 @@
#include <core/paths.hpp>
#include <core/globals.hpp>
#include <koalabox/loader.hpp>
// TODO: Refactor to KoalaBox
namespace paths {
Path get_self_path() {
static const auto path = koalabox::loader::get_module_dir(globals::smokeapi_handle);
return path;
}
Path get_config_path() {
static const auto path = get_self_path() / "SmokeAPI.config.json";
return path;
}
Path get_cache_path() {
static const auto path = get_self_path() / "SmokeAPI.cache.json";
return path;
}
Path get_log_path() {
static const auto path = get_self_path() / "SmokeAPI.log.log";
return path;
}
}

15
src/core/paths.hpp Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include <koalabox/core.hpp>
namespace paths {
/**
* @return An std::path instance representing the directory containing this DLL
*/
Path get_self_path();
Path get_config_path();
Path get_cache_path();
Path get_log_path();
}

26
src/core/types.cpp Normal file
View File

@@ -0,0 +1,26 @@
#include <core/types.hpp>
Vector<DLC> DLC::get_dlcs_from_apps(const AppDlcNameMap& apps, AppId_t app_id) {
Vector<DLC> dlcs;
const auto app_id_str = std::to_string(app_id);
if (apps.contains(app_id_str)) {
const auto& app = apps.at(app_id_str);
for (auto const& [id, name]: app.dlcs) {
dlcs.emplace_back(id, name);
}
}
return dlcs;
}
DlcNameMap DLC::get_dlc_map_from_vector(const Vector<DLC>& dlcs) {
DlcNameMap map;
for (const auto& dlc: dlcs) {
map[dlc.get_id_str()] = dlc.get_name();
}
return map;
}

140
src/core/types.hpp Normal file
View File

@@ -0,0 +1,140 @@
#pragma once
#include <nlohmann/json.hpp>
#include <core/globals.hpp>
#include <koalabox/core.hpp>
#include <koalabox/hook.hpp>
#include <koalabox/logger.hpp>
/**
* By default, virtual functions are declared with __thiscall
* convention, which is normal since they are class members.
* But it presents an issue for us, since we cannot pass *this
* pointer as a function argument. This is because *this
* pointer is passed via register ECX in __thiscall
* convention. Hence, to resolve this issue we declare our
* hooked functions with __fastcall convention, to trick
* the compiler into reading ECX & EDX registers as 1st
* and 2nd function arguments respectively. Similarly, __fastcall
* makes the compiler push the first argument into the ECX register,
* which mimics the __thiscall calling convention. Register EDX
* is not used anywhere in this case, but we still pass it along
* to conform to the __fastcall convention. This all applies
* to the x86 architecture.
*
* In x86-64 however, there is only one calling convention,
* so __fastcall is simply ignored. However, RDX in this case
* will store_mode the 1st actual argument to the function, so we
* have to omit it from the function signature.
*
* The macros below implement the above-mentioned considerations.
*/
#ifdef _WIN64
#define PARAMS(...) void* RCX, __VA_ARGS__
#define ARGS(...) RCX, __VA_ARGS__
#define THIS RCX
#else
#define PARAMS(...) const void* ECX, const void* EDX, __VA_ARGS__
#define ARGS(...) ECX, EDX, __VA_ARGS__
#define THIS ECX
#endif
// Names beginning with $ designate macros that are not meant to be used directly by the sources consuming this file
#define DLL_EXPORT(TYPE) extern "C" [[maybe_unused]] __declspec( dllexport ) TYPE __cdecl
#define VIRTUAL(TYPE) __declspec(noinline) TYPE __fastcall
#define GET_ORIGINAL_HOOKED_FUNCTION(FUNC) \
static const auto FUNC##_o = koalabox::hook::get_original_hooked_function(globals::address_map, #FUNC, FUNC);
#define GET_ORIGINAL_FUNCTION_STEAMAPI(FUNC) \
static const auto FUNC##_o = koalabox::hook::get_original_function(globals::steamapi_module, #FUNC, FUNC);
#define DETOUR_ADDRESS(FUNC, ADDRESS) \
koalabox::hook::detour_or_warn(globals::address_map, ADDRESS, #FUNC, reinterpret_cast<uintptr_t>(FUNC));
#define $DETOUR(FUNC, NAME, MODULE_HANDLE) \
koalabox::hook::detour_or_warn(globals::address_map, MODULE_HANDLE, NAME, reinterpret_cast<uintptr_t>(FUNC));
#define DETOUR_STEAMCLIENT(FUNC) $DETOUR(FUNC, #FUNC, globals::steamclient_module)
#define DETOUR_VSTDLIB(FUNC) $DETOUR(vstdlib::FUNC, #FUNC, globals::vstdlib_module)
#ifdef _WIN64
#define COMPILE_STORE_MODE 0
#else
#define COMPILE_STORE_MODE 1
#endif
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 CLIENT_ENGINE = "CLIENTENGINE_INTERFACE_VERSION";
using AppId_t = uint32_t;
using SteamInventoryResult_t = uint32_t;
using SteamItemInstanceID_t = uint64_t;
using SteamItemDef_t = uint32_t;
using HSteamPipe = uint32_t;
using HSteamUser = uint32_t;
using CSteamID = uint64_t;
using EResult = uint32_t;
struct SteamItemDetails_t {
SteamItemInstanceID_t m_itemId;
uint32_t m_iDefinition;
uint16_t m_unQuantity;
uint16_t m_unFlags; // see ESteamItemFlags
};
// results from UserHasLicenseForApp
enum EUserHasLicenseForAppResult {
k_EUserHasLicenseResultHasLicense = 0, // User has a license for specified app
k_EUserHasLicenseResultDoesNotHaveLicense = 1, // User does not have a license for the specified app
k_EUserHasLicenseResultNoAuth = 2, // User has not been authenticated
};
// These aliases exist solely to increase code readability
using AppIdKey = String;
using DlcIdKey = String;
using DlcNameValue = String;
using DlcNameMap = Map<DlcIdKey, DlcNameValue>;
struct App {
DlcNameMap dlcs;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(App, dlcs) // NOLINT(misc-const-correctness)
};
using AppDlcNameMap = Map<AppIdKey, App>;
class DLC {
private:
// These 2 names must match the property names from Steam API
String appid;
String name;
public:
explicit DLC() = default;
explicit DLC(String appid, String name) : appid{std::move(appid)}, name{std::move(name)} {}
[[nodiscard]] String get_id_str() const {
return appid;
};
[[nodiscard]] uint32_t get_id() const {
return std::stoi(appid);
};
[[nodiscard]] String get_name() const {
return name;
};
NLOHMANN_DEFINE_TYPE_INTRUSIVE(DLC, appid, name)
static Vector<DLC> get_dlcs_from_apps(const AppDlcNameMap& apps, AppId_t app_id);
static DlcNameMap get_dlc_map_from_vector(const Vector<DLC>& vector);
};

View File

@@ -0,0 +1,227 @@
#include <steam_impl/steam_apps.hpp>
#include <steam_impl/steam_client.hpp>
#include <steam_impl/steam_inventory.hpp>
#include <steam_impl/steam_user.hpp>
#include <steam_impl/steam_impl.hpp>
#include <koalabox/logger.hpp>
// ISteamApps
DLL_EXPORT(bool) SteamAPI_ISteamApps_BIsSubscribedApp(void* self, AppId_t dlcID) {
return steam_apps::IsDlcUnlocked(
__func__, 0, dlcID, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamAPI_ISteamApps_BIsSubscribedApp)
return SteamAPI_ISteamApps_BIsSubscribedApp_o(self, dlcID);
}
);
}
DLL_EXPORT(bool) SteamAPI_ISteamApps_BIsDlcInstalled(void* self, AppId_t dlcID) {
return steam_apps::IsDlcUnlocked(
__func__, 0, dlcID, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamAPI_ISteamApps_BIsDlcInstalled)
return SteamAPI_ISteamApps_BIsDlcInstalled_o(self, dlcID);
}
);
}
DLL_EXPORT(int) SteamAPI_ISteamApps_GetDLCCount(void* self) {
return steam_apps::GetDLCCount(
__func__, 0, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamAPI_ISteamApps_GetDLCCount)
return SteamAPI_ISteamApps_GetDLCCount_o(self);
}
);
}
DLL_EXPORT(bool) SteamAPI_ISteamApps_BGetDLCDataByIndex(
void* self,
int iDLC,
AppId_t* pDlcID,
bool* pbAvailable,
char* pchName,
int cchNameBufferSize
) {
return steam_apps::GetDLCDataByIndex(
__func__, 0, iDLC, pDlcID, pbAvailable, pchName, cchNameBufferSize,
[&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamAPI_ISteamApps_BGetDLCDataByIndex)
return SteamAPI_ISteamApps_BGetDLCDataByIndex_o(
self, iDLC, pDlcID, pbAvailable, pchName, cchNameBufferSize
);
},
[&](AppId_t dlc_id) {
return SteamAPI_ISteamApps_BIsDlcInstalled(self, dlc_id);
}
);
}
// ISteamClient
DLL_EXPORT(void*) SteamAPI_ISteamClient_GetISteamGenericInterface(
void* self,
HSteamUser hSteamUser,
HSteamPipe hSteamPipe,
const char* pchVersion
) {
return steam_client::GetGenericInterface(
__func__, pchVersion, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamAPI_ISteamClient_GetISteamGenericInterface)
return SteamAPI_ISteamClient_GetISteamGenericInterface_o(self, hSteamUser, hSteamPipe, pchVersion);
}
);
}
// ISteamInventory
DLL_EXPORT(EResult) SteamAPI_ISteamInventory_GetResultStatus(
void* self,
SteamInventoryResult_t resultHandle
) {
return steam_inventory::GetResultStatus(
__func__, resultHandle, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamAPI_ISteamInventory_GetResultStatus)
return SteamAPI_ISteamInventory_GetResultStatus_o(self, resultHandle);
}
);
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetItemDefinitionIDs(
void* self,
SteamItemDef_t* pItemDefIDs,
uint32_t* punItemDefIDsArraySize
) {
return steam_inventory::GetItemDefinitionIDs(
__func__, pItemDefIDs, punItemDefIDsArraySize, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamAPI_ISteamInventory_GetItemDefinitionIDs)
return SteamAPI_ISteamInventory_GetItemDefinitionIDs_o(self, pItemDefIDs, punItemDefIDsArraySize);
}
);
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetResultItems(
void* self,
SteamInventoryResult_t resultHandle,
SteamItemDetails_t* pOutItemsArray,
uint32_t* punOutItemsArraySize
) {
return steam_inventory::GetResultItems(
__func__, resultHandle, pOutItemsArray, punOutItemsArraySize,
[&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamAPI_ISteamInventory_GetResultItems)
return SteamAPI_ISteamInventory_GetResultItems_o(self, resultHandle, pOutItemsArray, punOutItemsArraySize);
},
[&](SteamItemDef_t* pItemDefIDs, uint32_t* punItemDefIDsArraySize) {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamAPI_ISteamInventory_GetItemDefinitionIDs)
return SteamAPI_ISteamInventory_GetItemDefinitionIDs_o(self, pItemDefIDs, punItemDefIDsArraySize);
}
);
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetResultItemProperty(
void* self,
SteamInventoryResult_t resultHandle,
uint32_t unItemIndex,
const char* pchPropertyName,
char* pchValueBuffer,
uint32_t* punValueBufferSizeOut
) {
return steam_inventory::GetResultItemProperty(
__func__, resultHandle, unItemIndex, pchPropertyName, pchValueBuffer, punValueBufferSizeOut, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamAPI_ISteamInventory_GetResultItemProperty)
return SteamAPI_ISteamInventory_GetResultItemProperty_o(
self, resultHandle, unItemIndex, pchPropertyName, pchValueBuffer, punValueBufferSizeOut
);
}
);
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_CheckResultSteamID(
void* self,
SteamInventoryResult_t resultHandle,
CSteamID steamIDExpected
) {
return steam_inventory::CheckResultSteamID(
__func__, resultHandle, steamIDExpected, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamAPI_ISteamInventory_CheckResultSteamID)
return SteamAPI_ISteamInventory_CheckResultSteamID_o(self, resultHandle, steamIDExpected);
}
);
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetAllItems(
void* self,
SteamInventoryResult_t* pResultHandle
) {
return steam_inventory::GetAllItems(
__func__, pResultHandle, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamAPI_ISteamInventory_GetAllItems)
return SteamAPI_ISteamInventory_GetAllItems_o(self, pResultHandle);
}
);
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetItemsByID(
void* self,
SteamInventoryResult_t* pResultHandle,
const SteamItemInstanceID_t* pInstanceIDs,
uint32_t unCountInstanceIDs
) {
return steam_inventory::GetItemsByID(
__func__, pResultHandle, pInstanceIDs, unCountInstanceIDs, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamAPI_ISteamInventory_GetItemsByID)
return SteamAPI_ISteamInventory_GetItemsByID_o(self, pResultHandle, pInstanceIDs, unCountInstanceIDs);
}
);
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_SerializeResult(
void* self,
SteamInventoryResult_t resultHandle,
void* pOutBuffer,
uint32_t* punOutBufferSize
) {
return steam_inventory::SerializeResult(
__func__, resultHandle, pOutBuffer, punOutBufferSize, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamAPI_ISteamInventory_SerializeResult)
return SteamAPI_ISteamInventory_SerializeResult_o(self, resultHandle, pOutBuffer, punOutBufferSize);
}
);
}
// ISteamUser
DLL_EXPORT(EUserHasLicenseForAppResult) SteamAPI_ISteamUser_UserHasLicenseForApp(
void* self,
CSteamID steamID,
AppId_t dlcID
) {
AppId_t app_id;
try {
app_id = steam_impl::get_app_id_or_throw();
} catch (const Exception& e) {
LOG_ERROR("{} -> Error getting app id: {}", __func__, e.what())
}
return steam_user::UserHasLicenseForApp(
__func__, app_id, dlcID, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamAPI_ISteamUser_UserHasLicenseForApp)
return SteamAPI_ISteamUser_UserHasLicenseForApp_o(self, steamID, dlcID);
}
);
}

View File

@@ -0,0 +1,21 @@
#include <steam_impl/steam_client.hpp>
DLL_EXPORT(void*) SteamInternal_FindOrCreateUserInterface(HSteamUser hSteamUser, const char* version) {
return steam_client::GetGenericInterface(
__func__, version, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamInternal_FindOrCreateUserInterface)
return SteamInternal_FindOrCreateUserInterface_o(hSteamUser, version);
}
);
}
DLL_EXPORT(void*) SteamInternal_CreateInterface(const char* version) {
return steam_client::GetGenericInterface(
__func__, version, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamInternal_CreateInterface)
return SteamInternal_CreateInterface_o(version);
}
);
}

View File

@@ -1,14 +1,9 @@
#include <smoke_api/smoke_api.hpp> #include <steam_impl/steam_client.hpp>
#include <steam_impl/steam_impl.hpp> #include <koalabox/logger.hpp>
#include <steam_functions/steam_functions.hpp>
#include <koalabox/win_util.hpp> #include <koalabox/win_util.hpp>
#include <koalabox/util.hpp>
#include <regex> #include <regex>
using namespace smoke_api;
using namespace steam_functions;
/** /**
* Searches the `.rdata` section of the original dll for the full interface version string * Searches the `.rdata` section of the original dll for the full interface version string
* Results are cached for performance. * Results are cached for performance.
@@ -18,21 +13,21 @@ String get_versioned_interface(const String& version_prefix, const String& fallb
if (not version_map.contains(version_prefix)) { if (not version_map.contains(version_prefix)) {
try { try {
String rdata = win_util::get_pe_section_data_or_throw(original_library, ".rdata"); const String rdata = koalabox::win_util::get_pe_section_data_or_throw(globals::steamapi_module, ".rdata");
std::regex regex(version_prefix + "\\d{3}"); const std::regex regex(version_prefix + "\\d{3}");
std::smatch match; std::smatch match;
if (std::regex_search(rdata, match, regex)) { if (std::regex_search(rdata, match, regex)) {
version_map[version_prefix] = match[0]; version_map[version_prefix] = match[0];
return version_map[version_prefix]; return version_map[version_prefix];
} }
throw util::exception("No match found for '{}'", version_prefix); throw koalabox::util::exception("No match found for '{}'", version_prefix);
} catch (const Exception& ex) { } catch (const Exception& ex) {
logger->error( LOG_ERROR(
"Failed to get versioned interface: {}." "Failed to get versioned interface: {}."
"Falling back to version {}", ex.what(), fallback "Falling back to version {}", ex.what(), fallback
); )
version_map[version_prefix] = version_prefix + fallback; version_map[version_prefix] = version_prefix + fallback;
} }
@@ -44,40 +39,47 @@ String get_versioned_interface(const String& version_prefix, const String& fallb
DLL_EXPORT(void*) SteamClient() { DLL_EXPORT(void*) SteamClient() {
static auto version = get_versioned_interface(STEAM_CLIENT, "006"); static auto version = get_versioned_interface(STEAM_CLIENT, "006");
return steam_client::GetGenericInterface(__func__, version, [&]() { return steam_client::GetGenericInterface(
GET_ORIGINAL_FUNCTION(SteamClient) __func__, version, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamClient)
return SteamClient_o(); return SteamClient_o();
}); }
);
} }
DLL_EXPORT(void*) SteamApps() { DLL_EXPORT(void*) SteamApps() {
static auto version = get_versioned_interface(STEAM_APPS, "002"); static auto version = get_versioned_interface(STEAM_APPS, "002");
return steam_client::GetGenericInterface(__func__, version, [&]() { return steam_client::GetGenericInterface(
GET_ORIGINAL_FUNCTION(SteamApps) __func__, version, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamApps)
return SteamApps_o(); return SteamApps_o();
}); }
);
} }
DLL_EXPORT(void*) SteamUser() { DLL_EXPORT(void*) SteamUser() {
static auto version = get_versioned_interface(STEAM_USER, "012"); static auto version = get_versioned_interface(STEAM_USER, "012");
return steam_client::GetGenericInterface(__func__, version, [&]() { return steam_client::GetGenericInterface(
GET_ORIGINAL_FUNCTION(SteamUser) __func__, version, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamUser)
return SteamUser_o(); return SteamUser_o();
}); }
);
} }
DLL_EXPORT(void*) SteamInventory() { DLL_EXPORT(void*) SteamInventory() {
static auto version = get_versioned_interface(STEAM_INVENTORY, "001"); static auto version = get_versioned_interface(STEAM_INVENTORY, "001");
return steam_client::GetGenericInterface(__func__, version, [&]() { return steam_client::GetGenericInterface(
GET_ORIGINAL_FUNCTION(SteamInventory) __func__, version, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamInventory)
return SteamInventory_o(); return SteamInventory_o();
}); }
);
} }

View File

@@ -0,0 +1,56 @@
#include <game_mode/virtuals/steam_api_virtuals.hpp>
#include <steam_impl/steam_apps.hpp>
VIRTUAL(bool) ISteamApps_BIsSubscribedApp(PARAMS(AppId_t appID)) {
return steam_apps::IsDlcUnlocked(
__func__, 0, appID, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(ISteamApps_BIsSubscribedApp)
return ISteamApps_BIsSubscribedApp_o(ARGS(appID));
}
);
}
VIRTUAL(bool) ISteamApps_BIsDlcInstalled(PARAMS(AppId_t appID)) {
return steam_apps::IsDlcUnlocked(
__func__, 0, appID, [&]() {
GET_ORIGINAL_FUNCTION_STEAMAPI(ISteamApps_BIsDlcInstalled)
return ISteamApps_BIsDlcInstalled_o(ARGS(appID));
}
);
}
VIRTUAL(int) ISteamApps_GetDLCCount(PARAMS()) {
GET_ORIGINAL_HOOKED_FUNCTION(ISteamApps_GetDLCCount)
return steam_apps::GetDLCCount(
__func__, 0, [&]() {
return ISteamApps_GetDLCCount_o(ARGS());
}
);
}
VIRTUAL(bool) ISteamApps_BGetDLCDataByIndex(
PARAMS(
int iDLC,
AppId_t* pAppID,
bool* pbAvailable,
char* pchName,
int cchNameBufferSize
)
) {
return steam_apps::GetDLCDataByIndex(
__func__, 0, iDLC, pAppID, pbAvailable, pchName, cchNameBufferSize,
[&]() {
GET_ORIGINAL_HOOKED_FUNCTION(ISteamApps_BGetDLCDataByIndex)
return ISteamApps_BGetDLCDataByIndex_o(
ARGS(iDLC, pAppID, pbAvailable, pchName, cchNameBufferSize)
);
},
[&](AppId_t dlc_id) {
return ISteamApps_BIsDlcInstalled(ARGS(dlc_id));
}
);
}

View File

@@ -0,0 +1,66 @@
#include <game_mode/virtuals/steam_api_virtuals.hpp>
#include <steam_impl/steam_client.hpp>
VIRTUAL(void*) ISteamClient_GetISteamApps(
PARAMS(
HSteamUser hSteamUser,
HSteamPipe hSteamPipe,
const char* version
)
) {
return steam_client::GetGenericInterface(
__func__, version, [&]() {
GET_ORIGINAL_HOOKED_FUNCTION(ISteamClient_GetISteamApps)
return ISteamClient_GetISteamApps_o(ARGS(hSteamUser, hSteamPipe, version));
}
);
}
VIRTUAL(void*) ISteamClient_GetISteamUser(
PARAMS(
HSteamUser hSteamUser,
HSteamPipe hSteamPipe,
const char* version
)
) {
return steam_client::GetGenericInterface(
__func__, version, [&]() {
GET_ORIGINAL_HOOKED_FUNCTION(ISteamClient_GetISteamUser)
return ISteamClient_GetISteamUser_o(ARGS(hSteamUser, hSteamPipe, version));
}
);
}
VIRTUAL(void*) ISteamClient_GetISteamGenericInterface(
PARAMS(
HSteamUser hSteamUser,
HSteamPipe hSteamPipe,
const char* pchVersion
)
) {
return steam_client::GetGenericInterface(
__func__, pchVersion, [&]() {
GET_ORIGINAL_HOOKED_FUNCTION(ISteamClient_GetISteamGenericInterface)
return ISteamClient_GetISteamGenericInterface_o(ARGS(hSteamUser, hSteamPipe, pchVersion));
}
);
}
VIRTUAL(void*) ISteamClient_GetISteamInventory(
PARAMS(
HSteamUser hSteamUser,
HSteamPipe hSteamPipe,
const char* pchVersion
)
) {
return steam_client::GetGenericInterface(
__func__, pchVersion, [&]() {
GET_ORIGINAL_HOOKED_FUNCTION(ISteamClient_GetISteamInventory)
return ISteamClient_GetISteamInventory_o(ARGS(hSteamUser, hSteamPipe, pchVersion));
}
);
}

View File

@@ -1,14 +1,14 @@
#include <smoke_api/smoke_api.hpp> #include <game_mode/virtuals/steam_api_virtuals.hpp>
#include <steam_impl/steam_impl.hpp> #include <steam_impl/steam_inventory.hpp>
using namespace smoke_api;
VIRTUAL(EResult) ISteamInventory_GetResultStatus(PARAMS(SteamInventoryResult_t resultHandle)) { VIRTUAL(EResult) ISteamInventory_GetResultStatus(PARAMS(SteamInventoryResult_t resultHandle)) {
return steam_inventory::GetResultStatus(__func__, resultHandle, [&]() { return steam_inventory::GetResultStatus(
GET_ORIGINAL_VIRTUAL_FUNCTION(ISteamInventory_GetResultStatus) __func__, resultHandle, [&]() {
GET_ORIGINAL_HOOKED_FUNCTION(ISteamInventory_GetResultStatus)
return ISteamInventory_GetResultStatus_o(ARGS(resultHandle)); return ISteamInventory_GetResultStatus_o(ARGS(resultHandle));
}); }
);
} }
VIRTUAL(bool) ISteamInventory_GetResultItems( VIRTUAL(bool) ISteamInventory_GetResultItems(
@@ -18,16 +18,15 @@ VIRTUAL(bool) ISteamInventory_GetResultItems(
uint32_t * punOutItemsArraySize uint32_t * punOutItemsArraySize
) )
) { ) {
return steam_inventory::GetResultItems( return steam_inventory::GetResultItems(
__func__, resultHandle, pOutItemsArray, punOutItemsArraySize, __func__, resultHandle, pOutItemsArray, punOutItemsArraySize,
[&]() { [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(ISteamInventory_GetResultItems) GET_ORIGINAL_HOOKED_FUNCTION(ISteamInventory_GetResultItems)
return ISteamInventory_GetResultItems_o(ARGS(resultHandle, pOutItemsArray, punOutItemsArraySize)); return ISteamInventory_GetResultItems_o(ARGS(resultHandle, pOutItemsArray, punOutItemsArraySize));
}, },
[&](SteamItemDef_t* pItemDefIDs, uint32_t* punItemDefIDsArraySize) { [&](SteamItemDef_t* pItemDefIDs, uint32_t* punItemDefIDsArraySize) {
GET_ORIGINAL_VIRTUAL_FUNCTION(ISteamInventory_GetItemDefinitionIDs) GET_ORIGINAL_HOOKED_FUNCTION(ISteamInventory_GetItemDefinitionIDs)
return ISteamInventory_GetItemDefinitionIDs_o(ARGS(pItemDefIDs, punItemDefIDsArraySize)); return ISteamInventory_GetItemDefinitionIDs_o(ARGS(pItemDefIDs, punItemDefIDsArraySize));
} }
@@ -45,7 +44,7 @@ VIRTUAL(bool) ISteamInventory_GetResultItemProperty(
) { ) {
return steam_inventory::GetResultItemProperty( return steam_inventory::GetResultItemProperty(
__func__, resultHandle, unItemIndex, pchPropertyName, pchValueBuffer, punValueBufferSizeOut, [&]() { __func__, resultHandle, unItemIndex, pchPropertyName, pchValueBuffer, punValueBufferSizeOut, [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(ISteamInventory_GetResultItemProperty) GET_ORIGINAL_HOOKED_FUNCTION(ISteamInventory_GetResultItemProperty)
return ISteamInventory_GetResultItemProperty_o( return ISteamInventory_GetResultItemProperty_o(
ARGS(resultHandle, unItemIndex, pchPropertyName, pchValueBuffer, punValueBufferSizeOut) ARGS(resultHandle, unItemIndex, pchPropertyName, pchValueBuffer, punValueBufferSizeOut)
@@ -60,19 +59,23 @@ VIRTUAL(bool) ISteamInventory_CheckResultSteamID(
CSteamID steamIDExpected CSteamID steamIDExpected
) )
) { ) {
return steam_inventory::CheckResultSteamID(__func__, resultHandle, steamIDExpected, [&]() { return steam_inventory::CheckResultSteamID(
GET_ORIGINAL_VIRTUAL_FUNCTION(ISteamInventory_CheckResultSteamID) __func__, resultHandle, steamIDExpected, [&]() {
GET_ORIGINAL_HOOKED_FUNCTION(ISteamInventory_CheckResultSteamID)
return ISteamInventory_CheckResultSteamID_o(ARGS(resultHandle, steamIDExpected)); return ISteamInventory_CheckResultSteamID_o(ARGS(resultHandle, steamIDExpected));
}); }
);
} }
VIRTUAL(bool) ISteamInventory_GetAllItems(PARAMS(SteamInventoryResult_t* pResultHandle)) { VIRTUAL(bool) ISteamInventory_GetAllItems(PARAMS(SteamInventoryResult_t* pResultHandle)) {
return steam_inventory::GetAllItems(__func__, pResultHandle, [&]() { return steam_inventory::GetAllItems(
GET_ORIGINAL_VIRTUAL_FUNCTION(ISteamInventory_GetAllItems) __func__, pResultHandle, [&]() {
GET_ORIGINAL_HOOKED_FUNCTION(ISteamInventory_GetAllItems)
return ISteamInventory_GetAllItems_o(ARGS(pResultHandle)); return ISteamInventory_GetAllItems_o(ARGS(pResultHandle));
}); }
);
} }
VIRTUAL(bool) ISteamInventory_GetItemsByID( VIRTUAL(bool) ISteamInventory_GetItemsByID(
@@ -82,11 +85,13 @@ VIRTUAL(bool) ISteamInventory_GetItemsByID(
uint32_t unCountInstanceIDs uint32_t unCountInstanceIDs
) )
) { ) {
return steam_inventory::GetItemsByID(__func__, pResultHandle, pInstanceIDs, unCountInstanceIDs, [&]() { return steam_inventory::GetItemsByID(
GET_ORIGINAL_VIRTUAL_FUNCTION(ISteamInventory_GetItemsByID) __func__, pResultHandle, pInstanceIDs, unCountInstanceIDs, [&]() {
GET_ORIGINAL_HOOKED_FUNCTION(ISteamInventory_GetItemsByID)
return ISteamInventory_GetItemsByID_o(ARGS(pResultHandle, pInstanceIDs, unCountInstanceIDs)); return ISteamInventory_GetItemsByID_o(ARGS(pResultHandle, pInstanceIDs, unCountInstanceIDs));
}); }
);
} }
VIRTUAL(bool) ISteamInventory_SerializeResult( VIRTUAL(bool) ISteamInventory_SerializeResult(
@@ -96,11 +101,13 @@ VIRTUAL(bool) ISteamInventory_SerializeResult(
uint32_t * punOutBufferSize uint32_t * punOutBufferSize
) )
) { ) {
return steam_inventory::SerializeResult(__func__, resultHandle, pOutBuffer, punOutBufferSize, [&]() { return steam_inventory::SerializeResult(
GET_ORIGINAL_VIRTUAL_FUNCTION(ISteamInventory_SerializeResult) __func__, resultHandle, pOutBuffer, punOutBufferSize, [&]() {
GET_ORIGINAL_HOOKED_FUNCTION(ISteamInventory_SerializeResult)
return ISteamInventory_SerializeResult_o(ARGS(resultHandle, pOutBuffer, punOutBufferSize)); return ISteamInventory_SerializeResult_o(ARGS(resultHandle, pOutBuffer, punOutBufferSize));
}); }
);
} }
VIRTUAL(bool) ISteamInventory_GetItemDefinitionIDs( VIRTUAL(bool) ISteamInventory_GetItemDefinitionIDs(
@@ -109,9 +116,11 @@ VIRTUAL(bool) ISteamInventory_GetItemDefinitionIDs(
uint32_t * punItemDefIDsArraySize uint32_t * punItemDefIDsArraySize
) )
) { ) {
return steam_inventory::GetItemDefinitionIDs(__func__, pItemDefIDs, punItemDefIDsArraySize, [&]() { return steam_inventory::GetItemDefinitionIDs(
GET_ORIGINAL_VIRTUAL_FUNCTION(ISteamInventory_GetItemDefinitionIDs) __func__, pItemDefIDs, punItemDefIDsArraySize, [&]() {
GET_ORIGINAL_HOOKED_FUNCTION(ISteamInventory_GetItemDefinitionIDs)
return ISteamInventory_GetItemDefinitionIDs_o(ARGS(pItemDefIDs, punItemDefIDsArraySize)); return ISteamInventory_GetItemDefinitionIDs_o(ARGS(pItemDefIDs, punItemDefIDsArraySize));
}); }
);
} }

View File

@@ -0,0 +1,21 @@
#include <game_mode/virtuals/steam_api_virtuals.hpp>
#include <steam_impl/steam_user.hpp>
#include <steam_impl/steam_impl.hpp>
#include <koalabox/logger.hpp>
VIRTUAL(EUserHasLicenseForAppResult) ISteamUser_UserHasLicenseForApp(PARAMS(CSteamID steamID, AppId_t dlcID)) {
AppId_t app_id = 0;
try {
app_id = steam_impl::get_app_id_or_throw();
} catch (const Exception& e) {
LOG_ERROR("{} -> Error getting app id: {}", __func__, e.what())
}
return steam_user::UserHasLicenseForApp(
__func__, app_id, dlcID, [&]() {
GET_ORIGINAL_HOOKED_FUNCTION(ISteamUser_UserHasLicenseForApp)
return ISteamUser_UserHasLicenseForApp_o(ARGS(steamID, dlcID));
}
);
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <core/types.hpp>
// ISteamApps
VIRTUAL(bool) ISteamApps_BIsSubscribedApp(PARAMS(AppId_t));
VIRTUAL(bool) ISteamApps_BIsDlcInstalled(PARAMS(AppId_t));
VIRTUAL(int) ISteamApps_GetDLCCount(PARAMS());
VIRTUAL(bool) ISteamApps_BGetDLCDataByIndex(PARAMS(int, AppId_t*, bool*, char*, int));
// ISteamClient
VIRTUAL(void*) ISteamClient_GetISteamApps(PARAMS(HSteamUser, HSteamPipe, const char*));
VIRTUAL(void*) ISteamClient_GetISteamUser(PARAMS(HSteamUser, HSteamPipe, const char*));
VIRTUAL(void*) ISteamClient_GetISteamGenericInterface(PARAMS(HSteamUser, HSteamPipe, const char*));
VIRTUAL(void*) ISteamClient_GetISteamInventory(PARAMS(HSteamUser, HSteamPipe, const char*));
// ISteamInventory
VIRTUAL(EResult) ISteamInventory_GetResultStatus(PARAMS(SteamInventoryResult_t));
VIRTUAL(bool) ISteamInventory_GetResultItems(PARAMS(SteamInventoryResult_t, SteamItemDetails_t*, uint32_t*));
VIRTUAL(bool) ISteamInventory_GetResultItemProperty(
PARAMS(SteamInventoryResult_t, uint32_t, const char*, char*, uint32_t*)
);
VIRTUAL(bool) ISteamInventory_GetAllItems(PARAMS(SteamInventoryResult_t*));
VIRTUAL(bool) ISteamInventory_GetItemsByID(PARAMS(SteamInventoryResult_t*, const SteamItemInstanceID_t*, uint32_t));
VIRTUAL(bool) ISteamInventory_SerializeResult(PARAMS(SteamInventoryResult_t, void*, uint32_t*));
VIRTUAL(bool) ISteamInventory_GetItemDefinitionIDs(PARAMS(SteamItemDef_t*, uint32_t*));
VIRTUAL(bool) ISteamInventory_CheckResultSteamID(PARAMS(SteamInventoryResult_t, CSteamID));
// ISteamUser
VIRTUAL(EUserHasLicenseForAppResult) ISteamUser_UserHasLicenseForApp(PARAMS(CSteamID, AppId_t));

View File

@@ -1,54 +0,0 @@
#include <smoke_api/smoke_api.hpp>
#include <steam_functions/steam_functions.hpp>
#include <koalabox/hook.hpp>
using namespace smoke_api;
DLL_EXPORT(void) Log_Interface(const char* interface_name, const char* function_name) {
try {
void**** parent_ebp;
__asm mov parent_ebp, ebp
auto* interface_address = (*parent_ebp)[2];
static Set<String> hooked_functions;
auto hook_function = [&](const auto hook_function, const String& name, const int ordinal) {
if (hooked_functions.contains(name)) {
return;
}
hook::swap_virtual_func_or_throw(interface_address, name, ordinal, (FunctionAddress) (hook_function));
hooked_functions.insert(name);
};
const auto compound_name = interface_name + String("::") + function_name;
#define HOOK(FUNC, ORDINAL) hook_function(FUNC, #FUNC, ORDINAL);
if (util::strings_are_equal(interface_name, "IClientAppManager")) {
HOOK(IClientAppManager_IsAppDlcInstalled, 8)
} else if (util::strings_are_equal(interface_name, "IClientApps")) {
HOOK(IClientApps_GetDLCCount, 8)
HOOK(IClientApps_BGetDLCDataByIndex, 9)
} else if (util::strings_are_equal(interface_name, "IClientInventory")) {
HOOK(IClientInventory_GetResultStatus, 0)
HOOK(IClientInventory_GetResultItems, 2)
HOOK(IClientInventory_GetResultItemProperty, 3)
HOOK(IClientInventory_CheckResultSteamID, 5)
HOOK(IClientInventory_GetAllItems, 8)
HOOK(IClientInventory_GetItemsByID, 9)
HOOK(IClientInventory_SerializeResult, 6)
HOOK(IClientInventory_GetItemDefinitionIDs, 19)
}
GET_ORIGINAL_FUNCTION(Log_Interface)
Log_Interface_o(interface_name, function_name);
} catch (const Exception& ex) {
logger->error("{} -> Error: {}", __func__, ex.what());
}
}

View File

@@ -1,83 +0,0 @@
#include <smoke_api/smoke_api.hpp>
#include <steam_functions/steam_functions.hpp>
#include <koalabox/hook.hpp>
using namespace smoke_api;
#define DETOUR(FUNC, address) hook::detour((FunctionAddress) (address), #FUNC, (FunctionAddress) (FUNC));
VIRTUAL(bool) SharedLicensesLockStatus(PARAMS(void* arg)) { // NOLINT(misc-unused-parameters)
logger->debug("{} -> instance: {}, arg: {}", __func__, fmt::ptr(THIS), fmt::ptr(arg));
return true;
}
VIRTUAL(bool) SharedLibraryStopPlaying(PARAMS(void* arg)) { // NOLINT(misc-unused-parameters)
logger->debug("{} -> instance: {}, arg: {}", __func__, fmt::ptr(THIS), fmt::ptr(arg));
return true;
}
struct CallbackData {
[[maybe_unused]] void* pad1[1];
void* set_callback_name_address; // to_do: fetch online
[[maybe_unused]] void* pad19[17];
void* callback_address; // to_do: fetch online
};
struct CoroutineData {
CallbackData* callback_data; // to_do: fetch online
[[maybe_unused]] uint32_t pad4[3];
const char* callback_name; // to_do: fetch online
};
VIRTUAL(void) set_callback_name(PARAMS(const char** p_name)) {
GET_ORIGINAL_FUNCTION(set_callback_name)
set_callback_name_o(ARGS(p_name));
static auto hooked_functions = 0;
if (hooked_functions == 2) {
return;
}
auto* const data = (CoroutineData*) THIS;
if (data && data->callback_name) {
const auto name = String(data->callback_name);
// logger->trace("{} -> instance: {}, name: '{}'", __func__, fmt::ptr(THIS), name);
if (name == "SharedLicensesLockStatus") {
static std::once_flag flag;
std::call_once(flag, [&]() {
DETOUR(SharedLicensesLockStatus, data->callback_data->callback_address)
hooked_functions++;
});
} else if (name == "SharedLibraryStopPlaying") {
static std::once_flag flag;
std::call_once(flag, [&]() {
DETOUR(SharedLibraryStopPlaying, data->callback_data->callback_address)
hooked_functions++;
});
}
}
}
/**
* Initially, callback data passed into this function is not complete,
* hence we must hook an interface method that sets the callback name.
*/
DLL_EXPORT(HCoroutine) Coroutine_Create(void* callback_address, CoroutineData* data) {
GET_ORIGINAL_FUNCTION(Coroutine_Create)
const auto result = Coroutine_Create_o(callback_address, data);
// Coroutine callback appears to be always the same
static std::once_flag flag;
std::call_once(flag, [&]() {
logger->debug("Coroutine_Create -> callback: {}, data: {}", callback_address, fmt::ptr(data));
DETOUR(set_callback_name, data->callback_data->set_callback_name_address)
});
return result;
}

View File

@@ -3,9 +3,9 @@
// This header will be populated at build time // This header will be populated at build time
#include <linker_exports.h> #include <linker_exports.h>
EXTERN_C [[maybe_unused]] BOOL WINAPI DllMain(HMODULE module, DWORD reason, LPVOID) { EXTERN_C [[maybe_unused]] BOOL WINAPI DllMain(HMODULE module_handle, DWORD reason, LPVOID) {
if (reason == DLL_PROCESS_ATTACH) { if (reason == DLL_PROCESS_ATTACH) {
smoke_api::init(module); smoke_api::init(module_handle);
} else if (reason == DLL_PROCESS_DETACH) { } else if (reason == DLL_PROCESS_DETACH) {
smoke_api::shutdown(); smoke_api::shutdown();
} }

72
src/smoke_api/config.cpp Normal file
View File

@@ -0,0 +1,72 @@
#include <smoke_api/config.hpp>
#include <core/paths.hpp>
#include <koalabox/util.hpp>
#include <koalabox/io.hpp>
#include <koalabox/logger.hpp>
namespace smoke_api::config {
Config instance; // NOLINT(cert-err58-cpp)
// TODO: Refactor to Koalabox
void init_config() {
const auto path = paths::get_config_path();
if (exists(path)) {
try {
const auto config_str = koalabox::io::read_file(path);
instance = Json::parse(config_str).get<Config>();
LOG_DEBUG("Parsed config:\n{}", Json(instance).dump(2))
} catch (const Exception& e) {
const auto message = fmt::format("Error parsing config file: {}", e.what());
koalabox::util::error_box("SmokeAPI Error", message);
}
}
}
Vector<DLC> get_extra_dlcs(AppId_t app_id) {
return DLC::get_dlcs_from_apps(instance.extra_dlcs, app_id);
}
bool is_dlc_unlocked(AppId_t app_id, AppId_t dlc_id, const Function<bool()>& original_function) {
auto status = instance.default_app_status;
const auto app_id_str = std::to_string(app_id);
if (instance.override_app_status.contains(app_id_str)) {
status = instance.override_app_status[app_id_str];
}
const auto dlc_id_str = std::to_string(dlc_id);
if (instance.override_dlc_status.contains(dlc_id_str)) {
status = instance.override_dlc_status[dlc_id_str];
}
bool is_unlocked;
switch (status) {
case AppStatus::UNLOCKED:
is_unlocked = true;
break;
case AppStatus::LOCKED:
is_unlocked = false;
break;
case AppStatus::ORIGINAL:
case AppStatus::UNDEFINED:
is_unlocked = original_function();
break;
}
LOG_TRACE(
"App ID: {}, DLC ID: {}, Status: {}, Original: {}, Unlocked: {}",
app_id_str, dlc_id_str, Json(status).dump(), original_function(), is_unlocked
)
return is_unlocked;
}
DLL_EXPORT(void) ReloadConfig() {
LOG_INFO("Reloading config")
init_config();
}
}

58
src/smoke_api/config.hpp Normal file
View File

@@ -0,0 +1,58 @@
#pragma once
#include <core/types.hpp>
namespace smoke_api::config {
enum class AppStatus {
UNDEFINED,
ORIGINAL,
UNLOCKED,
LOCKED,
};
NLOHMANN_JSON_SERIALIZE_ENUM(AppStatus, {
{ AppStatus::UNDEFINED, nullptr },
{ AppStatus::ORIGINAL, "original" },
{ AppStatus::UNLOCKED, "unlocked" },
{ AppStatus::LOCKED, "locked" },
})
struct Config {
uint32_t $version = 2;
bool logging = false;
bool unlock_family_sharing = true;
AppStatus default_app_status = AppStatus::UNLOCKED;
Map<String, AppStatus> override_app_status;
Map<String, AppStatus> override_dlc_status;
AppDlcNameMap extra_dlcs;
bool auto_inject_inventory = true;
Vector<uint32_t> extra_inventory_items;
// We have to use general json type here since the library doesn't support std::optional
Json store_config;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(
Config, // NOLINT(misc-const-correctness)
$version,
logging,
unlock_family_sharing,
default_app_status,
override_app_status,
override_dlc_status,
extra_dlcs,
auto_inject_inventory,
extra_inventory_items,
store_config
)
};
extern Config instance;
void init_config();
Vector<DLC> get_extra_dlcs(AppId_t app_id);
bool is_dlc_unlocked(uint32_t app_id, uint32_t dlc_id, const Function<bool()>& original_function);
DLL_EXPORT(void) ReloadConfig();
}

View File

@@ -1,141 +1,136 @@
#include <smoke_api/smoke_api.hpp> #include <smoke_api/smoke_api.hpp>
#include <steam_functions/steam_functions.hpp>
#include <build_config.h> #include <build_config.h>
#include <smoke_api/config.hpp>
#include <koalabox/config_parser.hpp> #include <core/globals.hpp>
#include <core/paths.hpp>
#include <common/steamclient_exports.hpp>
#include <koalabox/dll_monitor.hpp> #include <koalabox/dll_monitor.hpp>
#include <koalabox/file_logger.hpp> #include <koalabox/logger.hpp>
#include <koalabox/hook.hpp> #include <koalabox/hook.hpp>
#include <koalabox/cache.hpp>
#include <koalabox/loader.hpp> #include <koalabox/loader.hpp>
#include <koalabox/win_util.hpp> #include <koalabox/win_util.hpp>
#ifndef _WIN64 #include <koalabox/util.hpp>
#include <koalabox/patcher.hpp> //#include <steam_api_exports/steam_api_exports.hpp>
#if COMPILE_STORE_MODE
#include <store_mode/store.hpp>
#endif #endif
#define DETOUR_EX(FUNC, ADDRESS) hook::detour_or_warn(ADDRESS, #FUNC, reinterpret_cast<FunctionAddress>(FUNC)); // Hooking steam_api has shown itself to be less desirable than steamclient
#define DETOUR(FUNC) hook::detour_or_warn(original_library, #FUNC, reinterpret_cast<FunctionAddress>(FUNC)); // 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.
void init_proxy_mode() {
LOG_INFO("🔀 Detected proxy mode")
globals::steamapi_module = koalabox::loader::load_original_library(paths::get_self_path(), STEAMAPI_DLL);
}
void init_hook_mode() {
LOG_INFO("🪝 Detected hook mode")
koalabox::dll_monitor::init_listener(
STEAMCLIENT_DLL, [](const HMODULE& library) {
globals::steamclient_module = library;
DETOUR_STEAMCLIENT(CreateInterface)
koalabox::dll_monitor::shutdown_listener();
}
);
}
bool is_valve_steam(const String& exe_name) noexcept {
try {
if (exe_name < not_equals > "steam.exe") {
return false;
}
// Verify that it's steam from valve, and not some other executable coincidentally named steam
const HMODULE steam_handle = koalabox::win_util::get_module_handle_or_throw(nullptr);
const auto manifest = koalabox::win_util::get_module_manifest(steam_handle);
// Steam.exe manifest is expected to contain this string
return manifest < contains > "valvesoftware.steam.steam";
} catch (const Exception& e) {
LOG_ERROR("{} -> {}", __func__, e.what())
return false;
}
}
namespace smoke_api { namespace smoke_api {
Config config = {}; // NOLINT(cert-err58-cpp)
HMODULE original_library = nullptr; void init(HMODULE module_handle) {
bool is_hook_mode = false;
Path self_directory;
void init(HMODULE self_module) {
try { try {
DisableThreadLibraryCalls(self_module); DisableThreadLibraryCalls(module_handle);
koalabox::project_name = PROJECT_NAME; globals::smokeapi_handle = module_handle;
self_directory = loader::get_module_dir(self_module); config::init_config();
config = config_parser::parse<Config>(self_directory / PROJECT_NAME".json"); if (config::instance.logging) {
koalabox::logger::init_file_logger(paths::get_log_path());
const auto exe_path = Path(win_util::get_module_file_name_or_throw(nullptr));
const auto exe_name = exe_path.filename().string();
const auto exe_bitness = util::is_x64() ? 64 : 32;
if (config.logging) {
logger = file_logger::create(self_directory / fmt::format("{}.log", PROJECT_NAME));
} }
logger->info("🐨 {} v{}", PROJECT_NAME, PROJECT_VERSION); // 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__)
logger->debug(R"(Process name: "{}" [{}-bit])", exe_name, exe_bitness); koalabox::cache::init_cache(paths::get_cache_path());
is_hook_mode = hook::is_hook_mode(self_module, ORIGINAL_DLL); const auto exe_path = Path(koalabox::win_util::get_module_file_name_or_throw(nullptr));
const auto exe_name = exe_path.filename().string();
if (is_hook_mode) { LOG_DEBUG("Process name: '{}' [{}-bit]", exe_name, BITNESS)
hook::init(true);
if (util::strings_are_equal(exe_name, "steam.exe")) { if (koalabox::hook::is_hook_mode(globals::smokeapi_handle, STEAMAPI_DLL)) {
#ifndef _WIN64 koalabox::hook::init(true);
logger->info("🐨 Detected Koalageddon mode 💥");
dll_monitor::init({VSTDLIB_DLL, STEAMCLIENT_DLL}, [](const HMODULE& library, const String& name) { if (is_valve_steam(exe_name)) {
original_library = library; // TODO: Is this necessary? #if COMPILE_STORE_MODE
LOG_INFO("🛍️ Detected Store mode")
if (name == VSTDLIB_DLL) { store::init_store_mode();
// Family Sharing functions
DETOUR(Coroutine_Create)
} else if (name == STEAMCLIENT_DLL) {
// Unlocking functions
// TODO: Un-hardcode the pattern
const String pattern("55 8B EC 8B ?? ?? ?? ?? ?? 81 EC ?? ?? ?? ?? 53 FF 15");
auto Log_Interface_address = (FunctionAddress) patcher::find_pattern_address(
win_util::get_module_info(library), "Log_Interface", pattern
);
if (Log_Interface_address) {
DETOUR_EX(Log_Interface, Log_Interface_address)
}
}
});
#endif #endif
} else if (config.hook_steamclient) { // target steamclient(64).dll } else {
logger->info("🪝 Detected hook mode for SteamClient"); init_hook_mode();
dll_monitor::init(STEAMCLIENT_DLL, [](const HMODULE& library) {
original_library = library;
DETOUR(CreateInterface)
});
} else { // target steam_api.dll
logger->info("🪝 Detected hook mode for Steam_API");
dll_monitor::init(ORIGINAL_DLL, [](const HMODULE& library) {
original_library = library;
DETOUR(SteamInternal_FindOrCreateUserInterface)
DETOUR(SteamInternal_CreateInterface)
DETOUR(SteamApps)
DETOUR(SteamClient)
DETOUR(SteamUser)
DETOUR(SteamAPI_ISteamApps_BIsSubscribedApp)
DETOUR(SteamAPI_ISteamApps_BIsDlcInstalled)
DETOUR(SteamAPI_ISteamApps_GetDLCCount)
DETOUR(SteamAPI_ISteamApps_BGetDLCDataByIndex)
DETOUR(SteamAPI_ISteamClient_GetISteamGenericInterface)
DETOUR(SteamAPI_ISteamInventory_GetResultStatus)
DETOUR(SteamAPI_ISteamInventory_GetResultItems)
DETOUR(SteamAPI_ISteamInventory_GetResultItemProperty)
DETOUR(SteamAPI_ISteamInventory_CheckResultSteamID)
DETOUR(SteamAPI_ISteamInventory_GetAllItems)
DETOUR(SteamAPI_ISteamInventory_GetItemsByID)
DETOUR(SteamAPI_ISteamInventory_GetItemDefinitionIDs)
});
} }
} else { } else {
logger->info("🔀 Detected proxy mode for Steam_API"); init_proxy_mode();
original_library = loader::load_original_library(self_directory, ORIGINAL_DLL);
} }
logger->info("🚀 Initialization complete");
LOG_INFO("🚀 Initialization complete")
} catch (const Exception& ex) { } catch (const Exception& ex) {
util::panic(fmt::format("Initialization error: {}", ex.what())); koalabox::util::panic(fmt::format("Initialization error: {}", ex.what()));
} }
} }
void shutdown() { void shutdown() {
try { try {
if (is_hook_mode) { if (globals::steamapi_module != nullptr) {
dll_monitor::shutdown(); koalabox::win_util::free_library(globals::steamapi_module);
} else {
win_util::free_library(original_library);
} }
logger->info("💀 Shutdown complete"); LOG_INFO("💀 Shutdown complete")
} catch (const Exception& ex) { } catch (const Exception& ex) {
logger->error("Shutdown error: {}", ex.what()); LOG_ERROR("Shutdown error: {}", ex.what())
} }
} }
bool should_unlock(uint32_t app_id) {
return config.unlock_all != config.override.contains(app_id);
}
} }

View File

@@ -1,63 +1,9 @@
#pragma once #pragma once
#include <koalabox/koalabox.hpp>
#include <koalabox/hook.hpp> // For macros
#include <nlohmann/json.hpp>
#define GET_ORIGINAL_FUNCTION(FUNC) \
static const auto FUNC##_o = hook::get_original_function( \
smoke_api::is_hook_mode, \
smoke_api::original_library, \
#FUNC, \
FUNC \
);
#define GET_ORIGINAL_VIRTUAL_FUNCTION(FUNC) \
static const auto FUNC##_o = hook::get_original_function( \
true, \
smoke_api::original_library, \
#FUNC, \
FUNC \
);
namespace smoke_api { namespace smoke_api {
using namespace koalabox;
struct Config { void init(HMODULE module_handle);
uint32_t $version = 1;
bool logging = false;
bool hook_steamclient = true;
bool unlock_all = true;
Set<uint32_t> override;
Vector<uint32_t> dlc_ids;
bool auto_inject_inventory = true;
Vector<uint32_t> inventory_items;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(
Config, $version,
logging,
unlock_all,
hook_steamclient,
override,
dlc_ids,
auto_inject_inventory,
inventory_items
)
};
extern Config config;
extern HMODULE original_library;
extern bool is_hook_mode;
extern Path self_directory;
void init(HMODULE self_module);
void shutdown(); void shutdown();
bool should_unlock(uint32_t app_id);
} }

View File

@@ -1,182 +0,0 @@
#include <smoke_api/smoke_api.hpp>
#include <steam_impl/steam_impl.hpp>
using namespace smoke_api;
// ISteamApps
DLL_EXPORT(bool) SteamAPI_ISteamApps_BIsSubscribedApp(ISteamApps*, AppId_t appID) {
return steam_apps::IsDlcUnlocked(__func__, 0, appID);
}
DLL_EXPORT(bool) SteamAPI_ISteamApps_BIsDlcInstalled(ISteamApps*, AppId_t appID) {
return steam_apps::IsDlcUnlocked(__func__, 0, appID);
}
DLL_EXPORT(int) SteamAPI_ISteamApps_GetDLCCount(ISteamApps* self) {
return steam_apps::GetDLCCount(__func__, 0, [&]() {
GET_ORIGINAL_FUNCTION(SteamAPI_ISteamApps_GetDLCCount)
return SteamAPI_ISteamApps_GetDLCCount_o(self);
});
}
DLL_EXPORT(bool) SteamAPI_ISteamApps_BGetDLCDataByIndex(
ISteamApps* self,
int iDLC,
AppId_t* pAppID,
bool* pbAvailable,
char* pchName,
int cchNameBufferSize
) {
return steam_apps::GetDLCDataByIndex(__func__, 0, iDLC, pAppID, pbAvailable, pchName, cchNameBufferSize, [&]() {
GET_ORIGINAL_FUNCTION(SteamAPI_ISteamApps_BGetDLCDataByIndex)
return SteamAPI_ISteamApps_BGetDLCDataByIndex_o(
self, iDLC, pAppID, pbAvailable, pchName, cchNameBufferSize
);
});
}
// ISteamUser
DLL_EXPORT(EUserHasLicenseForAppResult) SteamAPI_ISteamUser_UserHasLicenseForApp(
ISteamUser* self,
CSteamID steamID,
AppId_t appID
) {
return steam_user::UserHasLicenseForApp(__func__, appID, [&]() {
GET_ORIGINAL_FUNCTION(SteamAPI_ISteamUser_UserHasLicenseForApp)
return SteamAPI_ISteamUser_UserHasLicenseForApp_o(self, steamID, appID);
});
}
// ISteamClient
DLL_EXPORT(void*) SteamAPI_ISteamClient_GetISteamGenericInterface(
ISteamClient* self,
HSteamUser hSteamUser,
HSteamPipe hSteamPipe,
const char* pchVersion
) {
return steam_client::GetGenericInterface(__func__, pchVersion, [&]() {
GET_ORIGINAL_FUNCTION(SteamAPI_ISteamClient_GetISteamGenericInterface)
return SteamAPI_ISteamClient_GetISteamGenericInterface_o(self, hSteamUser, hSteamPipe, pchVersion);
});
}
// ISteamInventory
DLL_EXPORT(EResult) SteamAPI_ISteamInventory_GetResultStatus(
ISteamInventory* self,
SteamInventoryResult_t resultHandle
) {
return steam_inventory::GetResultStatus(__func__, resultHandle, [&]() {
GET_ORIGINAL_FUNCTION(SteamAPI_ISteamInventory_GetResultStatus)
return SteamAPI_ISteamInventory_GetResultStatus_o(self, resultHandle);
});
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetResultItems(
ISteamInventory* self,
SteamInventoryResult_t resultHandle,
SteamItemDetails_t* pOutItemsArray,
uint32_t* punOutItemsArraySize
) {
return steam_inventory::GetResultItems(
__func__, resultHandle, pOutItemsArray, punOutItemsArraySize,
[&]() {
GET_ORIGINAL_FUNCTION(SteamAPI_ISteamInventory_GetResultItems)
return SteamAPI_ISteamInventory_GetResultItems_o(self, resultHandle, pOutItemsArray, punOutItemsArraySize);
},
[&](SteamItemDef_t* pItemDefIDs, uint32_t* punItemDefIDsArraySize) {
GET_ORIGINAL_FUNCTION(SteamAPI_ISteamInventory_GetItemDefinitionIDs)
return SteamAPI_ISteamInventory_GetItemDefinitionIDs_o(self, pItemDefIDs, punItemDefIDsArraySize);
}
);
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetResultItemProperty(
ISteamInventory* self,
SteamInventoryResult_t resultHandle,
uint32_t unItemIndex,
const char* pchPropertyName,
char* pchValueBuffer,
uint32_t* punValueBufferSizeOut
) {
return steam_inventory::GetResultItemProperty(
__func__, resultHandle, unItemIndex, pchPropertyName, pchValueBuffer, punValueBufferSizeOut, [&]() {
GET_ORIGINAL_FUNCTION(SteamAPI_ISteamInventory_GetResultItemProperty)
return SteamAPI_ISteamInventory_GetResultItemProperty_o(
self, resultHandle, unItemIndex, pchPropertyName, pchValueBuffer, punValueBufferSizeOut
);
}
);
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_CheckResultSteamID(
ISteamInventory* self,
SteamInventoryResult_t resultHandle,
CSteamID steamIDExpected
) {
return steam_inventory::CheckResultSteamID(__func__, resultHandle, steamIDExpected, [&]() {
GET_ORIGINAL_FUNCTION(SteamAPI_ISteamInventory_CheckResultSteamID)
return SteamAPI_ISteamInventory_CheckResultSteamID_o(self, resultHandle, steamIDExpected);
});
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetAllItems(
ISteamInventory* self,
SteamInventoryResult_t* pResultHandle
) {
return steam_inventory::GetAllItems(__func__, pResultHandle, [&]() {
GET_ORIGINAL_FUNCTION(SteamAPI_ISteamInventory_GetAllItems)
return SteamAPI_ISteamInventory_GetAllItems_o(self, pResultHandle);
});
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetItemsByID(
ISteamInventory* self,
SteamInventoryResult_t* pResultHandle,
const SteamItemInstanceID_t* pInstanceIDs,
uint32_t unCountInstanceIDs
) {
return steam_inventory::GetItemsByID(__func__, pResultHandle, pInstanceIDs, unCountInstanceIDs, [&]() {
GET_ORIGINAL_FUNCTION(SteamAPI_ISteamInventory_GetItemsByID)
return SteamAPI_ISteamInventory_GetItemsByID_o(self, pResultHandle, pInstanceIDs, unCountInstanceIDs);
});
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_SerializeResult(
ISteamInventory* self,
SteamInventoryResult_t resultHandle,
void* pOutBuffer,
uint32_t* punOutBufferSize
) {
return steam_inventory::SerializeResult(__func__, resultHandle, pOutBuffer, punOutBufferSize, [&]() {
GET_ORIGINAL_FUNCTION(SteamAPI_ISteamInventory_SerializeResult)
return SteamAPI_ISteamInventory_SerializeResult_o(self, resultHandle, pOutBuffer, punOutBufferSize);
});
}
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetItemDefinitionIDs(
ISteamInventory* self,
SteamItemDef_t* pItemDefIDs,
uint32_t* punItemDefIDsArraySize
) {
return steam_inventory::GetItemDefinitionIDs(__func__, pItemDefIDs, punItemDefIDsArraySize, [&]() {
GET_ORIGINAL_FUNCTION(SteamAPI_ISteamInventory_GetItemDefinitionIDs)
return SteamAPI_ISteamInventory_GetItemDefinitionIDs_o(self, pItemDefIDs, punItemDefIDsArraySize);
});
}

View File

@@ -1,21 +0,0 @@
#include <smoke_api/smoke_api.hpp>
#include <steam_impl/steam_impl.hpp>
#include <steam_functions/steam_functions.hpp>
using namespace smoke_api;
DLL_EXPORT(void*) SteamInternal_FindOrCreateUserInterface(HSteamUser hSteamUser, const char* version) {
return steam_client::GetGenericInterface(__func__, version, [&]() {
GET_ORIGINAL_FUNCTION(SteamInternal_FindOrCreateUserInterface)
return SteamInternal_FindOrCreateUserInterface_o(hSteamUser, version);
});
}
DLL_EXPORT(void*) SteamInternal_CreateInterface(const char* version) {
return steam_client::GetGenericInterface(__func__, version, [&]() {
GET_ORIGINAL_FUNCTION(SteamInternal_CreateInterface)
return SteamInternal_CreateInterface_o(version);
});
}

View File

@@ -1,38 +0,0 @@
#include <smoke_api/smoke_api.hpp>
#include <steam_impl/steam_impl.hpp>
using namespace smoke_api;
VIRTUAL(bool) ISteamApps_BIsSubscribedApp(PARAMS(AppId_t appID)) { // NOLINT(misc-unused-parameters)
return steam_apps::IsDlcUnlocked(__func__, 0, appID);
}
VIRTUAL(bool) ISteamApps_BIsDlcInstalled(PARAMS(AppId_t appID)) { // NOLINT(misc-unused-parameters)
return steam_apps::IsDlcUnlocked(__func__, 0, appID);
}
VIRTUAL(int) ISteamApps_GetDLCCount(PARAMS()) {
return steam_apps::GetDLCCount(__func__, 0, [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(ISteamApps_GetDLCCount)
return ISteamApps_GetDLCCount_o(ARGS());
});
}
VIRTUAL(bool) ISteamApps_BGetDLCDataByIndex(
PARAMS(
int iDLC,
AppId_t* pAppID,
bool* pbAvailable,
char* pchName,
int cchNameBufferSize
)
) {
return steam_apps::GetDLCDataByIndex(__func__, 0, iDLC, pAppID, pbAvailable, pchName, cchNameBufferSize, [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(ISteamApps_BGetDLCDataByIndex)
return ISteamApps_BGetDLCDataByIndex_o(
ARGS(iDLC, pAppID, pbAvailable, pchName, cchNameBufferSize)
);
});
}

View File

@@ -1,61 +0,0 @@
#include <smoke_api/smoke_api.hpp>
#include <steam_impl/steam_impl.hpp>
#include <steam_functions/steam_functions.hpp>
using namespace smoke_api;
VIRTUAL(void*) ISteamClient_GetISteamApps(
PARAMS(
HSteamUser hSteamUser,
HSteamPipe hSteamPipe,
const char* version
)
) {
return steam_client::GetGenericInterface(__func__, version, [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(ISteamClient_GetISteamApps)
return ISteamClient_GetISteamApps_o(ARGS(hSteamUser, hSteamPipe, version));
});
}
VIRTUAL(void*) ISteamClient_GetISteamUser(
PARAMS(
HSteamUser hSteamUser,
HSteamPipe hSteamPipe,
const char* version
)
) {
return steam_client::GetGenericInterface(__func__, version, [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(ISteamClient_GetISteamUser)
return ISteamClient_GetISteamUser_o(ARGS(hSteamUser, hSteamPipe, version));
});
}
VIRTUAL(void*) ISteamClient_GetISteamGenericInterface(
PARAMS(
HSteamUser hSteamUser,
HSteamPipe hSteamPipe,
const char* pchVersion
)
) {
return steam_client::GetGenericInterface(__func__, pchVersion, [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(ISteamClient_GetISteamGenericInterface)
return ISteamClient_GetISteamGenericInterface_o(ARGS(hSteamUser, hSteamPipe, pchVersion));
});
}
VIRTUAL(void*) ISteamClient_GetISteamInventory(
PARAMS(
HSteamUser hSteamUser,
HSteamPipe hSteamPipe,
const char* pchVersion
)
) {
return steam_client::GetGenericInterface(__func__, pchVersion, [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(ISteamClient_GetISteamInventory)
return ISteamClient_GetISteamInventory_o(ARGS(hSteamUser, hSteamPipe, pchVersion));
});
}

View File

@@ -1,12 +0,0 @@
#include <smoke_api/smoke_api.hpp>
#include <steam_impl/steam_impl.hpp>
using namespace smoke_api;
VIRTUAL(EUserHasLicenseForAppResult) ISteamUser_UserHasLicenseForApp(PARAMS(CSteamID steamID, AppId_t appID)) {
return steam_user::UserHasLicenseForApp(__func__, appID, [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(ISteamUser_UserHasLicenseForApp)
return ISteamUser_UserHasLicenseForApp_o(ARGS(steamID, appID));
});
}

View File

@@ -1,157 +0,0 @@
#pragma once
#include <koalabox/koalabox.hpp>
#include <steam_types/steam_types.hpp>
/**
* By default, virtual functions are declared with __thiscall
* convention, which is normal since they are class members.
* But it presents an issue for us, since we cannot pass *this
* pointer as a function argument. This is because *this
* pointer is passed via register ECX in __thiscall
* convention. Hence, to resolve this issue we declare our
* hooked functions with __fastcall convention, to trick
* the compiler into reading ECX & EDX registers as 1st
* and 2nd function arguments respectively. Similarly, __fastcall
* makes the compiler push the first argument into the ECX register,
* which mimics the __thiscall calling convention. Register EDX
* is not used anywhere in this case, but we still pass it along
* to conform to the __fastcall convention. This all applies
* to the x86 architecture.
*
* In x86-64 however, there is only one calling convention,
* so __fastcall is simply ignored. However, RDX in this case
* will store the 1st actual argument to the function, so we
* have to omit it from the function signature.
*
* The macros below implement the above-mentioned considerations.
*/
#ifdef _WIN64
#define PARAMS(...) void* RCX, ##__VA_ARGS__
#define ARGS(...) RCX, ##__VA_ARGS__
#define THIS RCX
#else
#define PARAMS(...) void* ECX, void* EDX, ##__VA_ARGS__
#define ARGS(...) ECX, EDX, ##__VA_ARGS__
#define THIS ECX
#endif
#define DLL_EXPORT(TYPE) extern "C" __declspec( dllexport ) TYPE __cdecl
#define VIRTUAL(TYPE) __declspec(noinline) TYPE __fastcall
class ISteamClient;
class ISteamApps;
class ISteamUser;
class ISteamInventory;
typedef __int32 HSteamPipe;
typedef __int32 HSteamUser;
typedef uint32_t AppId_t;
typedef uint64_t CSteamID;
typedef uint32_t HCoroutine;
// ISteamClient
VIRTUAL(void*) ISteamClient_GetISteamApps(PARAMS(HSteamUser, HSteamPipe, const char*));
VIRTUAL(void*) ISteamClient_GetISteamUser(PARAMS(HSteamUser, HSteamPipe, const char*));
VIRTUAL(void*) ISteamClient_GetISteamGenericInterface(PARAMS(HSteamUser, HSteamPipe, const char*));
VIRTUAL(void*) ISteamClient_GetISteamInventory(PARAMS(HSteamUser, HSteamPipe, const char*));
// ISteamApps
VIRTUAL(bool) ISteamApps_BIsSubscribedApp(PARAMS(AppId_t));
VIRTUAL(bool) ISteamApps_BIsDlcInstalled(PARAMS(AppId_t));
VIRTUAL(int) ISteamApps_GetDLCCount(PARAMS());
VIRTUAL(bool) ISteamApps_BGetDLCDataByIndex(PARAMS(int, AppId_t*, bool*, char*, int));
// ISteamUser
VIRTUAL(EUserHasLicenseForAppResult) ISteamUser_UserHasLicenseForApp(PARAMS(CSteamID, AppId_t));
// ISteamInventory
VIRTUAL(EResult) ISteamInventory_GetResultStatus(PARAMS(uint32_t));
VIRTUAL(bool) ISteamInventory_GetResultItems(PARAMS(SteamInventoryResult_t, SteamItemDetails_t*, uint32_t*));
VIRTUAL(bool) ISteamInventory_GetResultItemProperty(
PARAMS(SteamInventoryResult_t, uint32_t, const char*, char*, uint32_t*)
);
VIRTUAL(bool) ISteamInventory_GetAllItems(PARAMS(SteamInventoryResult_t*));
VIRTUAL(bool) ISteamInventory_GetItemsByID(PARAMS(SteamInventoryResult_t*, const SteamItemInstanceID_t*, uint32_t));
VIRTUAL(bool) ISteamInventory_SerializeResult(PARAMS(SteamInventoryResult_t, void*, uint32_t*));
VIRTUAL(bool) ISteamInventory_GetItemDefinitionIDs(PARAMS(SteamItemDef_t*, uint32_t*));
VIRTUAL(bool) ISteamInventory_CheckResultSteamID(PARAMS(SteamInventoryResult_t, CSteamID));
// API
DLL_EXPORT(void*) CreateInterface(const char*, int*);
DLL_EXPORT(void*) SteamInternal_FindOrCreateUserInterface(HSteamUser, const char*);
DLL_EXPORT(void*) SteamInternal_CreateInterface(const char*);
DLL_EXPORT(void*) SteamApps();
DLL_EXPORT(void*) SteamClient();
DLL_EXPORT(void*) SteamUser();
// Flat interfaces
DLL_EXPORT(bool) SteamAPI_ISteamApps_BIsSubscribedApp(ISteamApps*, AppId_t);
DLL_EXPORT(bool) SteamAPI_ISteamApps_BIsDlcInstalled(ISteamApps*, AppId_t);
DLL_EXPORT(int) SteamAPI_ISteamApps_GetDLCCount(ISteamApps*);
DLL_EXPORT(bool) SteamAPI_ISteamApps_BGetDLCDataByIndex(ISteamApps*, int, AppId_t*, bool*, char*, int);
DLL_EXPORT(EUserHasLicenseForAppResult) SteamAPI_ISteamUser_UserHasLicenseForApp(ISteamUser*, CSteamID, AppId_t);
DLL_EXPORT(void*) SteamAPI_ISteamClient_GetISteamGenericInterface(ISteamClient*, HSteamUser, HSteamPipe, const char*);
DLL_EXPORT(EResult) SteamAPI_ISteamInventory_GetResultStatus(ISteamInventory*, SteamInventoryResult_t);
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetResultItems(
ISteamInventory*, SteamInventoryResult_t, SteamItemDetails_t*, uint32_t*
);
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetResultItemProperty(
ISteamInventory*, SteamInventoryResult_t, uint32_t, const char*, char*, uint32_t*
);
DLL_EXPORT(bool) SteamAPI_ISteamInventory_CheckResultSteamID(ISteamInventory*, SteamInventoryResult_t, CSteamID);
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetAllItems(ISteamInventory*, SteamInventoryResult_t*);
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetItemsByID(
ISteamInventory*, SteamInventoryResult_t*, const SteamItemInstanceID_t*, uint32_t
);
DLL_EXPORT(bool) SteamAPI_ISteamInventory_SerializeResult(ISteamInventory*, SteamInventoryResult_t, void*, uint32_t*);
DLL_EXPORT(bool) SteamAPI_ISteamInventory_GetItemDefinitionIDs(ISteamInventory*, SteamItemDef_t*, uint32_t*);
// Koalageddon mode
DLL_EXPORT(HCoroutine) Coroutine_Create(void* callback_address, struct CoroutineData* data);
DLL_EXPORT(void) Log_Interface(const char* interface_name, const char* function_name);
// IClientApps
VIRTUAL(int) IClientApps_GetDLCCount(PARAMS(AppId_t));
VIRTUAL(bool) IClientApps_BGetDLCDataByIndex(PARAMS(AppId_t, int, AppId_t*, bool*, char*, int));
// IClientAppManager
VIRTUAL(bool) IClientAppManager_IsAppDlcInstalled(PARAMS(AppId_t, AppId_t));
// IClientUser
VIRTUAL(bool) IClientUser_IsSubscribedApp(PARAMS(AppId_t));
// IClientInventory
VIRTUAL(EResult) IClientInventory_GetResultStatus(PARAMS(SteamInventoryResult_t));
VIRTUAL(bool) IClientInventory_GetResultItems(
PARAMS(SteamInventoryResult_t, SteamItemDetails_t*, uint32_t, uint32_t *)
);
//////
VIRTUAL(bool) IClientInventory_GetResultItemProperty(
PARAMS(SteamInventoryResult_t, uint32_t, const char*, char*, uint32_t, uint32_t*)
);
VIRTUAL(bool) IClientInventory_CheckResultSteamID(PARAMS(SteamInventoryResult_t, CSteamID));
VIRTUAL(bool) IClientInventory_GetAllItems(PARAMS(SteamInventoryResult_t*));
VIRTUAL(bool) IClientInventory_GetItemsByID(PARAMS(SteamInventoryResult_t*, const SteamItemInstanceID_t*, uint32_t));
VIRTUAL(bool) IClientInventory_SerializeResult(PARAMS(SteamInventoryResult_t, void*, uint32_t, uint32_t *));
VIRTUAL(bool) IClientInventory_GetItemDefinitionIDs(PARAMS(SteamItemDef_t*, uint32_t, uint32_t *));
namespace steam_functions {
using namespace koalabox;
const String STEAM_APPS = "STEAMAPPS_INTERFACE_VERSION"; // NOLINT(cert-err58-cpp)
const String STEAM_CLIENT = "SteamClient"; // NOLINT(cert-err58-cpp)
const String STEAM_USER = "SteamUser"; // NOLINT(cert-err58-cpp)
const String STEAM_INVENTORY = "STEAMINVENTORY_INTERFACE_V"; // NOLINT(cert-err58-cpp)
void hook_virtuals(void* interface, const String& version_string);
uint32_t get_app_id_or_throw();
}

View File

@@ -1,190 +1,131 @@
#include <smoke_api/smoke_api.hpp> #include <steam_impl/steam_apps.hpp>
#include <steam_impl/steam_impl.hpp> #include <steam_impl/steam_impl.hpp>
#include <common/app_cache.hpp>
#include <koalabox/io.hpp> #include <smoke_api/config.hpp>
#include <koalabox/logger.hpp>
#include <cpr/cpr.h> #include <koalabox/util.hpp>
#include <core/types.hpp>
using namespace smoke_api; #include <core/api.hpp>
constexpr auto max_dlc = 64;
Vector<AppId_t> cached_dlcs;
int original_dlc_count = 0;
Path get_cache_path() {
static const auto path = self_directory / "SmokeAPI.cache.json";
return path;
}
void read_from_cache(const String& app_id_str) {
try {
const auto text = io::read_file(get_cache_path());
if (text.empty()) {
return;
}
auto json = nlohmann::json::parse(text);
cached_dlcs = json[app_id_str]["dlc"].get<decltype(cached_dlcs)>();
logger->debug("Read {} DLCs from cache", cached_dlcs.size());
} catch (const Exception& ex) {
logger->error("Error reading DLCs from cache: {}", ex.what());
}
}
void save_to_cache(const String& app_id_str) {
try {
logger->debug("Saving {} DLCs to cache", cached_dlcs.size());
const nlohmann::json json = {
{app_id_str, {
{"dlc", cached_dlcs}
}}
};
io::write_file(get_cache_path(), json.dump(2));
} catch (const Exception& ex) {
logger->error("Error saving DLCs to cache: {}", ex.what());
}
}
void fetch_and_cache_dlcs(AppId_t app_id) {
if (not app_id) {
try {
app_id = steam_functions::get_app_id_or_throw();
logger->info("Detected App ID: {}", app_id);
} catch (const Exception& ex) {
logger->error("Failed to get app ID: {}", ex.what());
return;
}
}
const auto app_id_str = std::to_string(app_id);
const auto fetch_from_steam = [&]() {
const auto url = fmt::format("https://store.steampowered.com/dlc/{}/ajaxgetdlclist", app_id_str);
const auto res = cpr::Get(cpr::Url{url});
if (res.status_code != cpr::status::HTTP_OK) {
throw util::exception(
"Steam Web API didn't responded with HTTP_OK result. Code: {}, Error: {},\n"
"Headers:\n{}\nBody:\n{}",
res.status_code, (int) res.error.code, res.raw_header, res.text
);
}
const auto json = nlohmann::json::parse(res.text);
if (json["success"] != 1) {
throw util::exception("Web API responded with 'success': 1.");
}
Vector<AppId_t> dlcs;
for (const auto& dlc: json["dlcs"]) {
const auto app_id = dlc["appid"].get<String>();
dlcs.emplace_back(std::stoi(app_id));
}
return dlcs;
};
const auto fetch_from_github = [&]() {
const String url = "https://raw.githubusercontent.com/acidicoala/public-entitlements/main/steam/v1/dlc.json";
const auto res = cpr::Get(cpr::Url{url});
if (res.status_code != cpr::status::HTTP_OK) {
throw util::exception(
"Github Web API didn't responded with HTTP_OK result. Code: {}, Error: {},\n"
"Headers:\n{}\nBody:\n{}",
res.status_code, (int) res.error.code, res.raw_header, res.text
);
}
const auto json = nlohmann::json::parse(res.text);
if (json.contains(app_id_str)) {
return json[app_id_str].get<decltype(cached_dlcs)>();
}
return Vector<AppId_t>{};
};
try {
read_from_cache(app_id_str);
auto list1 = fetch_from_steam();
auto list2 = fetch_from_github();
list1.insert(list1.end(), list2.begin(), list2.end());
Set<AppId_t> fetched_dlcs(list1.begin(), list1.end());
if (fetched_dlcs.size() > cached_dlcs.size()) {
cached_dlcs = Vector<AppId_t>(fetched_dlcs.begin(), fetched_dlcs.end());
}
save_to_cache(app_id_str);
} catch (const Exception& ex) {
logger->error("Failed to fetch DLC: {}", ex.what());
}
}
String get_app_id_log(const AppId_t app_id) {
return app_id ? fmt::format("App ID: {}, ", app_id) : "";
}
namespace steam_apps { namespace steam_apps {
/// Steamworks may max GetDLCCount value at 64, depending on how much unowned DLCs the user has.
/// Despite this limit, some games with more than 64 DLCs still keep using this method.
/// This means we have to get extra DLC IDs from local config, remote config, or cache.
constexpr auto MAX_DLC = 64;
bool IsDlcUnlocked(const String& function_name, AppId_t app_id, AppId_t dlc_id) { Map<AppId_t, Vector<DLC>> app_dlcs; // NOLINT(cert-err58-cpp)
const auto app_id_unlocked = not app_id or should_unlock(app_id); // true if app_id == 0 Set<AppId_t> fully_fetched; // NOLINT(cert-err58-cpp)
const auto dlc_id_unlocked = should_unlock(dlc_id);
const auto installed = app_id_unlocked and dlc_id_unlocked; String get_app_id_log(const AppId_t app_id) {
return app_id ? fmt::format("App ID: {:>8}, ", app_id) : "";
logger->info("{} -> {}DLC ID: {}, Unlocked: {}", function_name, get_app_id_log(app_id), dlc_id, installed);
return installed;
} }
int GetDLCCount(const String& function_name, const AppId_t app_id, const std::function<int()>& original_function) { /**
static std::mutex section; * @param app_id
std::lock_guard<std::mutex> guard(section); * @return boolean indicating if the function was able to successfully fetch DLC IDs from all sources.
*/
void fetch_and_cache_dlcs(AppId_t app_id) {
static std::mutex mutex;
const std::lock_guard<std::mutex> guard(mutex);
if (app_id) { if (not app_id) {
logger->debug("{} -> App ID: {}", function_name, app_id); // No app id means we are operating in game mode.
// Hence, we need to use utility functions to get app id.
try {
app_id = steam_impl::get_app_id_or_throw();
LOG_INFO("Detected App ID: {}", app_id)
} catch (const Exception& ex) {
LOG_ERROR("Failed to get app ID: {}", ex.what())
app_dlcs[app_id] = {}; // Dummy value to avoid checking for presence on each access
return;
}
} }
// Compute count only once // FIXME: This doesn't work in Koalageddon mode // We want to fetch data only once. However, if any of the remote sources have failed
static int total_count = [&]() { // previously, we want to attempt fetching again.
original_dlc_count = original_function(); if (fully_fetched.contains(app_id)) {
logger->debug("{} -> Original DLC count: {}", function_name, original_dlc_count); return;
}
const auto injected_count = static_cast<int>(config.dlc_ids.size()); // Any of the sources might fail, so we try to get optimal result
logger->debug("{} -> Injected DLC count: {}", function_name, injected_count); // by aggregating results from all the sources into a single set.
Vector<DLC> aggregated_dlcs;
if (original_dlc_count < max_dlc) { const auto append_dlcs = [&](const Vector<DLC>& source, const String& source_name) {
// Steamworks may max out this value at 64, depending on how much unowned DLCs the user has. LOG_DEBUG("App ID {} has {} DLCs defined in {}", app_id, source.size(), source_name)
// Despite this limit, some games with more than 64 DLCs still keep using this method. aggregated_dlcs < append > source;
// This means we have to fetch full list of IDs from web api. };
return original_dlc_count + injected_count; append_dlcs(smoke_api::config::get_extra_dlcs(app_id), "local config");
const auto github_dlcs_opt = api::fetch_dlcs_from_github(app_id);
if (github_dlcs_opt) {
append_dlcs(*github_dlcs_opt, "GitHub repository");
}
const auto steam_dlcs_opt = api::fetch_dlcs_from_steam(app_id);
if (steam_dlcs_opt) {
append_dlcs(*steam_dlcs_opt, "Steam API");
}
if (github_dlcs_opt && steam_dlcs_opt) {
fully_fetched.insert(app_id);
} else {
append_dlcs(smoke_api::app_cache::get_dlcs(app_id), "disk cache");
}
// Cache DLCs in memory and cache for future use
app_dlcs[app_id] = aggregated_dlcs;
smoke_api::app_cache::save_dlcs(app_id, aggregated_dlcs);
}
bool IsDlcUnlocked(
const String& function_name,
AppId_t app_id,
AppId_t dlc_id,
const Function<bool()>& original_function
) {
try {
const auto unlocked = smoke_api::config::is_dlc_unlocked(app_id, dlc_id, original_function);
LOG_INFO("{} -> {}DLC ID: {:>8}, Unlocked: {}", function_name, get_app_id_log(app_id), dlc_id, unlocked)
return unlocked;
} catch (const Exception& e) {
LOG_ERROR("Uncaught exception: {}", e.what())
return false;
}
}
int GetDLCCount(const String& function_name, const AppId_t app_id, const Function<int()>& original_function) {
try {
const auto total_count = [&](int count) {
LOG_INFO("{} -> Responding with DLC count: {}", function_name, count)
return count;
};
if (app_id != 0) {
LOG_DEBUG("{} -> App ID: {}", function_name, app_id)
} }
logger->debug("Game has {} or more DLCs. Fetching DLCs from a web API.", max_dlc); const auto original_count = original_function();
LOG_DEBUG("{} -> Original DLC count: {}", function_name, original_count)
if (original_count < MAX_DLC) {
return total_count(original_count);
}
LOG_DEBUG("Game has {} or more DLCs. Fetching DLCs from remote sources.", original_count)
fetch_and_cache_dlcs(app_id); fetch_and_cache_dlcs(app_id);
const auto fetched_count = static_cast<int>(cached_dlcs.size()); return total_count(static_cast<int>(app_dlcs[app_id].size()));
logger->debug("{} -> Fetched/cached DLC count: {}", function_name, fetched_count); } catch (const Exception& e) {
LOG_ERROR(" Uncaught exception: {}", function_name, e.what())
return fetched_count + injected_count; return 0;
}(); }
logger->info("{} -> Responding with DLC count: {}", function_name, total_count);
return total_count;
} }
bool GetDLCDataByIndex( bool GetDLCDataByIndex(
@@ -195,78 +136,62 @@ namespace steam_apps {
bool* pbAvailable, bool* pbAvailable,
char* pchName, char* pchName,
int cchNameBufferSize, int cchNameBufferSize,
const std::function<bool()>& original_function const Function<bool()>& original_function,
const Function<bool(AppId_t)>& is_originally_unlocked
) { ) {
const auto print_dlc_info = [&](const String& tag) { try {
logger->info( LOG_DEBUG("{} -> {}index: {:>3}", function_name, get_app_id_log(app_id), iDLC)
"{} -> [{}] {}index: {}, DLC ID: {}, available: {}, name: '{}'",
function_name, tag, get_app_id_log(app_id), iDLC, *pDlcId, *pbAvailable, pchName
);
};
const auto fill_dlc_info = [&](const AppId_t id) { const auto print_dlc_info = [&](const String& tag) {
*pDlcId = id; LOG_INFO(
*pbAvailable = should_unlock(id); R"({} -> [{:^12}] {}index: {:>3}, DLC ID: {:>8}, available: {:5}, name: "{}")",
function_name, tag, get_app_id_log(app_id), iDLC, *pDlcId, *pbAvailable, pchName
)
};
auto name = fmt::format("DLC #{} with ID: {} ", iDLC, id); const auto inject_dlc = [&](const DLC& dlc) {
name = name.substr(0, cchNameBufferSize); // Fill the output pointers
*name.rbegin() = '\0'; *pDlcId = dlc.get_id();
memcpy_s(pchName, cchNameBufferSize, name.c_str(), name.size()); *pbAvailable = smoke_api::config::is_dlc_unlocked(
}; app_id, *pDlcId, [&]() {
return is_originally_unlocked(*pDlcId);
}
);
const auto inject_dlc = [&](const int index) { auto name = dlc.get_name();
if (index >= config.dlc_ids.size()) { name = name.substr(0, cchNameBufferSize + 1);
logger->error("{} -> Out of bounds injected index: {}", function_name, index); memcpy_s(pchName, cchNameBufferSize, name.c_str(), name.size());
};
if (app_dlcs.contains(app_id)) {
const auto& dlcs = app_dlcs[app_id];
if (iDLC >= 0 && iDLC < dlcs.size()) {
inject_dlc(dlcs[iDLC]);
print_dlc_info("injected");
return true;
}
LOG_WARN("{} -> Out of bounds DLC index: {}", function_name, iDLC)
return false; return false;
} }
const auto dlc_id = config.dlc_ids[index]; const auto success = original_function();
fill_dlc_info(dlc_id);
print_dlc_info("injected");
return true;
};
// Original response if (success) {
if (cached_dlcs.empty()) { *pbAvailable = smoke_api::config::is_dlc_unlocked(
// Original DLC index app_id, *pDlcId, [&]() { return *pbAvailable; }
if (iDLC < original_dlc_count) { );
const auto success = original_function(); print_dlc_info("original");
} else {
if (success) { LOG_WARN("{} -> original call failed for index: {}", function_name, iDLC)
*pbAvailable = should_unlock(*pDlcId);
print_dlc_info("original");
} else {
logger->warn("{} -> original function failed for index: {}", function_name, iDLC);
}
return success;
} }
// Injected DLC index (after original) return success;
const auto index = iDLC - original_dlc_count; } catch (const Exception& e) {
return inject_dlc(index); LOG_ERROR("{} -> Uncaught exception: {}", function_name, e.what())
}
// Cached response
const auto total_size = cached_dlcs.size() + config.dlc_ids.size();
if (iDLC < 0 or iDLC >= total_size) {
logger->error(
"{} -> Game accessed out of bounds DLC index: {}. Total size: {}",
function_name, iDLC, total_size
);
return false; return false;
} }
// Cached index
if (iDLC < cached_dlcs.size()) {
const auto dlc_id = cached_dlcs[iDLC];
fill_dlc_info(dlc_id);
print_dlc_info("cached");
return true;
}
// Injected DLC index (after cached)
const auto index = iDLC - static_cast<int>(cached_dlcs.size());
return inject_dlc(index);
} }
} }

View File

@@ -0,0 +1,32 @@
#pragma once
#include <core/types.hpp>
namespace steam_apps {
bool IsDlcUnlocked(
const String& function_name,
AppId_t app_id,
AppId_t dlc_id,
const Function<bool()>& original_function
);
int GetDLCCount(
const String& function_name,
AppId_t app_id,
const Function<int()>& original_function
);
bool GetDLCDataByIndex(
const String& dlc_id,
AppId_t dlc_ids,
int iDLC,
AppId_t* pDlcId,
bool* pbAvailable,
char* pchName,
int cchNameBufferSize,
const Function<bool()>& original_function,
const Function<bool(AppId_t)>& is_originally_unlocked // Aux function to resolve original dlc status
);
}

View File

@@ -1,19 +1,19 @@
#include <steam_impl/steam_client.hpp>
#include <steam_impl/steam_impl.hpp> #include <steam_impl/steam_impl.hpp>
#include <koalabox/logger.hpp>
namespace steam_client{ namespace steam_client {
void* GetGenericInterface( void* GetGenericInterface(
const String& function_name, const String& function_name,
const String& interface_version, const String& interface_version,
const std::function<void*()>& original_function const Function<void*()>& original_function
) { ) {
logger->debug("{} -> Version: '{}'", function_name, interface_version);
auto* const interface = original_function(); auto* const interface = original_function();
logger->debug("{} -> Result: {}", function_name, fmt::ptr(interface)); LOG_DEBUG("{} -> '{}' @ {}", function_name, interface_version, interface)
steam_functions::hook_virtuals(interface, interface_version); steam_impl::hook_virtuals(interface, interface_version);
return interface; return interface;
} }

View File

@@ -0,0 +1,13 @@
#pragma once
#include <core/types.hpp>
namespace steam_client {
void* GetGenericInterface(
const String& function_name,
const String& interface_version,
const Function<void*()>& original_function
);
}

View File

@@ -1,12 +1,18 @@
#include <steam_functions/steam_functions.hpp> #include <steam_impl/steam_impl.hpp>
#include <game_mode/virtuals/steam_api_virtuals.hpp>
#include <common/steamclient_exports.hpp>
#include <core/globals.hpp>
#include <build_config.h> #include <build_config.h>
#include <koalabox/util.hpp>
#include <koalabox/hook.hpp>
#include <koalabox/win_util.hpp> #include <koalabox/win_util.hpp>
#include <koalabox/logger.hpp>
#include <polyhook2/Misc.hpp> #include <polyhook2/Misc.hpp>
namespace steam_functions { #if COMPILE_STORE_MODE
#include <store_mode/steamclient/steamclient.hpp>
#endif
namespace steam_impl {
typedef Map<String, Map<int, int>> FunctionOrdinalMap; typedef Map<String, Map<int, int>> FunctionOrdinalMap;
@@ -15,7 +21,7 @@ namespace steam_functions {
{ {
{6, 16}, {6, 16},
{7, 18}, {7, 18},
{8, 15}, {8, 15},
{9, 16}, {9, 16},
{12, 15}, {12, 15},
} }
@@ -83,25 +89,25 @@ namespace steam_functions {
int min_version, int min_version,
int max_version int max_version
) { ) {
logger->debug("Hooking interface '{}'", version_string); LOG_DEBUG("Hooking interface '{}'", version_string)
try { try {
const auto version_number = stoi(version_string.substr(prefix.length())); const auto version_number = stoi(version_string.substr(prefix.length()));
if (version_number < min_version) { if (version_number < min_version) {
logger->warn("Legacy version of {}: {}", version_string, version_number); LOG_WARN("Legacy version of {}: {}", version_string, version_number)
} }
if (version_number > max_version) { if (version_number > max_version) {
logger->warn( LOG_WARN(
"Unsupported new version of {}: {}. Fallback version {} will be used", "Unsupported new version of {}: {}. Fallback version {} will be used",
version_string, version_number, max_version version_string, version_number, max_version
); )
} }
return version_number; return version_number;
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
util::panic("Failed to extract version number from: '{}'", version_string); koalabox::util::panic("Failed to extract version number from: '{}'", version_string);
} }
} }
@@ -116,21 +122,22 @@ namespace steam_functions {
} }
} }
util::panic("Invalid interface version ({}) for function {}", interface_version, function_name); koalabox::util::panic("Invalid interface version ({}) for function {}", interface_version, function_name);
} }
#define HOOK(MAP, FUNC) \ #define HOOK_VIRTUALS(MAP, FUNC) \
hook::swap_virtual_func( \ koalabox::hook::swap_virtual_func( \
interface, \ globals::address_map, \
interface, \
#FUNC, \ #FUNC, \
get_ordinal(MAP, #FUNC, version_number), \ get_ordinal(MAP, #FUNC, version_number), \
(FunctionAddress) (FUNC) \ reinterpret_cast<uintptr_t>(FUNC) \
); );
#define HOOK_STEAM_CLIENT(FUNC) HOOK(steam_client_ordinal_map, FUNC) #define HOOK_STEAM_CLIENT(FUNC) HOOK_VIRTUALS(steam_client_ordinal_map, FUNC)
#define HOOK_STEAM_APPS(FUNC) HOOK(steam_apps_ordinal_map, FUNC) #define HOOK_STEAM_APPS(FUNC) HOOK_VIRTUALS(steam_apps_ordinal_map, FUNC)
#define HOOK_STEAM_USER(FUNC) HOOK(steam_user_ordinal_map, FUNC) #define HOOK_STEAM_USER(FUNC) HOOK_VIRTUALS(steam_user_ordinal_map, FUNC)
#define HOOK_STEAM_INVENTORY(FUNC) HOOK(steam_inventory_ordinal_map, FUNC) #define HOOK_STEAM_INVENTORY(FUNC) HOOK_VIRTUALS(steam_inventory_ordinal_map, FUNC)
void hook_virtuals(void* interface, const String& version_string) { void hook_virtuals(void* interface, const String& version_string) {
if (interface == nullptr) { if (interface == nullptr) {
@@ -138,17 +145,15 @@ namespace steam_functions {
return; return;
} }
static Set<std::pair<void*, String>> hooked_interfaces; static Set<void*> hooked_interfaces;
const auto interface_pair = std::pair{interface, version_string}; if (hooked_interfaces.contains(interface)) {
if (hooked_interfaces.contains(interface_pair)) {
// This interface is already hooked. Skipping it. // This interface is already hooked. Skipping it.
return; return;
} }
static std::mutex section; static Mutex section;
std::lock_guard<std::mutex> guard(section); const MutexLockGuard guard(section);
if (version_string.starts_with(STEAM_CLIENT)) { if (version_string.starts_with(STEAM_CLIENT)) {
const auto version_number = extract_version_number(version_string, STEAM_CLIENT, 6, 20); const auto version_number = extract_version_number(version_string, STEAM_CLIENT, 6, 20);
@@ -194,22 +199,26 @@ namespace steam_functions {
if (version_number >= 2) { if (version_number >= 2) {
HOOK_STEAM_INVENTORY(ISteamInventory_GetResultItemProperty) HOOK_STEAM_INVENTORY(ISteamInventory_GetResultItemProperty)
} }
} else if (version_string.starts_with(CLIENT_ENGINE)) {
#if COMPILE_STORE_MODE
store::steamclient::process_client_engine(reinterpret_cast<uintptr_t>(interface));
#endif
} else { } else {
return; return;
} }
hooked_interfaces.insert(interface_pair); hooked_interfaces.insert(interface);
} }
HSteamPipe get_steam_pipe_or_throw() { HSteamPipe get_steam_pipe_or_throw() {
const auto& steam_api_module = win_util::get_module_handle_or_throw(ORIGINAL_DLL); const auto& steam_api_module = koalabox::win_util::get_module_handle_or_throw(STEAMAPI_DLL);
void* GetHSteamPipe_address; void* GetHSteamPipe_address;
try { try {
GetHSteamPipe_address = (void*) win_util::get_proc_address_or_throw( GetHSteamPipe_address = (void*) koalabox::win_util::get_proc_address_or_throw(
steam_api_module, "SteamAPI_GetHSteamPipe" steam_api_module, "SteamAPI_GetHSteamPipe"
); );
} catch (const Exception& ex) { } catch (const Exception& ex) {
GetHSteamPipe_address = (void*) win_util::get_proc_address_or_throw( GetHSteamPipe_address = (void*) koalabox::win_util::get_proc_address_or_throw(
steam_api_module, "GetHSteamPipe" steam_api_module, "GetHSteamPipe"
); );
} }
@@ -236,10 +245,10 @@ namespace steam_functions {
return function(ARGS(args...)); return function(ARGS(args...));
} }
uint32_t get_app_id_or_throw() { AppId_t get_app_id_or_throw() {
// Get CreateInterface // Get CreateInterface
const auto& steam_client_module = win_util::get_module_handle_or_throw(STEAMCLIENT_DLL); const auto& steam_client_module = koalabox::win_util::get_module_handle_or_throw(STEAMCLIENT_DLL);
auto* CreateInterface_address = (void*) win_util::get_proc_address_or_throw( auto* CreateInterface_address = (void*) koalabox::win_util::get_proc_address_or_throw(
steam_client_module, "CreateInterface" steam_client_module, "CreateInterface"
); );
auto* CreateInterface_o = PLH::FnCast(CreateInterface_address, CreateInterface); auto* CreateInterface_o = PLH::FnCast(CreateInterface_address, CreateInterface);
@@ -248,7 +257,7 @@ namespace steam_functions {
int result; int result;
auto* i_steam_client = CreateInterface_o("SteamClient006", &result); auto* i_steam_client = CreateInterface_o("SteamClient006", &result);
if (i_steam_client == nullptr) { if (i_steam_client == nullptr) {
throw util::exception("Failed to obtain SteamClient006 interface. Result: {}", result); throw koalabox::util::exception("Failed to obtain SteamClient006 interface. Result: {}", result);
} }
// Get GetISteamUtils // Get GetISteamUtils

View File

@@ -1,107 +1,11 @@
#include <steam_functions/steam_functions.hpp> #pragma once
using namespace koalabox; #include <core/types.hpp>
namespace steam_apps { namespace steam_impl {
bool IsDlcUnlocked(const String& function_name, AppId_t app_id, AppId_t dlc_id); void hook_virtuals(void* interface, const String& version_string);
int GetDLCCount(const String& function_name, AppId_t app_id, const std::function<int()>& original_function); uint32_t get_app_id_or_throw();
bool GetDLCDataByIndex(
const String& function_name,
AppId_t app_id,
int iDLC,
AppId_t* pDlcId,
bool* pbAvailable,
char* pchName,
int cchNameBufferSize,
const std::function<bool()>& original_function
);
} }
namespace steam_user {
EUserHasLicenseForAppResult UserHasLicenseForApp(
const String& function_name,
AppId_t appID,
const std::function<EUserHasLicenseForAppResult()>& original_function
);
}
namespace steam_client {
void* GetGenericInterface(
const String& function_name,
const String& interface_version,
const std::function<void*()>& original_function
);
}
namespace steam_inventory {
EResult GetResultStatus(
const String& function_name,
SteamInventoryResult_t resultHandle,
const std::function<EResult()>& original_function
);
bool GetResultItems(
const String& function_name,
SteamInventoryResult_t resultHandle,
SteamItemDetails_t* pOutItemsArray,
uint32_t* punOutItemsArraySize,
const std::function<bool()>& original_function,
const std::function<bool(SteamItemDef_t*, uint32_t*)>& get_item_definition_ids
);
bool GetResultItemProperty(
const String& function_name,
SteamInventoryResult_t resultHandle,
uint32_t unItemIndex,
const char* pchPropertyName,
char* pchValueBuffer,
uint32_t* punValueBufferSizeOut,
const std::function<bool()>& original_function
);
bool GetAllItems(
const String& function_name,
const SteamInventoryResult_t* pResultHandle,
const std::function<bool()>& original_function
);
bool GetItemsByID(
const String& function_name,
SteamInventoryResult_t* pResultHandle,
const SteamItemInstanceID_t* pInstanceIDs,
uint32_t unCountInstanceIDs,
const std::function<bool()>& original_function
);
bool SerializeResult(
const String& function_name,
SteamInventoryResult_t resultHandle,
void* pOutBuffer,
uint32_t* punOutBufferSize,
const std::function<bool()>& original_function
);
bool GetItemDefinitionIDs(
const String& function_name,
const SteamItemDef_t* pItemDefIDs,
uint32_t* punItemDefIDsArraySize,
const std::function<bool()>& original_function
);
bool CheckResultSteamID(
const String& function_name,
SteamInventoryResult_t resultHandle,
CSteamID steamIDExpected,
const std::function<bool()>& original_function
);
}

View File

@@ -1,16 +1,17 @@
#include <steam_impl/steam_impl.hpp> #include <steam_impl/steam_inventory.hpp>
#include <smoke_api/smoke_api.hpp> #include <smoke_api/config.hpp>
#include <koalabox/logger.hpp>
namespace steam_inventory { namespace steam_inventory {
EResult GetResultStatus( EResult GetResultStatus(
const String& function_name, const String& function_name,
const SteamInventoryResult_t resultHandle, const SteamInventoryResult_t resultHandle,
const std::function<EResult()>& original_function const Function<EResult()>& original_function
) { ) {
const auto status = original_function(); const auto status = original_function();
logger->debug("{} -> handle: {}, status: {}", function_name, resultHandle, (int) status); LOG_DEBUG("{} -> handle: {}, status: {}", function_name, resultHandle, (int) status)
return status; return status;
} }
@@ -20,62 +21,61 @@ namespace steam_inventory {
const SteamInventoryResult_t resultHandle, const SteamInventoryResult_t resultHandle,
SteamItemDetails_t* pOutItemsArray, SteamItemDetails_t* pOutItemsArray,
uint32_t* punOutItemsArraySize, uint32_t* punOutItemsArraySize,
const std::function<bool()>& original_function, const Function<bool()>& original_function,
const std::function<bool(SteamItemDef_t*, uint32_t*)>& get_item_definition_ids const Function<bool(SteamItemDef_t*, uint32_t*)>& get_item_definition_ids
) { ) {
static std::mutex section; static std::mutex section;
std::lock_guard<std::mutex> guard(section); const std::lock_guard<std::mutex> guard(section);
const auto success = original_function(); const auto success = original_function();
auto print_item = [](const String& tag, const SteamItemDetails_t& item) { auto print_item = [](const String& tag, const SteamItemDetails_t& item) {
logger->debug( LOG_DEBUG(
" [{}] definitionId: {}, itemId: {}, quantity: {}, flags: {}", " [{}] definitionId: {}, itemId: {}, quantity: {}, flags: {}",
tag, item.m_iDefinition, item.m_itemId, item.m_unQuantity, item.m_unFlags tag, item.m_iDefinition, item.m_itemId, item.m_unQuantity, item.m_unFlags
); )
}; };
if (not success) { if (not success) {
logger->debug("{} -> original result is false", function_name); LOG_DEBUG("{} -> original result is false", function_name)
return success; return success;
} }
if (punOutItemsArraySize == nullptr) { if (punOutItemsArraySize == nullptr) {
logger->error("{} -> arraySize pointer is null", function_name); LOG_ERROR("{} -> arraySize pointer is null", function_name)
return success; return success;
} }
logger->debug( LOG_DEBUG(
"{} -> handle: {}, pOutItemsArray: {}, arraySize: {}", "{} -> handle: {}, pOutItemsArray: {}, arraySize: {}",
function_name, resultHandle, fmt::ptr(pOutItemsArray), *punOutItemsArraySize function_name, resultHandle, fmt::ptr(pOutItemsArray), *punOutItemsArraySize
); )
static uint32_t original_count = 0; static uint32_t original_count = 0;
const auto injected_count = smoke_api::config.inventory_items.size(); const auto injected_count = smoke_api::config::instance.extra_inventory_items.size();
// Automatically get inventory items from steam // Automatically get inventory items from steam
static Vector<SteamItemDef_t> auto_inventory_items; static Vector<SteamItemDef_t> auto_inventory_items;
if (smoke_api::config.auto_inject_inventory) { if (smoke_api::config::instance.auto_inject_inventory) {
static std::once_flag flag; CALL_ONCE({
std::call_once(flag, [&]() {
uint32_t count = 0; uint32_t count = 0;
if (get_item_definition_ids(nullptr, &count)) { if (get_item_definition_ids(nullptr, &count)) {
auto_inventory_items.resize(count); auto_inventory_items.resize(count);
get_item_definition_ids(auto_inventory_items.data(), &count); get_item_definition_ids(auto_inventory_items.data(), &count);
} }
}); })
} }
const auto auto_injected_count = auto_inventory_items.size();
const auto auto_injected_count = auto_inventory_items.size();
if (not pOutItemsArray) { if (not pOutItemsArray) {
// If pOutItemsArray is NULL then we must set the array size. // If pOutItemsArray is NULL then we must set the array size.
original_count = *punOutItemsArraySize; original_count = *punOutItemsArraySize;
*punOutItemsArraySize += auto_injected_count + injected_count; *punOutItemsArraySize += auto_injected_count + injected_count;
logger->debug( LOG_DEBUG(
"{} -> Original count: {}, Total count: {}", "{} -> Original count: {}, Total count: {}",
function_name, original_count, *punOutItemsArraySize function_name, original_count, *punOutItemsArraySize
); )
} else { } else {
// Otherwise, we modify the array // Otherwise, we modify the array
for (int i = 0; i < original_count; i++) { for (int i = 0; i < original_count; i++) {
@@ -102,7 +102,7 @@ namespace steam_inventory {
for (int i = 0; i < injected_count; i++) { for (int i = 0; i < injected_count; i++) {
auto& item = pOutItemsArray[original_count + auto_injected_count + i]; auto& item = pOutItemsArray[original_count + auto_injected_count + i];
const auto item_def_id = smoke_api::config.inventory_items[i]; const auto item_def_id = smoke_api::config::instance.extra_inventory_items[i];
item = new_item(item_def_id); item = new_item(item_def_id);
@@ -119,8 +119,8 @@ namespace steam_inventory {
uint32_t unItemIndex, uint32_t unItemIndex,
const char* pchPropertyName, const char* pchPropertyName,
char* pchValueBuffer, char* pchValueBuffer,
uint32_t* punValueBufferSizeOut, const uint32_t* punValueBufferSizeOut,
const std::function<bool()>& original_function const Function<bool()>& original_function
) { ) {
const auto common_info = fmt::format( const auto common_info = fmt::format(
"{} -> Handle: {}, Index: {}, Name: '{}'", function_name, resultHandle, unItemIndex, pchPropertyName "{} -> Handle: {}, Index: {}, Name: '{}'", function_name, resultHandle, unItemIndex, pchPropertyName
@@ -129,11 +129,11 @@ namespace steam_inventory {
const auto success = original_function(); const auto success = original_function();
if (!success) { if (!success) {
logger->warn("{}, Result is false", common_info); LOG_WARN("{}, Result is false", common_info)
return false; return false;
} }
logger->debug("{}, Buffer: '{}'", common_info, String(pchValueBuffer, *punValueBufferSizeOut - 1)); LOG_DEBUG("{}, Buffer: '{}'", common_info, String(pchValueBuffer, *punValueBufferSizeOut - 1))
return success; return success;
} }
@@ -141,11 +141,11 @@ namespace steam_inventory {
bool GetAllItems( bool GetAllItems(
const String& function_name, const String& function_name,
const SteamInventoryResult_t* pResultHandle, const SteamInventoryResult_t* pResultHandle,
const std::function<bool()>& original_function const Function<bool()>& original_function
) { ) {
const auto success = original_function(); const auto success = original_function();
logger->debug("{} -> Handle: {}", function_name, fmt::ptr(pResultHandle)); LOG_DEBUG("{} -> Handle: {}", function_name, fmt::ptr(pResultHandle))
return success; return success;
} }
@@ -156,15 +156,15 @@ namespace steam_inventory {
SteamInventoryResult_t* pResultHandle, SteamInventoryResult_t* pResultHandle,
const SteamItemInstanceID_t* pInstanceIDs, const SteamItemInstanceID_t* pInstanceIDs,
const uint32_t unCountInstanceIDs, const uint32_t unCountInstanceIDs,
const std::function<bool()>& original_function const Function<bool()>& original_function
) { ) {
const auto success = original_function(); const auto success = original_function();
logger->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++) {
logger->debug(" Index: {}, ItemId: {}", i, pInstanceIDs[i]); LOG_DEBUG(" Index: {}, ItemId: {}", i, pInstanceIDs[i])
} }
} }
@@ -176,15 +176,15 @@ namespace steam_inventory {
SteamInventoryResult_t resultHandle, SteamInventoryResult_t resultHandle,
void* pOutBuffer, void* pOutBuffer,
uint32_t* punOutBufferSize, uint32_t* punOutBufferSize,
const std::function<bool()>& original_function const Function<bool()>& original_function
) { ) {
const auto success = original_function(); const auto success = original_function();
if (pOutBuffer != nullptr) { if (pOutBuffer != nullptr) {
String buffer((char*) pOutBuffer, *punOutBufferSize); String buffer((char*) pOutBuffer, *punOutBufferSize);
logger->debug("{} -> Handle: {}, Buffer: '{}'", function_name, resultHandle, buffer); LOG_DEBUG("{} -> Handle: {}, Buffer: '{}'", function_name, resultHandle, buffer)
} else { } else {
logger->debug("{} -> Handle: {}, Size: '{}'", function_name, resultHandle, *punOutBufferSize); LOG_DEBUG("{} -> Handle: {}, Size: '{}'", function_name, resultHandle, *punOutBufferSize)
} }
return success; return success;
@@ -194,23 +194,23 @@ namespace steam_inventory {
const String& function_name, const String& function_name,
const SteamItemDef_t* pItemDefIDs, const SteamItemDef_t* pItemDefIDs,
uint32_t* punItemDefIDsArraySize, uint32_t* punItemDefIDsArraySize,
const std::function<bool()>& original_function const Function<bool()>& original_function
) { ) {
const auto success = original_function(); const auto success = original_function();
if (!success) { if (!success) {
logger->warn("{} -> Result is false", function_name); LOG_WARN("{} -> Result is false", function_name)
return false; return false;
} }
if (punItemDefIDsArraySize) { if (punItemDefIDsArraySize) {
logger->debug("{} -> Size: {}", function_name, *punItemDefIDsArraySize); LOG_DEBUG("{} -> Size: {}", function_name, *punItemDefIDsArraySize)
} }
if (pItemDefIDs) { // Definitions were copied if (pItemDefIDs) { // Definitions were copied
for (int i = 0; i < *punItemDefIDsArraySize; i++) { for (int i = 0; i < *punItemDefIDsArraySize; i++) {
const auto& def = pItemDefIDs[i]; const auto& def = pItemDefIDs[i];
logger->debug(" Index: {}, ID: {}", i, def); LOG_DEBUG(" Index: {}, ID: {}", i, def)
} }
} }
@@ -221,14 +221,14 @@ namespace steam_inventory {
const String& function_name, const String& function_name,
SteamInventoryResult_t resultHandle, SteamInventoryResult_t resultHandle,
CSteamID steamIDExpected, CSteamID steamIDExpected,
const std::function<bool()>& original_function const Function<bool()>& original_function
) { ) {
const auto result = original_function(); const auto result = original_function();
logger->debug( LOG_DEBUG(
"{} -> handle: {}, steamID: {}, original result: {}", "{} -> handle: {}, steamID: {}, original result: {}",
function_name, resultHandle, steamIDExpected, result function_name, resultHandle, steamIDExpected, result
); )
return true; return true;
} }

View File

@@ -0,0 +1,68 @@
#pragma once
#include <core/types.hpp>
namespace steam_inventory {
EResult GetResultStatus(
const String& function_name,
SteamInventoryResult_t resultHandle,
const Function<EResult()>& original_function
);
bool GetResultItems(
const String& function_name,
SteamInventoryResult_t resultHandle,
SteamItemDetails_t* pOutItemsArray,
uint32_t* punOutItemsArraySize,
const Function<bool()>& original_function,
const Function<bool(SteamItemDef_t*, uint32_t*)>& get_item_definition_ids
);
bool GetResultItemProperty(
const String& function_name,
SteamInventoryResult_t resultHandle,
uint32_t unItemIndex,
const char* pchPropertyName,
char* pchValueBuffer,
const uint32_t* punValueBufferSizeOut,
const Function<bool()>& original_function
);
bool GetAllItems(
const String& function_name,
const SteamInventoryResult_t* pResultHandle,
const Function<bool()>& original_function
);
bool GetItemsByID(
const String& function_name,
SteamInventoryResult_t* pResultHandle,
const SteamItemInstanceID_t* pInstanceIDs,
uint32_t unCountInstanceIDs,
const Function<bool()>& original_function
);
bool SerializeResult(
const String& function_name,
SteamInventoryResult_t resultHandle,
void* pOutBuffer,
uint32_t* punOutBufferSize,
const Function<bool()>& original_function
);
bool GetItemDefinitionIDs(
const String& function_name,
const SteamItemDef_t* pItemDefIDs,
uint32_t* punItemDefIDsArraySize,
const Function<bool()>& original_function
);
bool CheckResultSteamID(
const String& function_name,
SteamInventoryResult_t resultHandle,
CSteamID steamIDExpected,
const Function<bool()>& original_function
);
}

View File

@@ -1,23 +1,29 @@
#include <steam_impl/steam_impl.hpp> #include <steam_impl/steam_user.hpp>
#include <smoke_api/smoke_api.hpp> #include <smoke_api/config.hpp>
#include <koalabox/logger.hpp>
namespace steam_user { namespace steam_user {
EUserHasLicenseForAppResult UserHasLicenseForApp( EUserHasLicenseForAppResult UserHasLicenseForApp(
const String& function_name, const String& function_name,
AppId_t appID, AppId_t appId,
const std::function<EUserHasLicenseForAppResult()>& original_function AppId_t dlcId,
const Function<EUserHasLicenseForAppResult()>& original_function
) { ) {
const auto result = original_function(); const auto result = original_function();
if (result == k_EUserHasLicenseResultNoAuth) { if (result == k_EUserHasLicenseResultNoAuth) {
logger->warn("{} -> App ID: {}, Result: NoAuth", function_name, appID); LOG_WARN("{} -> App ID: {:>8}, Result: NoAuth", function_name, dlcId)
return result; return result;
} }
const auto has_license = smoke_api::should_unlock(appID); const auto has_license = smoke_api::config::is_dlc_unlocked(
appId, dlcId, [&]() {
return result == k_EUserHasLicenseResultHasLicense;
}
);
logger->info("{} -> App ID: {}, HasLicense: {}", function_name, appID, has_license); LOG_INFO("{} -> App ID: {:>8}, HasLicense: {}", function_name, dlcId, has_license)
return has_license return has_license
? k_EUserHasLicenseResultHasLicense ? k_EUserHasLicenseResultHasLicense

View File

@@ -0,0 +1,14 @@
#pragma once
#include <core/types.hpp>
namespace steam_user {
EUserHasLicenseForAppResult UserHasLicenseForApp(
const String& function_name,
AppId_t appId,
AppId_t dlcId,
const Function<EUserHasLicenseForAppResult()>& original_function
);
}

View File

@@ -1,146 +0,0 @@
#pragma once
// results from UserHasLicenseForApp
enum EUserHasLicenseForAppResult {
k_EUserHasLicenseResultHasLicense = 0, // User has a license for specified app
k_EUserHasLicenseResultDoesNotHaveLicense = 1, // User does not have a license for the specified app
k_EUserHasLicenseResultNoAuth = 2, // User has not been authenticated
};
typedef uint32_t SteamInventoryResult_t;
typedef uint64_t SteamItemInstanceID_t;
typedef uint32_t SteamItemDef_t;
struct SteamItemDetails_t {
SteamItemInstanceID_t m_itemId;
uint32_t m_iDefinition;
uint16_t m_unQuantity;
uint16_t m_unFlags; // see ESteamItemFlags
};
enum EResult {
k_EResultNone = 0, // no result
k_EResultOK = 1, // success
k_EResultFail = 2, // generic failure
k_EResultNoConnection = 3, // no/failed network connection
// k_EResultNoConnectionRetry = 4, // OBSOLETE - removed
k_EResultInvalidPassword = 5, // password/ticket is invalid
k_EResultLoggedInElsewhere = 6, // same user logged in elsewhere
k_EResultInvalidProtocolVer = 7, // protocol version is incorrect
k_EResultInvalidParam = 8, // a parameter is incorrect
k_EResultFileNotFound = 9, // file was not found
k_EResultBusy = 10, // called method busy - action not taken
k_EResultInvalidState = 11, // called object was in an invalid state
k_EResultInvalidName = 12, // name is invalid
k_EResultInvalidEmail = 13, // email is invalid
k_EResultDuplicateName = 14, // name is not unique
k_EResultAccessDenied = 15, // access is denied
k_EResultTimeout = 16, // operation timed out
k_EResultBanned = 17, // VAC2 banned
k_EResultAccountNotFound = 18, // account not found
k_EResultInvalidSteamID = 19, // steamID is invalid
k_EResultServiceUnavailable = 20, // The requested service is currently unavailable
k_EResultNotLoggedOn = 21, // The user is not logged on
k_EResultPending = 22, // Request is pending (may be in process, or waiting on third party)
k_EResultEncryptionFailure = 23, // Encryption or Decryption failed
k_EResultInsufficientPrivilege = 24, // Insufficient privilege
k_EResultLimitExceeded = 25, // Too much of a good thing
k_EResultRevoked = 26, // Access has been revoked (used for revoked guest passes)
k_EResultExpired = 27, // License/Guest pass the user is trying to access is expired
k_EResultAlreadyRedeemed = 28, // Guest pass has already been redeemed by account, cannot be acked again
k_EResultDuplicateRequest = 29, // The request is a duplicate and the action has already occurred in the past, ignored this time
k_EResultAlreadyOwned = 30, // All the games in this guest pass redemption request are already owned by the user
k_EResultIPNotFound = 31, // IP address not found
k_EResultPersistFailed = 32, // failed to write change to the data store
k_EResultLockingFailed = 33, // failed to acquire access lock for this operation
k_EResultLogonSessionReplaced = 34,
k_EResultConnectFailed = 35,
k_EResultHandshakeFailed = 36,
k_EResultIOFailure = 37,
k_EResultRemoteDisconnect = 38,
k_EResultShoppingCartNotFound = 39, // failed to find the shopping cart requested
k_EResultBlocked = 40, // a user didn't allow it
k_EResultIgnored = 41, // target is ignoring sender
k_EResultNoMatch = 42, // nothing matching the request found
k_EResultAccountDisabled = 43,
k_EResultServiceReadOnly = 44, // this service is not accepting content changes right now
k_EResultAccountNotFeatured = 45, // account doesn't have value, so this feature isn't available
k_EResultAdministratorOK = 46, // allowed to take this action, but only because requester is admin
k_EResultContentVersion = 47, // A Version mismatch in content transmitted within the Steam protocol.
k_EResultTryAnotherCM = 48, // The current CM can't service the user making a request, user should try another.
k_EResultPasswordRequiredToKickSession = 49,// You are already logged in elsewhere, this cached credential login has failed.
k_EResultAlreadyLoggedInElsewhere = 50, // You are already logged in elsewhere, you must wait
k_EResultSuspended = 51, // Long running operation (content download) suspended/paused
k_EResultCancelled = 52, // Operation canceled (typically by user: content download)
k_EResultDataCorruption = 53, // Operation canceled because data is ill formed or unrecoverable
k_EResultDiskFull = 54, // Operation canceled - not enough disk space.
k_EResultRemoteCallFailed = 55, // an remote call or IPC call failed
k_EResultPasswordUnset = 56, // Password could not be verified as it's unset server side
k_EResultExternalAccountUnlinked = 57, // External account (PSN, Facebook...) is not linked to a Steam account
k_EResultPSNTicketInvalid = 58, // PSN ticket was invalid
k_EResultExternalAccountAlreadyLinked = 59, // External account (PSN, Facebook...) is already linked to some other account, must explicitly request to replace/delete the link first
k_EResultRemoteFileConflict = 60, // The sync cannot resume due to a conflict between the local and remote files
k_EResultIllegalPassword = 61, // The requested new password is not legal
k_EResultSameAsPreviousValue = 62, // new value is the same as the old one ( secret question and answer )
k_EResultAccountLogonDenied = 63, // account login denied due to 2nd factor authentication failure
k_EResultCannotUseOldPassword = 64, // The requested new password is not legal
k_EResultInvalidLoginAuthCode = 65, // account login denied due to auth code invalid
k_EResultAccountLogonDeniedNoMail = 66, // account login denied due to 2nd factor auth failure - and no mail has been sent
k_EResultHardwareNotCapableOfIPT = 67, //
k_EResultIPTInitError = 68, //
k_EResultParentalControlRestricted = 69, // operation failed due to parental control restrictions for current user
k_EResultFacebookQueryError = 70, // Facebook query returned an error
k_EResultExpiredLoginAuthCode = 71, // account login denied due to auth code expired
k_EResultIPLoginRestrictionFailed = 72,
k_EResultAccountLockedDown = 73,
k_EResultAccountLogonDeniedVerifiedEmailRequired = 74,
k_EResultNoMatchingURL = 75,
k_EResultBadResponse = 76, // parse failure, missing field, etc.
k_EResultRequirePasswordReEntry = 77, // The user cannot complete the action until they re-enter their password
k_EResultValueOutOfRange = 78, // the value entered is outside the acceptable range
k_EResultUnexpectedError = 79, // something happened that we didn't expect to ever happen
k_EResultDisabled = 80, // The requested service has been configured to be unavailable
k_EResultInvalidCEGSubmission = 81, // The set of files submitted to the CEG server are not valid !
k_EResultRestrictedDevice = 82, // The device being used is not allowed to perform this action
k_EResultRegionLocked = 83, // The action could not be complete because it is region restricted
k_EResultRateLimitExceeded = 84, // Temporary rate limit exceeded, try again later, different from k_EResultLimitExceeded which may be permanent
k_EResultAccountLoginDeniedNeedTwoFactor = 85, // Need two-factor code to login
k_EResultItemDeleted = 86, // The thing we're trying to access has been deleted
k_EResultAccountLoginDeniedThrottle = 87, // login attempt failed, try to throttle response to possible attacker
k_EResultTwoFactorCodeMismatch = 88, // two factor code mismatch
k_EResultTwoFactorActivationCodeMismatch = 89, // activation code for two-factor didn't match
k_EResultAccountAssociatedToMultiplePartners = 90, // account has been associated with multiple partners
k_EResultNotModified = 91, // data not modified
k_EResultNoMobileDevice = 92, // the account does not have a mobile device associated with it
k_EResultTimeNotSynced = 93, // the time presented is out of range or tolerance
k_EResultSmsCodeFailed = 94, // SMS code failure (no match, none pending, etc.)
k_EResultAccountLimitExceeded = 95, // Too many accounts access this resource
k_EResultAccountActivityLimitExceeded = 96, // Too many changes to this account
k_EResultPhoneActivityLimitExceeded = 97, // Too many changes to this phone
k_EResultRefundToWallet = 98, // Cannot refund to payment method, must use wallet
k_EResultEmailSendFailure = 99, // Cannot send an email
k_EResultNotSettled = 100, // Can't perform operation till payment has settled
k_EResultNeedCaptcha = 101, // Needs to provide a valid captcha
k_EResultGSLTDenied = 102, // a game server login token owned by this token's owner has been banned
k_EResultGSOwnerDenied = 103, // game server owner is denied for other reason (account lock, community ban, vac ban, missing phone)
k_EResultInvalidItemType = 104, // the type of thing we were requested to act on is invalid
k_EResultIPBanned = 105, // the ip address has been banned from taking this action
k_EResultGSLTExpired = 106, // this token has expired from disuse; can be reset for use
k_EResultInsufficientFunds = 107, // user doesn't have enough wallet funds to complete the action
k_EResultTooManyPending = 108, // There are too many of this thing pending already
k_EResultNoSiteLicensesFound = 109, // No site licenses found
k_EResultWGNetworkSendExceeded = 110, // the WG couldn't send a response because we exceeded max network send size
k_EResultAccountNotFriends = 111, // the user is not mutually friends
k_EResultLimitedUserAccount = 112, // the user is limited
k_EResultCantRemoveItem = 113, // item can't be removed
k_EResultAccountDeleted = 114, // account has been deleted
k_EResultExistingUserCancelledLicense = 115, // A license for this already exists, but cancelled
k_EResultCommunityCooldown = 116, // access is denied because of a community cooldown (probably from support profile data resets)
k_EResultNoLauncherSpecified = 117, // No launcher was specified, but a launcher was needed to choose correct realm for operation.
k_EResultMustAgreeToSSA = 118, // User must agree to china SSA or global SSA before login
k_EResultLauncherMigrated = 119, // The specified launcher type is no longer supported; the user should be directed elsewhere
k_EResultSteamRealmMismatch = 120, // The user's realm does not match the realm of the requested resource
k_EResultInvalidSignature = 121, // signature check did not match
k_EResultParseFailure = 122, // Failed to parse input
k_EResultNoVerifiedPhone = 123, // account does not have a verified phone number
};

View File

@@ -1,13 +0,0 @@
#include <smoke_api/smoke_api.hpp>
#include <steam_functions/steam_functions.hpp>
#include <steam_impl/steam_impl.hpp>
using namespace smoke_api;
DLL_EXPORT(void*) CreateInterface(const char* interface_string, int* out_result) {
return steam_client::GetGenericInterface(__func__, interface_string, [&]() {
GET_ORIGINAL_FUNCTION(CreateInterface)
return CreateInterface_o(interface_string, out_result);
});
}

View File

@@ -1,13 +0,0 @@
#include <smoke_api/smoke_api.hpp>
#include <steam_impl/steam_impl.hpp>
using namespace smoke_api;
VIRTUAL(bool) IClientAppManager_IsAppDlcInstalled(
PARAMS( // NOLINT(misc-unused-parameters)
AppId_t app_id,
AppId_t dlc_id
)
) {
return steam_apps::IsDlcUnlocked(__func__, app_id, dlc_id);
}

View File

@@ -1,31 +0,0 @@
#include <smoke_api/smoke_api.hpp>
#include <steam_impl/steam_impl.hpp>
using namespace smoke_api;
VIRTUAL(int) IClientApps_GetDLCCount(PARAMS(AppId_t appId)) {
return steam_apps::GetDLCCount(__func__, appId, [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(IClientApps_GetDLCCount)
return IClientApps_GetDLCCount_o(ARGS(appId));
});
}
VIRTUAL(bool) IClientApps_BGetDLCDataByIndex(
PARAMS(
AppId_t appID,
int iDLC,
AppId_t* pDlcID,
bool* pbAvailable,
char* pchName,
int cchNameBufferSize
)
) {
return steam_apps::GetDLCDataByIndex(__func__, appID, iDLC, pDlcID, pbAvailable, pchName, cchNameBufferSize, [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(IClientApps_BGetDLCDataByIndex)
return IClientApps_BGetDLCDataByIndex_o(
ARGS(appID, iDLC, pDlcID, pbAvailable, pchName, cchNameBufferSize)
);
});
}

View File

@@ -1,8 +0,0 @@
#include <smoke_api/smoke_api.hpp>
#include <steam_impl/steam_impl.hpp>
using namespace smoke_api;
VIRTUAL(bool) IClientUser_IsSubscribedApp(PARAMS(AppId_t app_id)) { // NOLINT(misc-unused-parameters)
return steam_apps::IsDlcUnlocked(__func__, 0, app_id);
}

View File

@@ -0,0 +1,12 @@
#include <store_mode/steamclient/steamclient.hpp>
#include <steam_impl/steam_apps.hpp>
VIRTUAL(bool) IClientAppManager_IsAppDlcInstalled(PARAMS(AppId_t app_id, AppId_t dlc_id)) {
return steam_apps::IsDlcUnlocked(
__func__, app_id, dlc_id, [&]() {
GET_ORIGINAL_HOOKED_FUNCTION(IClientAppManager_IsAppDlcInstalled)
return IClientAppManager_IsAppDlcInstalled_o(ARGS(app_id, dlc_id));
}
);
}

View File

@@ -0,0 +1,44 @@
#include <store_mode/steamclient/steamclient.hpp>
#include <steam_impl/steam_apps.hpp>
VIRTUAL(int) IClientApps_GetDLCCount(PARAMS(AppId_t appId)) {
return steam_apps::GetDLCCount(
__func__, appId, [&]() {
GET_ORIGINAL_HOOKED_FUNCTION(IClientApps_GetDLCCount)
return IClientApps_GetDLCCount_o(ARGS(appId));
}
);
}
VIRTUAL(bool) IClientApps_BGetDLCDataByIndex(
PARAMS(
AppId_t appID,
int iDLC,
AppId_t* pDlcID,
bool* pbAvailable,
char* pchName,
int cchNameBufferSize
)
) {
return steam_apps::GetDLCDataByIndex(
__func__, appID, iDLC, pDlcID, pbAvailable, pchName, cchNameBufferSize,
[&]() {
GET_ORIGINAL_HOOKED_FUNCTION(IClientApps_BGetDLCDataByIndex)
return IClientApps_BGetDLCDataByIndex_o(
ARGS(appID, iDLC, pDlcID, pbAvailable, pchName, cchNameBufferSize)
);
},
[&](AppId_t dlc_id) {
const auto* app_manager_interface = store::steamclient::interface_name_to_address_map["IClientAppManager"];
if (app_manager_interface) {
IClientAppManager_IsAppDlcInstalled(app_manager_interface, EDX, appID, dlc_id);
return true;
}
// Would never happen in practice, as the interfaces would be instantiated almost simultaneously
return false;
}
);
}

View File

@@ -1,14 +1,14 @@
#include <smoke_api/smoke_api.hpp> #include <store_mode/steamclient/steamclient.hpp>
#include <steam_impl/steam_impl.hpp> #include <steam_impl/steam_inventory.hpp>
using namespace smoke_api;
VIRTUAL(EResult) IClientInventory_GetResultStatus(PARAMS(SteamInventoryResult_t resultHandle)) { VIRTUAL(EResult) IClientInventory_GetResultStatus(PARAMS(SteamInventoryResult_t resultHandle)) {
return steam_inventory::GetResultStatus(__func__, resultHandle, [&]() { return steam_inventory::GetResultStatus(
GET_ORIGINAL_VIRTUAL_FUNCTION(IClientInventory_GetResultStatus) __func__, resultHandle, [&]() {
GET_ORIGINAL_HOOKED_FUNCTION(IClientInventory_GetResultStatus)
return IClientInventory_GetResultStatus_o(ARGS(resultHandle)); return IClientInventory_GetResultStatus_o(ARGS(resultHandle));
}); }
);
} }
VIRTUAL(bool) IClientInventory_GetResultItems( VIRTUAL(bool) IClientInventory_GetResultItems(
@@ -19,11 +19,10 @@ VIRTUAL(bool) IClientInventory_GetResultItems(
uint32_t * punOutItemsArraySize // 1st pass: ptr to out array size, 2nd pass: ptr to 0 uint32_t * punOutItemsArraySize // 1st pass: ptr to out array size, 2nd pass: ptr to 0
) )
) { ) {
return steam_inventory::GetResultItems( return steam_inventory::GetResultItems(
__func__, resultHandle, pOutItemsArray, punOutItemsArraySize, __func__, resultHandle, pOutItemsArray, punOutItemsArraySize,
[&]() { [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(IClientInventory_GetResultItems) GET_ORIGINAL_HOOKED_FUNCTION(IClientInventory_GetResultItems)
*punOutItemsArraySize = item_count; *punOutItemsArraySize = item_count;
return IClientInventory_GetResultItems_o( return IClientInventory_GetResultItems_o(
@@ -31,7 +30,7 @@ VIRTUAL(bool) IClientInventory_GetResultItems(
); );
}, },
[&](SteamItemDef_t* pItemDefIDs, uint32_t* punItemDefIDsArraySize) { [&](SteamItemDef_t* pItemDefIDs, uint32_t* punItemDefIDsArraySize) {
GET_ORIGINAL_VIRTUAL_FUNCTION(IClientInventory_GetItemDefinitionIDs) GET_ORIGINAL_HOOKED_FUNCTION(IClientInventory_GetItemDefinitionIDs)
return IClientInventory_GetItemDefinitionIDs_o( return IClientInventory_GetItemDefinitionIDs_o(
ARGS(pItemDefIDs, *punItemDefIDsArraySize, punItemDefIDsArraySize) ARGS(pItemDefIDs, *punItemDefIDsArraySize, punItemDefIDsArraySize)
@@ -40,7 +39,6 @@ VIRTUAL(bool) IClientInventory_GetResultItems(
); );
} }
// TODO: Verify this function [ ] signature, in-game [ ]
VIRTUAL(bool) IClientInventory_GetResultItemProperty( VIRTUAL(bool) IClientInventory_GetResultItemProperty(
PARAMS( PARAMS(
SteamInventoryResult_t resultHandle, SteamInventoryResult_t resultHandle,
@@ -53,7 +51,7 @@ VIRTUAL(bool) IClientInventory_GetResultItemProperty(
) { ) {
return steam_inventory::GetResultItemProperty( return steam_inventory::GetResultItemProperty(
__func__, resultHandle, unItemIndex, pchPropertyName, pchValueBuffer, punValueBufferSizeOut, [&]() { __func__, resultHandle, unItemIndex, pchPropertyName, pchValueBuffer, punValueBufferSizeOut, [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(IClientInventory_GetResultItemProperty) GET_ORIGINAL_HOOKED_FUNCTION(IClientInventory_GetResultItemProperty)
*punValueBufferSizeOut = item_count; *punValueBufferSizeOut = item_count;
return IClientInventory_GetResultItemProperty_o( return IClientInventory_GetResultItemProperty_o(
@@ -63,7 +61,6 @@ VIRTUAL(bool) IClientInventory_GetResultItemProperty(
); );
} }
// TODO: Verify this function [x] signature, in-game [ ]
VIRTUAL(bool) IClientInventory_CheckResultSteamID( VIRTUAL(bool) IClientInventory_CheckResultSteamID(
PARAMS( PARAMS(
SteamInventoryResult_t resultHandle, SteamInventoryResult_t resultHandle,
@@ -71,23 +68,20 @@ VIRTUAL(bool) IClientInventory_CheckResultSteamID(
) )
) { ) {
return steam_inventory::CheckResultSteamID(__func__, resultHandle, steamIDExpected, [&]() { return steam_inventory::CheckResultSteamID(__func__, resultHandle, steamIDExpected, [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(IClientInventory_CheckResultSteamID) GET_ORIGINAL_HOOKED_FUNCTION(IClientInventory_CheckResultSteamID)
return IClientInventory_CheckResultSteamID_o(ARGS(resultHandle, steamIDExpected)); return IClientInventory_CheckResultSteamID_o(ARGS(resultHandle, steamIDExpected));
}); });
} }
// TODO: Verify this function [x] signature, in-game [ ]
VIRTUAL(bool) IClientInventory_GetAllItems(PARAMS(SteamInventoryResult_t* pResultHandle)) { VIRTUAL(bool) IClientInventory_GetAllItems(PARAMS(SteamInventoryResult_t* pResultHandle)) {
return steam_inventory::GetAllItems(__func__, pResultHandle, [&]() { return steam_inventory::GetAllItems(__func__, pResultHandle, [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(IClientInventory_GetAllItems) GET_ORIGINAL_HOOKED_FUNCTION(IClientInventory_GetAllItems)
return IClientInventory_GetAllItems_o(ARGS(pResultHandle)); return IClientInventory_GetAllItems_o(ARGS(pResultHandle));
}); });
} }
// TODO: Verify this function [x] signature, in-game [ ]
VIRTUAL(bool) IClientInventory_GetItemsByID( VIRTUAL(bool) IClientInventory_GetItemsByID(
PARAMS( PARAMS(
SteamInventoryResult_t* pResultHandle, SteamInventoryResult_t* pResultHandle,
@@ -96,13 +90,12 @@ VIRTUAL(bool) IClientInventory_GetItemsByID(
) )
) { ) {
return steam_inventory::GetItemsByID(__func__, pResultHandle, pInstanceIDs, unCountInstanceIDs, [&]() { return steam_inventory::GetItemsByID(__func__, pResultHandle, pInstanceIDs, unCountInstanceIDs, [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(IClientInventory_GetItemsByID) GET_ORIGINAL_HOOKED_FUNCTION(IClientInventory_GetItemsByID)
return IClientInventory_GetItemsByID_o(ARGS(pResultHandle, pInstanceIDs, unCountInstanceIDs)); return IClientInventory_GetItemsByID_o(ARGS(pResultHandle, pInstanceIDs, unCountInstanceIDs));
}); });
} }
// TODO: Verify this function [x] signature, in-game [ ]
VIRTUAL(bool) IClientInventory_SerializeResult( VIRTUAL(bool) IClientInventory_SerializeResult(
PARAMS( PARAMS(
SteamInventoryResult_t resultHandle, SteamInventoryResult_t resultHandle,
@@ -112,7 +105,7 @@ VIRTUAL(bool) IClientInventory_SerializeResult(
) )
) { ) {
return steam_inventory::SerializeResult(__func__, resultHandle, pOutBuffer, punOutBufferSize, [&]() { return steam_inventory::SerializeResult(__func__, resultHandle, pOutBuffer, punOutBufferSize, [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(IClientInventory_SerializeResult) GET_ORIGINAL_HOOKED_FUNCTION(IClientInventory_SerializeResult)
*punOutBufferSize = buffer_size; *punOutBufferSize = buffer_size;
return IClientInventory_SerializeResult_o(ARGS(resultHandle, pOutBuffer, buffer_size, punOutBufferSize)); return IClientInventory_SerializeResult_o(ARGS(resultHandle, pOutBuffer, buffer_size, punOutBufferSize));
@@ -127,7 +120,7 @@ VIRTUAL(bool) IClientInventory_GetItemDefinitionIDs(
) )
) { ) {
return steam_inventory::GetItemDefinitionIDs(__func__, pItemDefIDs, p_array_size, [&]() { return steam_inventory::GetItemDefinitionIDs(__func__, pItemDefIDs, p_array_size, [&]() {
GET_ORIGINAL_VIRTUAL_FUNCTION(IClientInventory_GetItemDefinitionIDs) GET_ORIGINAL_HOOKED_FUNCTION(IClientInventory_GetItemDefinitionIDs)
*p_array_size = item_count; *p_array_size = item_count;
return IClientInventory_GetItemDefinitionIDs_o(ARGS(pItemDefIDs, item_count, p_array_size)); return IClientInventory_GetItemDefinitionIDs_o(ARGS(pItemDefIDs, item_count, p_array_size));

View File

@@ -0,0 +1,14 @@
#include <store_mode/steamclient/steamclient.hpp>
#include <steam_impl/steam_apps.hpp>
VIRTUAL(bool) IClientUser_BIsSubscribedApp(PARAMS(AppId_t dlc_id)) {
const auto* utils_interface = store::steamclient::interface_name_to_address_map["IClientUtils"];
const auto app_id = utils_interface ? IClientUtils_GetAppID(utils_interface, EDX) : 0;
return steam_apps::IsDlcUnlocked(__func__, app_id, dlc_id, [&]() {
GET_ORIGINAL_HOOKED_FUNCTION(IClientUser_BIsSubscribedApp)
return IClientUser_BIsSubscribedApp_o(ARGS(dlc_id));
});
}

View File

@@ -0,0 +1,7 @@
#include <store_mode/steamclient/steamclient.hpp>
VIRTUAL(AppId_t) IClientUtils_GetAppID(PARAMS()) {
GET_ORIGINAL_HOOKED_FUNCTION(IClientUtils_GetAppID)
return IClientUtils_GetAppID_o(ARGS());
}

View File

@@ -0,0 +1,456 @@
#include <store_mode/steamclient/steamclient.hpp>
#include <store_mode/store.hpp>
#include <koalabox/hook.hpp>
#include <koalabox/logger.hpp>
#include <koalabox/util.hpp>
#include <Zydis/Zydis.h>
#include <Zydis/DecoderTypes.h>
namespace store::steamclient {
using namespace koalabox;
Map<String, void*> interface_name_to_address_map; // NOLINT(cert-err58-cpp)
struct InstructionContext {
std::optional<ZydisRegister> base_register;
std::optional<String> function_name;
};
// map<interface name, map<function name, function ordinal>>
Map<String, Map<String, uint32_t>> ordinal_map; // NOLINT(cert-err58-cpp)
const auto MAX_INSTRUCTION_SIZE = 15;
ZydisDecoder decoder = {};
ZydisFormatter formatter = {};
void construct_ordinal_map( // NOLINT(misc-no-recursion)
const String& target_interface,
Map<String, uint32_t>& function_name_to_ordinal_map,
uintptr_t start_address
);
#define CONSTRUCT_ORDINAL_MAP(INTERFACE) \
construct_ordinal_map(#INTERFACE, ordinal_map[#INTERFACE], function_selector_address);
#define HOOK_FUNCTION(INTERFACE, FUNC) hook::swap_virtual_func_or_throw( \
globals::address_map, \
interface, \
#INTERFACE"_"#FUNC, \
ordinal_map[#INTERFACE][#FUNC], \
reinterpret_cast<uintptr_t>(INTERFACE##_##FUNC) \
);
#define SELECTOR_IMPLEMENTATION(INTERFACE, FUNC_BODY) \
DLL_EXPORT(void) INTERFACE##_Selector( \
void* interface, \
void* arg2, \
void* arg3, \
void* arg4 \
) { \
CALL_ONCE({ \
interface_name_to_address_map[#INTERFACE] = interface; \
[&]()FUNC_BODY(); \
}) \
GET_ORIGINAL_HOOKED_FUNCTION(INTERFACE##_Selector) \
INTERFACE##_Selector_o(interface, arg2, arg3, arg4); \
}
SELECTOR_IMPLEMENTATION(IClientAppManager, {
HOOK_FUNCTION(IClientAppManager, IsAppDlcInstalled)
})
SELECTOR_IMPLEMENTATION(IClientApps, {
HOOK_FUNCTION(IClientApps, GetDLCCount)
HOOK_FUNCTION(IClientApps, BGetDLCDataByIndex)
})
SELECTOR_IMPLEMENTATION(IClientInventory, {
HOOK_FUNCTION(IClientInventory, GetResultStatus)
HOOK_FUNCTION(IClientInventory, GetResultItems)
HOOK_FUNCTION(IClientInventory, GetResultItemProperty)
HOOK_FUNCTION(IClientInventory, CheckResultSteamID)
HOOK_FUNCTION(IClientInventory, GetAllItems)
HOOK_FUNCTION(IClientInventory, GetItemsByID)
HOOK_FUNCTION(IClientInventory, SerializeResult)
HOOK_FUNCTION(IClientInventory, GetItemDefinitionIDs)
})
SELECTOR_IMPLEMENTATION(IClientUser, {
HOOK_FUNCTION(IClientUser, BIsSubscribedApp)
})
SELECTOR_IMPLEMENTATION(IClientUtils, {
HOOK_FUNCTION(IClientUtils, GetAppID)
})
#define DETOUR_SELECTOR(INTERFACE) \
if(interface_name == #INTERFACE){ \
CONSTRUCT_ORDINAL_MAP(INTERFACE) \
DETOUR_ADDRESS(INTERFACE##_Selector, function_selector_address) \
}
void detour_interface_selector(const String& interface_name, uintptr_t function_selector_address) {
LOG_DEBUG("Detected interface: '{}'", interface_name)
DETOUR_SELECTOR(IClientAppManager)
DETOUR_SELECTOR(IClientApps)
DETOUR_SELECTOR(IClientInventory)
DETOUR_SELECTOR(IClientUser)
DETOUR_SELECTOR(IClientUtils)
}
uintptr_t get_absolute_address(ZydisDecodedInstruction instruction, uintptr_t address) {
const auto operand = instruction.operands[0];
if (operand.imm.is_relative) {
ZyanU64 absolute_address;
ZydisCalcAbsoluteAddress(&instruction, &operand, address, &absolute_address);
return absolute_address;
}
return (uintptr_t) operand.imm.value.u;
}
bool is_push_immediate(const ZydisDecodedInstruction& instruction) {
const auto& operand = instruction.operands[0];
return instruction.mnemonic == ZYDIS_MNEMONIC_PUSH &&
operand.type == ZYDIS_OPERAND_TYPE_IMMEDIATE &&
operand.visibility == ZYDIS_OPERAND_VISIBILITY_EXPLICIT &&
operand.encoding == ZYDIS_OPERAND_ENCODING_SIMM16_32_32;
}
std::optional<String> get_string_argument(const ZydisDecodedInstruction& instruction) {
const auto* name_address = reinterpret_cast<char*>(instruction.operands[0].imm.value.u);
if (util::is_valid_pointer(name_address)) {
return name_address;
}
return std::nullopt;
}
std::optional<String> get_instruction_string(
const ZydisDecodedInstruction& instruction,
const uintptr_t address
) {
const auto buffer_size = 64;
char buffer[buffer_size] = {};
if (ZYAN_SUCCESS(
ZydisFormatterFormatInstruction(
&formatter,
&instruction,
buffer,
buffer_size,
address
)
)) {
return buffer;
}
return std::nullopt;
}
std::optional<String> find_interface_name(uintptr_t selector_address) {
auto current_address = selector_address;
ZydisDecodedInstruction instruction{};
while (ZYAN_SUCCESS(ZydisDecoderDecodeBuffer(
&decoder,
(void*) current_address,
MAX_INSTRUCTION_SIZE,
&instruction
))) {
const auto debug_str = get_instruction_string(instruction, current_address);
if (is_push_immediate(instruction)) {
auto string_opt = get_string_argument(instruction);
if (string_opt && string_opt->starts_with("IClient")) {
return string_opt;
}
} else if (instruction.mnemonic == ZYDIS_MNEMONIC_RET) {
break;
}
current_address += instruction.length;
}
// LOG_WARN("Failed to find any interface names at {}", (void*) selector_address);
return std::nullopt;
}
/**
* Recursively walks through the code, until a return instruction is reached.
* Recursion occurs whenever a jump/branch is encountered.
*/
template<typename T>
void visit_code( // NOLINT(misc-no-recursion)
Set<uintptr_t>& visited_addresses,
uintptr_t start_address,
T context,
const Function<bool(
const ZydisDecodedInstruction& instruction,
const ZydisDecodedOperand& operand,
const uintptr_t& current_address,
T& context,
const std::list<ZydisDecodedInstruction>& instruction_list
)>& callback
) {
LOG_TRACE("{} -> start_address: {}", __func__, (void*) start_address)
if (visited_addresses.contains(start_address)) {
LOG_TRACE("Breaking recursion due to visited address")
return;
}
auto current_address = start_address;
std::list instruction_list{ZydisDecodedInstruction{}};
ZydisDecodedInstruction instruction{};
while (
ZYAN_SUCCESS(
ZydisDecoderDecodeBuffer(
&decoder,
(void*) current_address,
MAX_INSTRUCTION_SIZE,
&instruction
)
)) {
visited_addresses.insert(current_address);
LOG_TRACE(
"{} -> visiting {} │ {}",
__func__, (void*) current_address, *get_instruction_string(instruction, current_address)
)
const auto operand = instruction.operands[0];
const auto should_return = callback(instruction, operand, current_address, context, instruction_list);
if (should_return) {
return;
}
if (instruction.meta.category == ZYDIS_CATEGORY_COND_BR) {
const auto jump_taken_destination = get_absolute_address(instruction, current_address);
const auto jump_not_taken_destination = current_address + instruction.length;
visit_code(visited_addresses, jump_taken_destination, context, callback);
visit_code(visited_addresses, jump_not_taken_destination, context, callback);
LOG_TRACE("{} -> Breaking recursion due to a conditional branch", __func__)
return;
}
if (instruction.mnemonic == ZYDIS_MNEMONIC_JMP && operand.type == ZYDIS_OPERAND_TYPE_IMMEDIATE) {
const auto jump_destination = get_absolute_address(instruction, current_address);
visit_code(visited_addresses, jump_destination, context, callback);
LOG_TRACE("{} -> Breaking recursion due to an unconditional jump", __func__)
return;
}
if (instruction.mnemonic == ZYDIS_MNEMONIC_JMP &&
operand.type == ZYDIS_OPERAND_TYPE_MEMORY &&
operand.mem.scale == sizeof(uintptr_t) &&
operand.mem.disp.has_displacement) {
// Special handling for jump tables. Guaranteed to be present in the interface selector.
const auto* table = (uintptr_t*) operand.mem.disp.value;
const auto* table_entry = table;
while (util::is_valid_pointer((void*) *table_entry)) {
visit_code(visited_addresses, *table_entry, context, callback);
table_entry++;
}
LOG_TRACE("{} -> Breaking recursion due to a jump table", __func__)
return;
}
if (instruction.mnemonic == ZYDIS_MNEMONIC_RET) {
LOG_TRACE("{} -> Breaking recursion due to return instruction", __func__)
return;
}
// We push items to the front so that it becomes easy to iterate over instructions
// in reverse order of addition.
instruction_list.push_front(instruction);
current_address += instruction.length;
}
}
void construct_ordinal_map( // NOLINT(misc-no-recursion)
const String& target_interface,
Map<String, uint32_t>& function_name_to_ordinal_map,
uintptr_t start_address
) {
Set<uintptr_t> visited_addresses;
visit_code<InstructionContext>(visited_addresses, start_address, {}, [&](
const ZydisDecodedInstruction& instruction,
const ZydisDecodedOperand& operand,
const auto&,
InstructionContext& context,
const std::list<ZydisDecodedInstruction>& instruction_list
) {
if (context.function_name && function_name_to_ordinal_map.contains(*context.function_name)) {
// Avoid duplicate work
return true;
}
const auto& last_instruction = instruction_list.front();
const auto is_mov_base_esp = instruction.mnemonic == ZYDIS_MNEMONIC_MOV &&
instruction.operand_count == 2 &&
instruction.operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER &&
instruction.operands[1].reg.value == ZYDIS_REGISTER_ESP;
if (!context.base_register && is_mov_base_esp) {
// Save base register
context.base_register = instruction.operands[0].reg.value;
} else if (is_push_immediate(last_instruction) &&
is_push_immediate(instruction) &&
!context.function_name) {
// The very first 2 consecutive pushes indicate interface and function names.
// However, subsequent pushes may contain irrelevant strings.
const auto push_string_1 = get_string_argument(last_instruction);
const auto push_string_2 = get_string_argument(instruction);
if (push_string_1 && push_string_2) {
if (*push_string_1 == target_interface) {
context.function_name = push_string_2;
} else if (*push_string_2 == target_interface) {
context.function_name = push_string_1;
}
if (context.function_name && function_name_to_ordinal_map.contains(*context.function_name)) {
// Bail early to avoid duplicate work
return true;
}
}
} else if (instruction.mnemonic == ZYDIS_MNEMONIC_CALL) {
// On call instructions we should extract the ordinal
if (context.base_register && context.function_name) {
const auto& base_register = *(context.base_register);
const auto& function_name = *(context.function_name);
std::optional<uint32_t> offset;
auto last_destination_reg = ZYDIS_REGISTER_NONE;
bool is_derived_from_base_reg = false;
// Sometimes the offset is present in the call instruction itself,
// hence we can immediately obtain it.
if (operand.type == ZYDIS_OPERAND_TYPE_MEMORY && operand.mem.base != ZYDIS_REGISTER_NONE) {
offset = static_cast<uint32_t>(operand.mem.disp.value);
last_destination_reg = operand.mem.base;
} else if (operand.type == ZYDIS_OPERAND_TYPE_REGISTER) {
last_destination_reg = operand.reg.value;
}
for (const auto& previous_instruction: instruction_list) {
const auto& destination_operand = previous_instruction.operands[0];
const auto& source_operand = previous_instruction.operands[1];
// Extract offset if necessary
if (previous_instruction.mnemonic == ZYDIS_MNEMONIC_MOV &&
previous_instruction.operand_count == 2 &&
destination_operand.reg.value == last_destination_reg &&
source_operand.type == ZYDIS_OPERAND_TYPE_MEMORY) {
const auto source_mem = source_operand.mem;
if (source_mem.base == base_register &&
source_mem.disp.has_displacement &&
source_mem.disp.value == 8) {
// We have verified that the chain eventually leads up to the base register.
// Hence, we can conclude that the offset is valid.
is_derived_from_base_reg = true;
break;
}
// Otherwise, keep going through the chain
last_destination_reg = source_mem.base;
if (!offset) {
offset = static_cast<uint32_t>(source_mem.disp.value);
}
}
}
if (offset && is_derived_from_base_reg) {
const auto ordinal = *offset / sizeof(uintptr_t);
LOG_DEBUG("Found function ordinal {}::{}@{}", target_interface, function_name, ordinal)
function_name_to_ordinal_map[function_name] = ordinal;
return true;
}
}
}
return false;
}
);
}
void process_interface_selector( // NOLINT(misc-no-recursion)
const uintptr_t start_address,
Set<uintptr_t>& visited_addresses
) {
visit_code<nullptr_t>(visited_addresses, start_address, nullptr, [](
const ZydisDecodedInstruction& instruction,
const ZydisDecodedOperand& operand,
const auto& current_address,
auto,
const auto&
) {
if (instruction.mnemonic == ZYDIS_MNEMONIC_CALL && operand.type == ZYDIS_OPERAND_TYPE_IMMEDIATE) {
LOG_TRACE("Found call instruction at {}", (void*) current_address)
const auto function_selector_address = get_absolute_address(instruction, current_address);
const auto interface_name_opt = find_interface_name(function_selector_address);
if (interface_name_opt) {
const auto& interface_name = *interface_name_opt;
detour_interface_selector(interface_name, function_selector_address);
}
}
return false;
}
);
}
void process_client_engine(uintptr_t interface) {
const auto* steam_client_internal = ((uintptr_t***) interface)[
store::config.client_engine_steam_client_internal_ordinal
];
const auto interface_selector_address = (*steam_client_internal)[
store::config.steam_client_internal_interface_selector_ordinal
];
LOG_DEBUG("Found interface selector at: {}", (void*) interface_selector_address)
if (ZYAN_FAILED(ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LEGACY_32, ZYDIS_ADDRESS_WIDTH_32))) {
LOG_ERROR("Failed to initialize zydis decoder")
return;
}
if (ZYAN_FAILED(ZydisFormatterInit(&formatter, ZYDIS_FORMATTER_STYLE_INTEL))) {
LOG_ERROR("Failed to initialize zydis formatter")
return;
}
Set<uintptr_t> visited_addresses;
process_interface_selector(interface_selector_address, visited_addresses);
}
}

View File

@@ -0,0 +1,39 @@
#pragma once
#include <core/types.hpp>
// IClientAppManager
VIRTUAL(bool) IClientAppManager_IsAppDlcInstalled(PARAMS(AppId_t, AppId_t));
// IClientApps
VIRTUAL(int) IClientApps_GetDLCCount(PARAMS(AppId_t));
VIRTUAL(bool) IClientApps_BGetDLCDataByIndex(PARAMS(AppId_t, int, AppId_t*, bool*, char*, int));
// IClientInventory
VIRTUAL(EResult) IClientInventory_GetResultStatus(PARAMS(SteamInventoryResult_t));
VIRTUAL(bool) IClientInventory_GetResultItems(
PARAMS(SteamInventoryResult_t, SteamItemDetails_t*, uint32_t, uint32_t *)
);
VIRTUAL(bool) IClientInventory_GetResultItemProperty(
PARAMS(SteamInventoryResult_t, uint32_t, const char*, char*, uint32_t, uint32_t*)
);
VIRTUAL(bool) IClientInventory_CheckResultSteamID(PARAMS(SteamInventoryResult_t, CSteamID));
VIRTUAL(bool) IClientInventory_GetAllItems(PARAMS(SteamInventoryResult_t*));
VIRTUAL(bool) IClientInventory_GetItemsByID(PARAMS(SteamInventoryResult_t*, const SteamItemInstanceID_t*, uint32_t));
VIRTUAL(bool) IClientInventory_SerializeResult(PARAMS(SteamInventoryResult_t, void*, uint32_t, uint32_t *));
VIRTUAL(bool) IClientInventory_GetItemDefinitionIDs(PARAMS(SteamItemDef_t*, uint32_t, uint32_t *));
// IClientUser
VIRTUAL(bool) IClientUser_BIsSubscribedApp(PARAMS(AppId_t));
// IClientUtils
VIRTUAL(AppId_t) IClientUtils_GetAppID(PARAMS());
namespace store::steamclient {
/// We need this interface in other IClient* functions in order to call other functions
extern Map<String, void*> interface_name_to_address_map;
void process_client_engine(uintptr_t interface);
}

137
src/store_mode/store.cpp Normal file
View File

@@ -0,0 +1,137 @@
#include <store_mode/store.hpp>
#include <store_mode/vstdlib/vstdlib.hpp>
#include <store_mode/store_cache.hpp>
#include <store_mode/store_api.hpp>
#include <smoke_api/config.hpp>
#include <build_config.h>
#include <koalabox/dll_monitor.hpp>
#include <koalabox/logger.hpp>
#include <koalabox/ipc.hpp>
#include <common/steamclient_exports.hpp>
namespace store {
StoreConfig config; // NOLINT(cert-err58-cpp)
/**
* @return A string representing the source of the config.
*/
void init_store_config() {
const auto print_source = [](const String& source) {
LOG_INFO("Loaded Store config from the {}", source)
};
// First try to read a local config override
const auto& kg_config = smoke_api::config::instance.store_config;
if (!kg_config.is_null()) {
try {
config = kg_config.get<decltype(config)>();
print_source("local config override");
return;
} catch (const Exception& ex) {
LOG_ERROR("Failed to get local store_mode config: {}", ex.what())
}
}
// Then try to get a cached copy of a previously fetched config.
try {
config = store_cache::get_store_config().value();
print_source("disk cache");
} catch (const Exception& ex) {
LOG_ERROR("Failed to get cached store_mode config: {}", ex.what())
print_source("default config bundled in the binary");
// Fallback on the default config, to continue execution immediately.
config = {};
}
// Finally, fetch the remote config from GitHub, and inform user about the need to restart Steam,
// if a new config has been fetched
NEW_THREAD({
try {
const auto github_config_opt = api::fetch_store_config();
if (!github_config_opt) {
return;
}
const auto github_config = *github_config_opt;
store_cache::save_store_config(github_config);
if (github_config == config) {
LOG_DEBUG("Fetched Store config is equal to existing config")
return;
}
LOG_DEBUG("Fetched a new Store config")
::MessageBox(
nullptr,
TEXT(
"SmokeAPI has downloaded an updated config for Store mode. "
"Please restart Steam in order to apply the new Store config. "
),
TEXT("SmokeAPI - Store"),
MB_SETFOREGROUND | MB_ICONINFORMATION | MB_OK
);
} catch (const Exception& ex) {
LOG_ERROR("Failed to get remote store_mode config: {}", ex.what())
}
})
}
void init_store_mode() {
init_store_config();
koalabox::dll_monitor::init_listener(
{VSTDLIB_DLL, STEAMCLIENT_DLL}, [](const HMODULE& module_handle, const String& name) {
try {
if (name < equals > VSTDLIB_DLL) {
// VStdLib DLL handles Family Sharing functions
globals::vstdlib_module = module_handle;
if (smoke_api::config::instance.unlock_family_sharing) {
DETOUR_VSTDLIB(Coroutine_Create)
}
} else if (name < equals > STEAMCLIENT_DLL) {
// SteamClient DLL handles unlocking functions
globals::steamclient_module = module_handle;
DETOUR_STEAMCLIENT(CreateInterface)
}
if (globals::vstdlib_module != nullptr && globals::steamclient_module != nullptr) {
koalabox::dll_monitor::shutdown_listener();
}
} catch (const Exception& ex) {
LOG_ERROR(
"Error listening to DLL load events. Module: '{}', Message: {}",
name, ex.what()
)
}
}
);
NEW_THREAD({
koalabox::ipc::init_pipe_server("smokeapi.store.steam", [](const koalabox::ipc::Request& request) {
koalabox::ipc::Response response;
if (request.name < equals > "config::reload") {
smoke_api::config::ReloadConfig();
response.success = true;
} else {
response.success = false;
response.data["error_message"] = "Invalid request name: " + request.name;
}
return response;
});
})
}
}

36
src/store_mode/store.hpp Normal file
View File

@@ -0,0 +1,36 @@
#pragma once
#include <core/types.hpp>
namespace store {
// Offset values are interpreted according to pointer arithmetic rules, i.e.
// 1 unit offset represents 4 and 8 bytes in 32-bit and 64-bit architectures respectively.
class StoreConfig {
public:
uint32_t client_engine_steam_client_internal_ordinal = 12;
uint32_t steam_client_internal_interface_selector_ordinal = 18;
uint32_t vstdlib_callback_address_offset = 20;
uint32_t vstdlib_callback_data_offset = 0;
uint32_t vstdlib_callback_interceptor_address_offset = 1;
uint32_t vstdlib_callback_name_offset = 4;
// We do not use *_WITH_DEFAULT macro to ensure that overriding
// the store_mode config requires definition of all keys
NLOHMANN_DEFINE_TYPE_INTRUSIVE(
StoreConfig, // NOLINT(misc-const-correctness)
client_engine_steam_client_internal_ordinal,
steam_client_internal_interface_selector_ordinal,
vstdlib_callback_address_offset,
vstdlib_callback_data_offset,
vstdlib_callback_interceptor_address_offset,
vstdlib_callback_name_offset
)
bool operator==(const StoreConfig& other) const = default;
};
extern StoreConfig config;
void init_store_mode();
}

View File

@@ -0,0 +1,19 @@
#include <store_mode/store_api.hpp>
#include <koalabox/http_client.hpp>
namespace store::api {
std::optional<StoreConfig> fetch_store_config() noexcept {
try {
const String url =
"https://raw.githubusercontent.com/acidicoala/public-entitlements/main/steam/v2/store_config.json";
const auto kg_config_json = koalabox::http_client::fetch_json(url);
return kg_config_json.get<StoreConfig>();
} catch (const Exception& e) {
LOG_ERROR("Failed to fetch Store config from GitHub: {}", e.what())
return std::nullopt;
}
}
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include <core/types.hpp>
#include <store_mode/store.hpp>
namespace store::api {
std::optional<store::StoreConfig> fetch_store_config() noexcept;
}

View File

@@ -0,0 +1,32 @@
#include <store_mode/store_cache.hpp>
#include <koalabox/cache.hpp>
constexpr auto KEY_KG_CONFIG = "store_config";
namespace store::store_cache {
std::optional<StoreConfig> get_store_config() {
try {
const auto config_json = koalabox::cache::read_from_cache(KEY_KG_CONFIG);
return config_json.get<StoreConfig>();
} catch (const Exception& e) {
LOG_ERROR("Failed to get cached store_mode config: {}", e.what())
return std::nullopt;
}
}
bool save_store_config(const StoreConfig& config) {
try {
LOG_DEBUG("Caching store_mode config")
return koalabox::cache::save_to_cache(KEY_KG_CONFIG, Json(config));
} catch (const Exception& e) {
LOG_ERROR("Failed to cache store_mode config: {}", e.what())
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include <store_mode/store.hpp>
namespace store::store_cache {
std::optional<StoreConfig> get_store_config();
bool save_store_config(const StoreConfig& config);
}

View File

@@ -0,0 +1,64 @@
#include <store_mode/vstdlib/vstdlib.hpp>
namespace store::vstdlib {
VIRTUAL(bool) SharedLicensesLockStatus(PARAMS(void* arg)) {
LOG_DEBUG("{}(this={}, arg={})", __func__, THIS, arg)
ARGS();
return true;
}
VIRTUAL(bool) SharedLibraryStopPlaying(PARAMS(void* arg)) {
LOG_DEBUG("{}(this={}, arg={})", __func__, THIS, arg)
ARGS();
return true;
}
VIRTUAL(void) VStdLib_Callback_Interceptor(PARAMS(const char** name_ptr)) {
GET_ORIGINAL_HOOKED_FUNCTION(VStdLib_Callback_Interceptor)
VStdLib_Callback_Interceptor_o(ARGS(name_ptr));
static auto lock_status_hooked = false;
static auto stop_playing_hooked = false;
if (lock_status_hooked && stop_playing_hooked) {
return;
}
auto* const data = (CoroutineData*) THIS;
if (data && data->get_callback_name()) {
const auto name = String(data->get_callback_name());
LOG_TRACE("{}(ecx={}, edx={}, name='{}')", __func__, ARGS(), name)
if (name == "SharedLicensesLockStatus" && !lock_status_hooked) {
DETOUR_ADDRESS(SharedLicensesLockStatus, data->get_callback_data()->get_callback_address())
lock_status_hooked = true;
} else if (name == "SharedLibraryStopPlaying" && !stop_playing_hooked) {
DETOUR_ADDRESS(SharedLibraryStopPlaying, data->get_callback_data()->get_callback_address())
stop_playing_hooked = true;
}
}
}
/**
* Initially, callback data passed into this function is not complete,
* hence we must hook an interface method that sets the callback name.
*/
DLL_EXPORT(HCoroutine) Coroutine_Create(void* callback_address, CoroutineData* data) {
GET_ORIGINAL_HOOKED_FUNCTION(Coroutine_Create)
const auto result = Coroutine_Create_o(callback_address, data);
// Coroutine callback appears to be always the same
CALL_ONCE({
LOG_DEBUG("Coroutine_Create -> callback: {}, data: {}", callback_address, fmt::ptr(data));
DETOUR_ADDRESS(
VStdLib_Callback_Interceptor,
data->get_callback_data()->get_callback_intercept_address()
)
})
return result;
}
}

View File

@@ -0,0 +1,27 @@
#include <store_mode/store.hpp>
namespace store::vstdlib {
struct CallbackData {
uintptr_t get_callback_intercept_address() {
return reinterpret_cast<uintptr_t*>(this)[store::config.vstdlib_callback_interceptor_address_offset];
}
uintptr_t get_callback_address() {
return reinterpret_cast<uintptr_t*>(this)[store::config.vstdlib_callback_address_offset];
}
};
struct CoroutineData {
CallbackData* get_callback_data() {
return reinterpret_cast<CallbackData**>(this)[store::config.vstdlib_callback_data_offset];
}
const char* get_callback_name() {
return reinterpret_cast<const char**>(this)[store::config.vstdlib_callback_name_offset];
}
};
typedef uint32_t HCoroutine;
DLL_EXPORT(HCoroutine) Coroutine_Create(void* callback_address, CoroutineData* data);
}