diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e34b26..1a3fc82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,6 @@ jobs: zip_command: > zip -j $ZIP_NAME artifacts/*/*.dll - res/SmokeAPI.json + res/SmokeAPI.config.json config: Release diff --git a/CMakeLists.txt b/CMakeLists.txt index 929888e..7ad5b30 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,10 +77,10 @@ set( if (CMAKE_SIZEOF_VOID_P EQUAL 4) set( SMOKE_API_SOURCES ${SMOKE_API_SOURCES} - src/koalageddon/kg_cache.hpp src/koalageddon/kg_cache.cpp - src/koalageddon/koalageddon.hpp + src/koalageddon/kg_cache.hpp src/koalageddon/koalageddon.cpp + src/koalageddon/koalageddon.hpp src/koalageddon/vstdlib.cpp src/koalageddon/vstdlib.hpp src/koalageddon/steamclient/client_app_manager.cpp diff --git a/KoalaBox b/KoalaBox index 17c3922..41649fd 160000 --- a/KoalaBox +++ b/KoalaBox @@ -1 +1 @@ -Subproject commit 17c39229db33c908e958e9c733cefdfc46204b17 +Subproject commit 41649fd55c6b720b4136ab983640312cbddfdd8c diff --git a/README.md b/README.md index 8713624..941e5eb 100644 --- a/README.md +++ b/README.md @@ -68,12 +68,12 @@ If the unlocker is not working as expected, then please fully read the [Generic ## ⚙ 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. +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 [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. 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` | +| `$version` | A technical field reserved for use by tools like GUI config editors. Do not modify this value. | Integer | `2` | +| `logging` | Toggles generation of `SmokeAPI.log.log` file | Boolean | `false` | | `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 | `[]` | | `extra_dlcs` | When a game requests number of all DLCs from Steam and gets a response that is equal to or greater than 64, it means that we need to get extra DLCs because Steam returns maximum 64 values this way. In this case, SmokeAPI will fetch extra DLCs from several online source, such as Steam API and a [manually maintained list of DLC IDs] from GitHub. However, in some cases these sources doesn't return all possible DLCs. To address this issue, you can specify the missing DLC IDs¹ in this option. | Object | `{}` | @@ -83,7 +83,7 @@ SmokeAPI does not require any manual configuration. By default, it uses the most ¹ 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.config.json +[SmokeAPI.config.json]: res/SmokeAPI.config.json [manually maintained list of DLC IDs]: https://github.com/acidicoala/public-entitlements/blob/main/steam/v1/dlc.json diff --git a/src/core/api.cpp b/src/core/api.cpp index 11bfc9e..5cdefcb 100644 --- a/src/core/api.cpp +++ b/src/core/api.cpp @@ -30,8 +30,6 @@ namespace api { const auto url = fmt::format("https://store.steampowered.com/dlc/{}/ajaxgetdlclist", app_id); const auto json = koalabox::http_client::fetch_json(url); - LOG_TRACE("Steam response: \n{}", json.dump(2)) - const auto response = json.get(); if (response.success != 1) { @@ -45,4 +43,17 @@ namespace api { } } + std::optional fetch_koalageddon_config() noexcept { + try { + const String url = + "https://raw.githubusercontent.com/acidicoala/public-entitlements/main/koalageddon/v2/steam.json"; + const auto kg_config_json = koalabox::http_client::fetch_json(url); + + return kg_config_json.get(); + } catch (const Exception& e) { + LOG_ERROR("Failed to fetch Koalageddon config from GitHub: {}", e.what()) + return std::nullopt; + } + } + } diff --git a/src/core/api.hpp b/src/core/api.hpp index ca7d229..4d65478 100644 --- a/src/core/api.hpp +++ b/src/core/api.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace api { @@ -8,4 +8,6 @@ namespace api { std::optional> fetch_dlcs_from_steam(AppId_t app_id) noexcept; + std::optional fetch_koalageddon_config() noexcept; + } diff --git a/src/koalageddon/kg_cache.cpp b/src/koalageddon/kg_cache.cpp index 88ad5b0..e5f0e70 100644 --- a/src/koalageddon/kg_cache.cpp +++ b/src/koalageddon/kg_cache.cpp @@ -8,9 +8,9 @@ namespace koalageddon::kg_cache { std::optional get_koalageddon_config() { try { - const auto cache = koalabox::cache::read_from_cache(KEY_KG_CONFIG); + const auto config_json = koalabox::cache::read_from_cache(KEY_KG_CONFIG); - return cache[KEY_KG_CONFIG].get(); + return config_json.get(); } catch (const Exception& e) { LOG_ERROR("Failed to get cached koalageddon config: {}", e.what()) diff --git a/src/koalageddon/koalageddon.cpp b/src/koalageddon/koalageddon.cpp index eeef08c..b19132d 100644 --- a/src/koalageddon/koalageddon.cpp +++ b/src/koalageddon/koalageddon.cpp @@ -4,9 +4,10 @@ #include #include #include +#include #include -#include #include +#include namespace koalageddon { @@ -15,54 +16,76 @@ namespace koalageddon { /** * @return A string representing the source of the config. */ - String init_koalageddon_config() { - if (!smoke_api::config::instance.koalageddon_config.is_null()) { - try { - // First try to read a local config override - config = smoke_api::config::instance.koalageddon_config.get(); + void init_koalageddon_config() { + const auto print_source = [](const String& source) { + LOG_INFO("Loaded Koalageddon config from the {}", source) + }; - return "local config override"; + // First try to read a local config override + const auto& kg_config = smoke_api::config::instance.koalageddon_config; + if (!kg_config.is_null()) { + try { + config = kg_config.get(); + + print_source("local config override"); + return; } catch (const Exception& ex) { LOG_ERROR("Failed to get local koalageddon config: {}", ex.what()) } } + // Then try to get a cached copy of a previously fetched config. try { - // Then try to fetch config from GitHub - const String url = "https://raw.githubusercontent.com/acidicoala/public-entitlements/main/koalageddon/v2/steam.json"; - config = koalabox::http_client::fetch_json(url).get(); - - kg_cache::save_koalageddon_config(config); - - return "GitHub repository"; - } catch (const Exception& ex) { - LOG_ERROR("Failed to get remote koalageddon config: {}", ex.what()) - } - - try { - // Then try to get a cached copy of a previously fetched config. - // We expect this unboxing to throw exception if no koalageddon config is present. config = kg_cache::get_koalageddon_config().value(); - return "disk cache"; + print_source("disk cache"); } catch (const Exception& ex) { LOG_ERROR("Failed to get cached koalageddon config: {}", ex.what()) + + print_source("default config bundled in the binary"); + + // Fallback on the default config, to continue execution immediately. + config = {}; } - // Finally, fallback on the default config - config = {}; - return "default config bundled in the binary"; + // 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_koalageddon_config(); + if (!github_config_opt) { + return; + } + + const auto github_config = *github_config_opt; + + kg_cache::save_koalageddon_config(github_config); + + if (github_config == config) { + LOG_DEBUG("Fetched Koalageddon config is equal to existing config") + + return; + } + + LOG_DEBUG("Fetched a new Koalageddon config") + + ::MessageBox( + nullptr, + TEXT( + "SmokeAPI has downloaded an updated config for Koalageddon mode. " + "Please restart Steam in order to apply the new Koalageddon config. " + ), + TEXT("SmokeAPI - Koalageddon"), + MB_SETFOREGROUND | MB_ICONINFORMATION | MB_OK + ); + } catch (const Exception& ex) { + LOG_ERROR("Failed to get remote koalageddon config: {}", ex.what()) + } + }) } void init() { - // TODO: Load cached koalageddon config by default - - std::thread( - []() { - const auto kg_config_source = init_koalageddon_config(); - LOG_INFO("Loaded Koalageddon config from the {}", kg_config_source) - } - ).detach(); + init_koalageddon_config(); koalabox::dll_monitor::init_listener( {VSTDLIB_DLL, STEAMCLIENT_DLL}, [](const HMODULE& module_handle, const String& name) { diff --git a/src/koalageddon/koalageddon.hpp b/src/koalageddon/koalageddon.hpp index 1a3cdf4..fa3aa61 100644 --- a/src/koalageddon/koalageddon.hpp +++ b/src/koalageddon/koalageddon.hpp @@ -5,7 +5,8 @@ namespace koalageddon { // 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. - struct KoalageddonConfig { + class KoalageddonConfig { + 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; @@ -24,6 +25,8 @@ namespace koalageddon { vstdlib_callback_interceptor_address_offset, vstdlib_callback_name_offset ) + + bool operator==(const KoalageddonConfig& other) const = default; }; extern KoalageddonConfig config; diff --git a/src/smoke_api/config.cpp b/src/smoke_api/config.cpp index ee824a2..df6e2ee 100644 --- a/src/smoke_api/config.cpp +++ b/src/smoke_api/config.cpp @@ -7,7 +7,6 @@ namespace smoke_api::config { Config instance; // NOLINT(cert-err58-cpp) - // TODO: Reloading via export void init() { const auto path = paths::get_config_path(); @@ -15,10 +14,9 @@ namespace smoke_api::config { try { const auto config_str = koalabox::io::read_file(path); - LOG_DEBUG("Parsing config:\n{}", config_str) - instance = Json::parse(config_str).get(); + LOG_DEBUG("Parsed config:\n{}", Json(instance)) } catch (const Exception& e) { const auto message = fmt::format("Error parsing config file: {}", e.what()); koalabox::util::error_box("SmokeAPI Error", message); diff --git a/src/smoke_api/config.hpp b/src/smoke_api/config.hpp index f12fe28..ab070ea 100644 --- a/src/smoke_api/config.hpp +++ b/src/smoke_api/config.hpp @@ -32,7 +32,7 @@ namespace smoke_api::config { // We have to use general json type here since the library doesn't support std::optional Json koalageddon_config; - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT( + NLOHMANN_DEFINE_TYPE_INTRUSIVE( Config, // NOLINT(misc-const-correctness) $version, logging, diff --git a/src/smoke_api/smoke_api.cpp b/src/smoke_api/smoke_api.cpp index 9e56528..919c5be 100644 --- a/src/smoke_api/smoke_api.cpp +++ b/src/smoke_api/smoke_api.cpp @@ -54,23 +54,24 @@ void init_hook_mode() { // the support for it has been dropped from this project. } -bool is_valve_steam(const String& exe_name) { - if (exe_name < not_equals > "steam.exe") { +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; } - - const HMODULE steam_handle = koalabox::win_util::get_module_handle_or_throw(nullptr); - const auto manifest = koalabox::win_util::get_module_manifest(steam_handle); - - // Verify that it's steam from valve, and not some other executable coincidentally named steam - - if (!manifest) { - // Steam.exe is expected to have a manifest - return false; - } - - // Steam.exe manifest is expected to contain this string - return *manifest < contains > "valvesoftware.steam.steam"; } namespace smoke_api { @@ -87,7 +88,8 @@ namespace smoke_api { koalabox::logger::init_file_logger(paths::get_log_path()); } - // FIXME: Dynamic timestamp resolution: https://stackoverflow.com/q/17212518 + // This kind of timestamp is reliable on 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__) koalabox::cache::init_cache(paths::get_cache_path());