13 Commits

Author SHA1 Message Date
acidicoala
3bd112c8f4 Refactored version check 2026-01-17 12:06:19 +05:00
acidicoala
7b54b4bc7b Added null checks 2026-01-14 19:34:00 +05:00
acidicoala
077096b7ed renew steamapi_handle 2026-01-12 04:10:43 +05:00
acidicoala
bc8ee85e19 Fixed proxy mode detection 2026-01-08 23:16:10 +05:00
acidicoala
5aaa9ed151 Added trace logs to proxy exports 2026-01-04 21:41:26 +05:00
acidicoala
e2b126c8b6 Regenerated README [skip ci] 2026-01-04 07:22:14 +05:00
acidicoala
2f6d6cc9aa Regenerated linux proxy exports 2026-01-04 07:12:44 +05:00
acidicoala
8784df5f45 Fix mbedtls include attempt 3 2026-01-04 06:54:06 +05:00
acidicoala
dc12301090 Fix mbedtls include attempt 2 2026-01-04 06:13:58 +05:00
acidicoala
907e939b67 Fixed string section name 2026-01-04 04:49:35 +05:00
acidicoala
7b82994b17 Regenerate linux proxy exports 2026-01-04 04:49:28 +05:00
acidicoala
11bd820921 Sync KoalaBox (brotli, openssl) 2026-01-04 04:49:19 +05:00
acidicoala
9acf7312d3 Fixed static order init fiasco 2026-01-02 21:57:41 +05:00
18 changed files with 5128 additions and 61 deletions

View File

@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="steamworks_downloader [prompt]" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="$Prompt$" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$ProjectFileDir$/res" PASS_PARENT_ENVS_2="true" PROJECT_NAME="SmokeAPI" TARGET_NAME="steamworks_downloader" CONFIG_NAME="Debug [32]" RUN_TARGET_PROJECT_NAME="SmokeAPI" RUN_TARGET_NAME="steamworks_downloader">
<configuration default="false" name="steamworks_downloader [prompt]" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="$Prompt$" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$ProjectFileDir$/res" PASS_PARENT_ENVS_2="true" PROJECT_NAME="SmokeAPI" TARGET_NAME="steamworks_downloader" CONFIG_NAME="Debug [64]" RUN_TARGET_PROJECT_NAME="SmokeAPI" RUN_TARGET_NAME="steamworks_downloader">
<method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>

View File

@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="sync" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$ProjectFileDir$" PASS_PARENT_ENVS_2="true" PROJECT_NAME="SmokeAPI" TARGET_NAME="sync" CONFIG_NAME="Debug [32]" RUN_TARGET_PROJECT_NAME="SmokeAPI" RUN_TARGET_NAME="sync">
<configuration default="false" name="sync" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$ProjectFileDir$" PASS_PARENT_ENVS_2="true" PROJECT_NAME="SmokeAPI" TARGET_NAME="sync" CONFIG_NAME="Debug [64]" RUN_TARGET_PROJECT_NAME="SmokeAPI" RUN_TARGET_NAME="sync">
<method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>

View File

@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.24)
project(SmokeAPI VERSION 4.1.0)
project(SmokeAPI VERSION 4.1.3)
include(KoalaBox/cmake/KoalaBox.cmake)
add_subdirectory(KoalaBox)

View File

@@ -145,14 +145,15 @@ In such cases, it might be worth trying [Special K], which can inject SmokeAPI a
### ✔️ Requirements
Linux builds of SmokeAPI depend on several libraries. Make sure they are installed on your system.
Linux builds of SmokeAPI depend on several libraries. These libraries are usually included in the
[Steam Linux Runtime 3.0 (sniper)](https://github.com/ValveSoftware/steam-runtime#readme),
so nothing is required to be installed on the system. But when launching a game directly via its executable
(such as in case of hook mode) your system needs to have the following required libraries installed.
The following list features links in Arch Linux repositories, but if you are using a different distribution,
you should use the equivalent package for your distro.
Required libraries:
- [brotli](https://archlinux.org/packages/core/x86_64/brotli/)
[[32-bit](https://archlinux.org/packages/multilib/x86_64/lib32-brotli/)]
- [gcc-libs](https://archlinux.org/packages/core/x86_64/gcc-libs/)
[[32-bit](https://archlinux.org/packages/core/x86_64/lib32-gcc-libs/)]
@@ -165,12 +166,6 @@ Required libraries:
- [libnghttp2](https://archlinux.org/packages/core/x86_64/libnghttp2/)
[[32-bit](https://archlinux.org/packages/multilib/x86_64/lib32-libnghttp2/)]
- [libssh2](https://archlinux.org/packages/core/x86_64/libssh2/)
[[32-bit](https://archlinux.org/packages/multilib/x86_64/lib32-libssh2/)]
- [openssl](https://archlinux.org/packages/core/x86_64/openssl/)
[[32-bit](https://archlinux.org/packages/multilib/x86_64/lib32-openssl/)]
- [zlib](https://archlinux.org/packages/core/x86_64/zlib/)
[[32-bit](https://archlinux.org/packages/multilib/x86_64/lib32-zlib/)]
@@ -194,7 +189,6 @@ wrappers might cause issues in theory. However, in practice real tests show that
chance of success compared to proxy mode. So, at the end of the day, try both modes to see which one works
best for you.
### 🔀 Proxy mode (🐧 Linux)
Same as on Windows:
@@ -214,17 +208,17 @@ For example:
1. Extract and paste the `libsmoke_api32.so` or `libsmoke_api64.so` in the root of game's installation directory.
2. In Steam _Library_ open game's _Properties_, switch to the _General_ tab, and set the following _LAUNCH OPTIONS_:
| Bitness | Launch Options |
|---------|------------------------------------------------------------------------------------------------------------------------|
| 32-bit | `LD_PRELOAD="./libsmoke_api32.so $HOME/.local/share/Steam/ubuntu12_32/gameoverlayrenderer.so" ./<GameExe32> %command%` |
| 64-bit | `LD_PRELOAD="./libsmoke_api64.so $HOME/.local/share/Steam/ubuntu12_64/gameoverlayrenderer.so" ./<GameExe64> %command%` |
| Bitness | Launch Options |
|---------|---------------------------------------------------------------------------------------------------------------------------------|
| 32-bit | `LD_PRELOAD="./libsmoke_api32.so $HOME/.local/share/Steam/ubuntu12_32/gameoverlayrenderer.so" ./<GameExe32> ; exit ; %command%` |
| 64-bit | `LD_PRELOAD="./libsmoke_api64.so $HOME/.local/share/Steam/ubuntu12_64/gameoverlayrenderer.so" ./<GameExe64> ; exit ; %command%` |
Where `<GameExe32>` and `<GameExe64>` correspond to the actual filename of the game executable. For example:
- `TheEscapists2.x86` (32-bit)
- `TheEscapists2.x86_64` (64-bit)
- `_linux/darkest.bin.x86` (32-bit)
- `_linux/darkest.bin.x86_64` (64-bit)
- `bin/linux_x64/eurotrucks2` (64-bit)
- `eurotrucks2` (64-bit)
- `binaries/victoria3` (64-bit)
And so on. Notice that Linux executables do not have `.exe` extension like on Windows, so make sure to copy the entire
@@ -234,6 +228,9 @@ If you have other environment variables, and you don't know how to correctly com
then please make extensive use of search engines and LLMs for guidance and examples
before seeking help the official forum topic.
> [!NOTE]
> The `; exit ; %command%` at the end of launch options
> is a trick used to force Steam to directly run the game executable.
## ⚙ Configuration
@@ -438,7 +435,8 @@ This project makes use of the following open source projects:
- [p-ranav/glob](https://github.com/p-ranav/glob)
- [pantor/inja](https://github.com/pantor/inja)
- [jarro2783/cxxopts](https://github.com/jarro2783/cxxopts)
- [serge1/ELFIO](https://github.com/serge1/ELFIO)
- [serge1/ELFIO](https://github.com/serge1/ELFIO)
- [Mbed-TLS/mbedtls](https://github.com/Mbed-TLS/mbedtls)
- [bshoshany/thread-pool](https://github.com/bshoshany/thread-pool)
- [batterycenter/embed](https://github.com/batterycenter/embed)

View File

@@ -20,8 +20,8 @@
"logging": {
"type": "boolean",
"default": false,
"x-packaged-default": true,
"description": "Enables logging to SmokeAPI.log.log file.",
"x-packaged-default": true,
"x-valid-values": "`true` or `false`."
},
"log_steam_http": {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@
#include <koalabox/config.hpp>
#include <koalabox/globals.hpp>
#include <koalabox/hook.hpp>
#include <koalabox/http_client.hpp>
#include <koalabox/lib.hpp>
#include <koalabox/lib_monitor.hpp>
#include <koalabox/logger.hpp>
@@ -25,7 +26,7 @@
#include "build_config.h"
#if defined(KB_WIN)
#ifdef KB_WIN
#include "koalabox/win.hpp"
#elif defined(KB_LINUX) && defined(KB_32)
#include "generated/32/proxy_exports.hpp"
@@ -57,6 +58,31 @@ namespace {
void* original_steamapi_handle = nullptr;
bool is_hook_mode;
void check_for_updates() {
try {
const auto latest_release_url = std::format(
"https://api.github.com/repos/acidicoala/{}/releases/latest",
PROJECT_NAME
);
const auto res = kb::http_client::get_json(latest_release_url);
const auto latest_tag = res["tag_name"].get<std::string>();
const auto current_tag = std::format("v{}", PROJECT_VERSION);
if(current_tag == latest_tag) {
LOG_DEBUG("Running the latest version");
} else {
const auto release_page = std::format(
"https://github.com/acidicoala/{}/releases/{}",
PROJECT_NAME, latest_tag
);
LOG_WARN("New version {} available: {}", latest_tag, release_page);
}
} catch(const std::exception& e) {
LOG_ERROR("{} -> Unexpected error: {}", __func__, e.what());
}
}
std::set<std::string> find_steamclient_versions(void* steamapi_handle) {
if(!steamapi_handle) {
kb::util::panic("Invalid state. steamapi_handle is null.");
@@ -64,11 +90,14 @@ namespace {
std::set<std::string> versions;
const auto rdata_section = kb::lib::get_section_or_throw(steamapi_handle, kb::lib::CONST_STR_SECTION);
const auto rdata = rdata_section.to_string();
// On Linux the section name depends on individual lib file, so we can't use generic constants
// ReSharper disable once CppDFAUnreachableCode
const std::string str_section_name = kb::platform::is_windows ? ".rdata" : ".rodata";
const auto str_section = kb::lib::get_section_or_throw(steamapi_handle, str_section_name);
const auto str_section_str = str_section.to_string();
const std::regex pattern(R"(SteamClient\d{3})");
const auto matches_begin = std::sregex_iterator(rdata.begin(), rdata.end(), pattern);
const auto matches_begin = std::sregex_iterator(str_section_str.begin(), str_section_str.end(), pattern);
const auto matches_end = std::sregex_iterator();
for(std::sregex_iterator i = matches_begin; i != matches_end; ++i) {
@@ -109,6 +138,15 @@ namespace {
static const auto CreateInterface$ = KB_LIB_GET_FUNC(steamclient_handle, CreateInterface);
if(auto* steamapi_handle = kb::lib::get_lib_handle(STEAM_API_MODULE)) {
if(original_steamapi_handle == nullptr) { // hook mode on Windows
original_steamapi_handle = steamapi_handle;
} else if(steamapi_handle != original_steamapi_handle) {
LOG_WARN(
"{} -> steamapi_handle ({}) != original_steamapi_handle ({})",
__func__, steamapi_handle, original_steamapi_handle
);
}
// SteamAPI might have been initialized.
// Hence, we need to query SteamClient interfaces and hook them if needed.
const auto steamclient_versions = find_steamclient_versions(steamapi_handle);
@@ -123,6 +161,8 @@ namespace {
LOG_INFO("'{}' has not been initialized. Waiting for initialization.", steamclient_version);
}
}
} else {
LOG_ERROR("{} -> steamapi_handle is null", __func__);
}
return true;
@@ -190,7 +230,10 @@ namespace {
if(const auto lib_bitness = kb::lib::get_bitness(lib_path)) {
if(static_cast<uint8_t>(*lib_bitness) == kb::platform::bitness) {
if(const auto lib_handle = kb::lib::load(lib_path)) {
LOG_INFO("Found original library: {}", kb::path::to_str(lib_path));
LOG_INFO(
"Found & loaded original library '{}' @ {}",
kb::path::to_str(lib_path), *lib_handle
);
original_steamapi_handle = *lib_handle;
proxy_exports::init(self_module_handle, original_steamapi_handle);
@@ -207,7 +250,7 @@ namespace {
}
void init_proxy_mode([[maybe_unused]] void* self_module_handle) {
is_hook_mode = true;
is_hook_mode = false;
original_steamapi_handle = kb::lib::load_original_library(kb::paths::get_self_dir(), STEAM_API_MODULE);
#ifdef KB_LINUX
@@ -229,16 +272,16 @@ namespace smoke_api {
kb::globals::init_globals(self_module_handle, PROJECT_NAME);
config::instance = kb::config::parse<config::Config>();
config::get() = kb::config::parse<config::Config>();
if(config::instance.logging) {
if(config::get().logging) {
kb::logger::init_file_logger(kb::paths::get_log_path());
} else {
kb::logger::init_null_logger();
}
LOG_INFO("{} v{}{} | Built at '{}'", PROJECT_NAME, PROJECT_VERSION, VERSION_SUFFIX, __TIMESTAMP__);
LOG_DEBUG("Parsed config:\n{}", nlohmann::ordered_json(config::instance).dump(2));
LOG_DEBUG("Parsed config:\n{}", nlohmann::ordered_json(config::get()).dump(2));
const auto exe_path = kb::lib::get_fs_path(nullptr);
const auto exe_name = kb::path::to_str(exe_path.filename());
@@ -270,6 +313,14 @@ namespace smoke_api {
}
}
void post_init() {
#ifdef KB_DEBUG
// TODO: Add config option to toggle this and show native OS notification
// The real reason behind this is for automatic testing of HTTPs dependencies
std::thread(check_for_updates).detach();
#endif
}
void shutdown() {
try {
static bool shutdown_complete = false;

View File

@@ -4,6 +4,14 @@
namespace smoke_api {
void init(void* self_module_handle);
/**
* Post-initialization procedures that must be done after the module is finished loading.
* Reason being that on Windows we should not start new threads while being in DllMain callback,
* otherwise we would run into deadlocks/race-conditions/undefined behavior.
*/
void post_init();
void shutdown();
AppId_t get_app_id();

View File

@@ -15,7 +15,9 @@ namespace steam_client {
if(interface_version) {
LOG_DEBUG("{} -> '{}' @ {}", function_name, interface_version, interface);
steam_interfaces::hook_virtuals(interface, interface_version);
if(interface) {
steam_interfaces::hook_virtuals(interface, interface_version);
}
}
return interface;

View File

@@ -237,13 +237,10 @@ namespace steam_interfaces {
continue;
}
const auto* const interface_ptr = ISteamClient_GetISteamGenericInterface(
ISteamClient_GetISteamGenericInterface(
ARGS(steam_user, steam_pipe, interface_version.c_str())
);
if(not interface_ptr) {
LOG_ERROR("Failed to get generic interface: '{}'", interface_version)
}
}
} catch(const std::exception& e) {
LOG_ERROR("{} -> Unhandled exception: {}", __func__, e.what());

View File

@@ -4,6 +4,7 @@
#include <koalabox/logger.hpp>
#include "smoke_api/steamclient/steamclient.hpp"
#include "smoke_api/smoke_api.hpp"
#include "smoke_api/types.hpp"
#include "steam_api/steam_client.hpp"
@@ -16,6 +17,9 @@ C_DECL(void*) CreateInterface(const char* interface_version, create_interface_re
static std::mutex section;
const std::lock_guard lock(section);
static std::once_flag once_flag;
std::call_once(once_flag, smoke_api::post_init);
return steam_client::GetGenericInterface(
__func__,
interface_version,

View File

@@ -1,14 +1,16 @@
#include <koalabox/config.hpp>
#include <koalabox/io.hpp>
#include <koalabox/logger.hpp>
#include "smoke_api/config.hpp"
namespace smoke_api::config {
Config instance; // NOLINT(cert-err58-cpp)
Config& get() noexcept {
static Config config;
return config;
}
std::vector<DLC> get_extra_dlcs(const uint32_t app_id) {
return DLC::get_dlcs_from_apps(instance.extra_dlcs, app_id);
return DLC::get_dlcs_from_apps(get().extra_dlcs, app_id);
}
bool is_dlc_unlocked(
@@ -16,16 +18,16 @@ namespace smoke_api::config {
const AppId_t dlc_id,
const std::function<bool()>& original_function
) {
auto status = instance.default_app_status;
auto status = get().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];
if(get().override_app_status.contains(app_id_str)) {
status = get().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];
if(get().override_dlc_status.contains(dlc_id_str)) {
status = get().override_dlc_status[dlc_id_str];
}
bool is_unlocked;

View File

@@ -43,7 +43,7 @@ namespace smoke_api::config {
)
};
extern Config instance;
Config& get() noexcept;
std::vector<DLC> get_extra_dlcs(AppId_t app_id);

View File

@@ -14,8 +14,15 @@ namespace {
/// This means we have to get extra DLC IDs from local config, remote config, or cache.
constexpr auto MAX_DLC = 64;
std::map<uint32_t, std::vector<DLC>> app_dlcs; // NOLINT(cert-err58-cpp)
std::set<uint32_t> fully_fetched; // NOLINT(cert-err58-cpp)
auto& get_fully_fetched_apps() {
static std::set<uint32_t> fully_fetched_apps;
return fully_fetched_apps;
}
auto& get_app_dlc_map() {
static std::map<uint32_t, std::vector<DLC>> app_dlc_map;
return app_dlc_map;
}
std::string get_app_id_log(const uint32_t app_id) {
return app_id ? std::format("App ID: {:>8}, ", app_id) : "";
@@ -31,13 +38,13 @@ namespace {
if(app_id == 0) {
LOG_ERROR("{} -> App ID is 0", __func__);
app_dlcs[app_id] = {}; // Dummy value to avoid checking for presence on each access
get_app_dlc_map()[app_id] = {}; // Dummy value to avoid checking for presence on each access
return;
}
// We want to fetch data only once. However, if any of the remote sources have failed
// previously, we want to attempt fetching again.
if(fully_fetched.contains(app_id)) {
if(get_fully_fetched_apps().contains(app_id)) {
return;
}
@@ -67,13 +74,13 @@ namespace {
}
if(github_dlcs_opt && steam_dlcs_opt) {
fully_fetched.insert(app_id);
get_fully_fetched_apps().insert(app_id);
} else {
append_dlcs(smoke_api::cache::get_dlcs(app_id), "disk cache");
}
// Cache DLCs in memory and cache for future use
app_dlcs[app_id] = aggregated_dlcs;
get_app_dlc_map()[app_id] = aggregated_dlcs;
smoke_api::cache::save_dlcs(app_id, aggregated_dlcs);
}
@@ -137,12 +144,12 @@ namespace smoke_api::steam_apps {
fetch_and_cache_dlcs(app_id);
if(app_dlcs.empty()) {
if(get_app_dlc_map().empty()) {
LOG_DEBUG("{} -> No cached DLCs, responding with original count", function_name);
return total_count(original_count);
}
return total_count(static_cast<int>(app_dlcs[app_id].size()));
return total_count(static_cast<int>(get_app_dlc_map()[app_id].size()));
} catch(const std::exception& e) {
LOG_ERROR("{} -> Uncaught exception: {}", function_name, e.what());
return 0;
@@ -188,8 +195,8 @@ namespace smoke_api::steam_apps {
pchName[bytes_to_copy] = '\0'; // Ensure null-termination
};
if(!app_dlcs.empty() && app_dlcs.contains(app_id)) {
const auto& dlcs = app_dlcs[app_id];
if(!get_app_dlc_map().empty() && get_app_dlc_map().contains(app_id)) {
const auto& dlcs = get_app_dlc_map()[app_id];
if(iDLC >= 0 && iDLC < dlcs.size()) {
output_dlc(dlcs[iDLC]);

View File

@@ -14,7 +14,7 @@ namespace smoke_api::steam_http {
try {
const auto result = original_function();
if(config::instance.log_steam_http) {
if(config::get().log_steam_http) {
const std::string_view buffer =
pBodyDataBuffer && unBufferSize
? std::string_view(
@@ -50,7 +50,7 @@ namespace smoke_api::steam_http {
try {
const auto result = original_function();
if(config::instance.log_steam_http) {
if(config::get().log_steam_http) {
const std::string_view buffer =
pBodyDataBuffer && unBufferSize
? std::string_view(
@@ -87,7 +87,7 @@ namespace smoke_api::steam_http {
try {
const auto result = original_function();
if(config::instance.log_steam_http) {
if(config::get().log_steam_http) {
const std::string_view content_type =
pchContentType ? pchContentType : "smoke_api::N/A";

View File

@@ -70,11 +70,11 @@ namespace smoke_api::steam_inventory {
);
static uint32_t original_count = 0;
const auto injected_count = config::instance.extra_inventory_items.size();
const auto injected_count = config::get().extra_inventory_items.size();
// Automatically get inventory items from steam
static std::vector<SteamItemDef_t> auto_inventory_items;
if(config::instance.auto_inject_inventory) {
if(config::get().auto_inject_inventory) {
static std::once_flag inventory_inject_flag;
std::call_once(
inventory_inject_flag,
@@ -126,7 +126,7 @@ namespace smoke_api::steam_inventory {
for(int i = 0; i < injected_count; i++) {
auto& item = pOutItemsArray[original_count + auto_injected_count + i];
const auto item_def_id = config::instance.extra_inventory_items[i];
const auto item_def_id = config::get().extra_inventory_items[i];
item = new_item(item_def_id);