mirror of
https://github.com/acidicoala/SmokeAPI.git
synced 2025-12-05 21:15:39 -05:00
Inventory logic re-write
This commit is contained in:
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -14,5 +14,6 @@ jobs:
|
||||
zip_command: >
|
||||
zip -j $ZIP_NAME
|
||||
artifacts/*/*.dll
|
||||
res/SmokeAPI.json
|
||||
|
||||
config: Debug
|
||||
|
||||
2
.idea/codeStyles/Project.xml
generated
2
.idea/codeStyles/Project.xml
generated
@@ -10,8 +10,6 @@
|
||||
<Objective-C>
|
||||
<option name="KEEP_STRUCTURES_IN_ONE_LINE" value="true" />
|
||||
<option name="FUNCTION_CALL_ARGUMENTS_ALIGN_MULTILINE" value="false" />
|
||||
<option name="FUNCTION_CALL_ARGUMENTS_NEW_LINE_AFTER_LPAR" value="true" />
|
||||
<option name="FUNCTION_CALL_ARGUMENTS_NEW_LINE_BEFORE_RPAR" value="true" />
|
||||
<option name="SPACE_BEFORE_POINTER_IN_DECLARATION" value="false" />
|
||||
<option name="SPACE_AFTER_POINTER_IN_DECLARATION" value="true" />
|
||||
<option name="SPACE_BEFORE_REFERENCE_IN_DECLARATION" value="false" />
|
||||
|
||||
@@ -42,6 +42,7 @@ set(
|
||||
src/core/macros.hpp
|
||||
src/core/paths.cpp
|
||||
src/core/paths.hpp
|
||||
src/core/types.cpp
|
||||
src/core/types.hpp
|
||||
src/smoke_api/smoke_api.cpp
|
||||
src/smoke_api/smoke_api.hpp
|
||||
@@ -82,6 +83,7 @@ if (CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
src/koalageddon/types.hpp
|
||||
src/koalageddon/vstdlib.cpp
|
||||
src/koalageddon/vstdlib.hpp
|
||||
# TODO: Move to koalageddon package
|
||||
src/steamclient_virtuals/client_app_manager.cpp
|
||||
src/steamclient_virtuals/client_apps.cpp
|
||||
src/steamclient_virtuals/client_inventory.cpp
|
||||
|
||||
16
README.md
16
README.md
@@ -54,7 +54,7 @@ Try installing the unlocker in hook mode first. If it doesn't work, try installi
|
||||
|
||||
#### 🔀 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`.
|
||||
1. Find `steam_api.dll`/`steam_api64.dll` file in game directory, and add `_o` to its 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.
|
||||
|
||||
@@ -71,12 +71,12 @@ If the unlocker is not working as expected, then please fully read the [Generic
|
||||
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` |
|
||||
| `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_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 | `[]` |
|
||||
| `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 | `{}` |
|
||||
| `auto_inject_inventory` | Toggles whether SmokeAPI should automatically inject a list of all registered inventory items, when a game queries user inventory | Boolean | `true` |
|
||||
| `extra_inventory_items` | A list of inventory items IDs¹ that will be added in addition to the automatically injected items | List of Integers | `[]` |
|
||||
| `koalageddon_config` | An object that specifies patterns and offsets required for koalageddon mode. It can be used to override online config for testing or development purposes. | Object | `null` |
|
||||
@@ -100,6 +100,16 @@ Some games that have a lot of DLCs begin ownership verification by querying the
|
||||
- Describe how Koalageddon mode works and its config parameters
|
||||
- Describe the organisation of the project
|
||||
|
||||
## 🏗️ Building from source
|
||||
|
||||
### Requirements
|
||||
- Git
|
||||
- CMake v3.24
|
||||
- Visual Studio 2022
|
||||
- Tested on Windows 11 SDK (10.0.22621.0). Lower versions may be supported as well.
|
||||
|
||||
TODO: build.ps1
|
||||
|
||||
## 👋 Acknowledgements
|
||||
|
||||
SmokeAPI makes use of the following open source projects:
|
||||
|
||||
@@ -4,9 +4,29 @@
|
||||
"unlock_family_sharing": true,
|
||||
"default_app_status": "unlocked",
|
||||
"default_dlc_status": "unlocked",
|
||||
"override_app_status": {},
|
||||
"override_dlc_status": {},
|
||||
"extra_dlc_ids": [],
|
||||
"override_app_status": {
|
||||
"1234": "locked",
|
||||
"4321": "unlocked"
|
||||
},
|
||||
"override_dlc_status": {
|
||||
"56789": "locked",
|
||||
"98765": "unlocked",
|
||||
"16384": "original"
|
||||
},
|
||||
"extra_dlcs": {
|
||||
"extra_dlcs": {
|
||||
"1234": {
|
||||
"dlcs": {
|
||||
"56789": "Example DLC 1"
|
||||
}
|
||||
},
|
||||
"4321": {
|
||||
"dlcs": {
|
||||
"98765": "Example DLC 2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"auto_inject_inventory": true,
|
||||
"extra_inventory_items": [],
|
||||
"koalageddon_config": null
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include <core/config.hpp>
|
||||
#include <core/paths.hpp>
|
||||
#include <koalabox/json.hpp>
|
||||
#include <koalabox/util.hpp>
|
||||
#include <koalabox/io.hpp>
|
||||
|
||||
namespace config {
|
||||
Config instance; // NOLINT(cert-err58-cpp)
|
||||
@@ -12,16 +12,16 @@ namespace config {
|
||||
|
||||
if (exists(path)) {
|
||||
try {
|
||||
instance = Json(path).get<Config>();
|
||||
instance = Json::parse(koalabox::io::read_file(path)).get<Config>();
|
||||
} catch (const Exception& e) {
|
||||
koalabox::util::panic("Error parsing config: {}", e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppStatus get_app_status(uint32_t app_id) {
|
||||
AppStatus get_app_status(AppId_t app_id) {
|
||||
if (app_id == 0) {
|
||||
// 0 is a special internal value reserved for cases where we don't know app_id.
|
||||
// 0 is a special internal value reserved for cases where we don't know id.
|
||||
// This is typically the case in non-koalageddon modes, hence we treat it as unlocked.
|
||||
return AppStatus::UNLOCKED;
|
||||
}
|
||||
@@ -35,7 +35,7 @@ namespace config {
|
||||
return instance.default_app_status;
|
||||
}
|
||||
|
||||
DlcStatus get_dlc_status(uint32_t dlc_id) {
|
||||
DlcStatus get_dlc_status(AppId_t dlc_id) {
|
||||
const auto dlc_id_key = std::to_string(dlc_id);
|
||||
|
||||
if (instance.override_dlc_status.contains(dlc_id_key)) {
|
||||
@@ -45,7 +45,7 @@ namespace config {
|
||||
return instance.default_dlc_status;
|
||||
}
|
||||
|
||||
bool is_dlc_unlocked(uint32_t app_id, uint32_t dlc_id, const Function<bool()>& original_function) {
|
||||
bool is_dlc_unlocked(AppId_t app_id, AppId_t dlc_id, const Function<bool()>& original_function) {
|
||||
const auto app_status = config::get_app_status(app_id);
|
||||
const auto dlc_status = config::get_dlc_status(dlc_id);
|
||||
|
||||
@@ -56,4 +56,8 @@ namespace config {
|
||||
|
||||
return app_unlocked && dlc_unlocked;
|
||||
}
|
||||
|
||||
Vector<DLC> get_extra_dlcs(AppId_t app_id) {
|
||||
return DLC::get_dlcs_from_apps(instance.extra_dlcs, app_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <core/types.hpp>
|
||||
#include <koalabox/core.hpp>
|
||||
#include <koalabox/json.hpp>
|
||||
|
||||
// TODO: move to smoke_api namespace
|
||||
namespace config {
|
||||
enum class AppStatus {
|
||||
LOCKED,
|
||||
@@ -38,7 +39,7 @@ namespace config {
|
||||
DlcStatus default_dlc_status = DlcStatus::UNLOCKED;
|
||||
Map<String, AppStatus> override_app_status;
|
||||
Map<String, DlcStatus> override_dlc_status;
|
||||
Vector<uint32_t> extra_dlc_ids;
|
||||
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
|
||||
@@ -53,7 +54,7 @@ namespace config {
|
||||
default_dlc_status,
|
||||
override_app_status,
|
||||
override_dlc_status,
|
||||
extra_dlc_ids,
|
||||
extra_dlcs,
|
||||
auto_inject_inventory,
|
||||
extra_inventory_items,
|
||||
koalageddon_config
|
||||
@@ -69,4 +70,6 @@ namespace config {
|
||||
DlcStatus get_dlc_status(uint32_t dlc_id);
|
||||
|
||||
bool is_dlc_unlocked(uint32_t app_id, uint32_t dlc_id, const Function<bool()>& original_function);
|
||||
|
||||
Vector<DLC> get_extra_dlcs(AppId_t app_id);
|
||||
}
|
||||
|
||||
@@ -27,12 +27,12 @@
|
||||
* The macros below implement the above-mentioned considerations.
|
||||
*/
|
||||
#ifdef _WIN64
|
||||
#define PARAMS(...) void* RCX, ##__VA_ARGS__
|
||||
#define ARGS(...) RCX, ##__VA_ARGS__
|
||||
#define PARAMS(...) void* RCX, __VA_ARGS__
|
||||
#define ARGS(...) RCX, __VA_ARGS__
|
||||
#define THIS RCX
|
||||
#else
|
||||
#define PARAMS(...) void* ECX, void* EDX __VA_OPT__(,) __VA_ARGS__
|
||||
#define ARGS(...) ECX, EDX __VA_OPT__(,) __VA_ARGS__
|
||||
#define PARAMS(...) const void* ECX, const void* EDX, __VA_ARGS__
|
||||
#define ARGS(...) ECX, EDX, __VA_ARGS__
|
||||
#define THIS ECX
|
||||
#endif
|
||||
|
||||
|
||||
26
src/core/types.cpp
Normal file
26
src/core/types.cpp
Normal 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;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <koalabox/core.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#ifdef _WIN64
|
||||
#define COMPILE_KOALAGEDDON 0
|
||||
@@ -38,3 +39,46 @@ class ISteamApps;
|
||||
class ISteamUser;
|
||||
|
||||
class ISteamInventory;
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include <koalabox/logger.hpp>
|
||||
|
||||
namespace koalageddon {
|
||||
const void* client_app_manager_interface = nullptr;
|
||||
|
||||
KoalageddonConfig config; // NOLINT(cert-err58-cpp)
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
namespace koalageddon {
|
||||
|
||||
/// We need this interface in other IClient* functions in order to query original DLC status
|
||||
extern const void* client_app_manager_interface;
|
||||
|
||||
extern KoalageddonConfig config;
|
||||
|
||||
void init();
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace koalageddon::steamclient {
|
||||
}
|
||||
|
||||
SELECTOR_IMPLEMENTATION(IClientAppManager, {
|
||||
koalageddon::client_app_manager_interface = interface;
|
||||
HOOK_FUNCTION(IClientAppManager, IsAppDlcInstalled)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <koalabox/core.hpp>
|
||||
#include <koalabox/json.hpp>
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -13,39 +13,38 @@ namespace koalageddon::vstdlib {
|
||||
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(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));
|
||||
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;
|
||||
static auto lock_status_hooked = false;
|
||||
static auto stop_playing_hooked = false;
|
||||
|
||||
if (
|
||||
lock_status_hooked&& stop_playing_hooked
|
||||
) {
|
||||
return;
|
||||
}
|
||||
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)
|
||||
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;
|
||||
|
||||
@@ -3,21 +3,13 @@
|
||||
#include <koalabox/cache.hpp>
|
||||
#include <koalabox/logger.hpp>
|
||||
|
||||
struct App {
|
||||
Vector<AppId_t> dlc_ids;
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(App, dlc_ids) // NOLINT(misc-const-correctness)
|
||||
};
|
||||
|
||||
using Apps = Map<String, App>;
|
||||
|
||||
constexpr auto KEY_APPS = "apps";
|
||||
|
||||
Apps get_cached_apps() {
|
||||
AppDlcNameMap get_cached_apps() noexcept {
|
||||
try {
|
||||
const auto cache = koalabox::cache::read_from_cache(KEY_APPS);
|
||||
|
||||
return cache.get<Apps>();
|
||||
return cache.get<AppDlcNameMap>();
|
||||
} catch (const Exception& e) {
|
||||
LOG_WARN("Failed to get cached apps: {}", e.what())
|
||||
|
||||
@@ -27,31 +19,31 @@ Apps get_cached_apps() {
|
||||
|
||||
namespace smoke_api::app_cache {
|
||||
|
||||
Vector<AppId_t> get_dlc_ids(AppId_t app_id) {
|
||||
Vector<DLC> get_dlcs(AppId_t app_id) noexcept {
|
||||
try {
|
||||
LOG_DEBUG("Reading cached DLC IDs for the app: {}", app_id)
|
||||
|
||||
const auto app = get_cached_apps().at(std::to_string(app_id));
|
||||
const auto apps = get_cached_apps();
|
||||
|
||||
return app.dlc_ids;
|
||||
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_dlc_ids(AppId_t app_id, const Vector<AppId_t>& dlc_ids) {
|
||||
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)] = {
|
||||
.dlc_ids = dlc_ids
|
||||
};
|
||||
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("Failed to cache DLC IDs fro the app: {}", app_id)
|
||||
LOG_ERROR("Error saving DLCs to disk cache: {}", e.what())
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
namespace smoke_api::app_cache {
|
||||
|
||||
Vector<AppId_t> get_dlc_ids(AppId_t app_id);
|
||||
Vector<DLC> get_dlcs(AppId_t app_id) noexcept;
|
||||
|
||||
bool save_dlc_ids(AppId_t app_id, const Vector<AppId_t>& dlc_ids);
|
||||
bool save_dlcs(AppId_t app_id, const Vector<DLC>& dlcs) noexcept;
|
||||
|
||||
}
|
||||
|
||||
@@ -81,16 +81,17 @@ namespace smoke_api {
|
||||
|
||||
globals::smokeapi_handle = module_handle;
|
||||
|
||||
koalabox::cache::init_cache(paths::get_cache_path());
|
||||
|
||||
config::init();
|
||||
|
||||
if (config::instance.logging) {
|
||||
koalabox::logger::init_file_logger(paths::get_log_path());
|
||||
}
|
||||
|
||||
// FIXME: Dynamic timestamp resolution: https://stackoverflow.com/q/17212518
|
||||
LOG_INFO("🐨 {} v{} | Compiled at '{}'", PROJECT_NAME, PROJECT_VERSION, __TIMESTAMP__)
|
||||
|
||||
koalabox::cache::init_cache(paths::get_cache_path());
|
||||
|
||||
const auto exe_path = Path(koalabox::win_util::get_module_file_name_or_throw(nullptr));
|
||||
const auto exe_name = exe_path.filename().string();
|
||||
|
||||
|
||||
@@ -43,18 +43,22 @@ DLL_EXPORT(int) SteamAPI_ISteamApps_GetDLCCount(ISteamApps* self) {
|
||||
DLL_EXPORT(bool) SteamAPI_ISteamApps_BGetDLCDataByIndex(
|
||||
ISteamApps* self,
|
||||
int iDLC,
|
||||
AppId_t* pAppID,
|
||||
AppId_t* pDlcID,
|
||||
bool* pbAvailable,
|
||||
char* pchName,
|
||||
int cchNameBufferSize
|
||||
) {
|
||||
return steam_apps::GetDLCDataByIndex(
|
||||
__func__, 0, iDLC, pAppID, pbAvailable, pchName, cchNameBufferSize, [&]() {
|
||||
__func__, 0, iDLC, pDlcID, pbAvailable, pchName, cchNameBufferSize,
|
||||
[&]() {
|
||||
GET_ORIGINAL_FUNCTION_STEAMAPI(SteamAPI_ISteamApps_BGetDLCDataByIndex)
|
||||
|
||||
return SteamAPI_ISteamApps_BGetDLCDataByIndex_o(
|
||||
self, iDLC, pAppID, pbAvailable, pchName, cchNameBufferSize
|
||||
self, iDLC, pDlcID, pbAvailable, pchName, cchNameBufferSize
|
||||
);
|
||||
},
|
||||
[&](AppId_t dlc_id) {
|
||||
return SteamAPI_ISteamApps_BIsDlcInstalled(self, dlc_id);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,12 +41,16 @@ VIRTUAL(bool) ISteamApps_BGetDLCDataByIndex(
|
||||
)
|
||||
) {
|
||||
return steam_apps::GetDLCDataByIndex(
|
||||
__func__, 0, iDLC, pAppID, pbAvailable, pchName, cchNameBufferSize, [&]() {
|
||||
__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));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,121 +6,119 @@
|
||||
#include <koalabox/util.hpp>
|
||||
#include <steam_functions/steam_functions.hpp>
|
||||
#include <core/types.hpp>
|
||||
#include <utility>
|
||||
|
||||
namespace steam_apps {
|
||||
// TODO: Needs to go to API
|
||||
|
||||
class DLC {
|
||||
private:
|
||||
String appid;
|
||||
public:
|
||||
String name;
|
||||
uint32_t app_id = std::stoi(appid);
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE(DLC, appid, name)
|
||||
};
|
||||
|
||||
struct SteamResponse {
|
||||
uint32_t success = 0;
|
||||
Vector<DLC> dlcs;
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE(SteamResponse, success, dlcs)
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(SteamResponse, success, dlcs) // NOLINT(misc-const-correctness)
|
||||
};
|
||||
|
||||
using GitHubResponse = Map<String, Vector<uint32_t>>;
|
||||
|
||||
/// 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;
|
||||
|
||||
// Key: App ID, Value: DLC ID
|
||||
Map<AppId_t, int> original_dlc_count_map; // NOLINT(cert-err58-cpp)
|
||||
Vector<AppId_t> cached_dlcs;
|
||||
Map<AppId_t, Vector<DLC>> app_dlcs; // NOLINT(cert-err58-cpp)
|
||||
Set<AppId_t> fully_fetched; // NOLINT(cert-err58-cpp)
|
||||
|
||||
/**
|
||||
* @param app_id
|
||||
* @return boolean indicating if the function was able to successfully fetch DLC IDs from all sources.
|
||||
*/
|
||||
bool fetch_and_cache_dlcs(AppId_t app_id) {
|
||||
if (not app_id) {
|
||||
std::optional<Vector<DLC>> fetch_from_github(AppId_t app_id) noexcept {
|
||||
try {
|
||||
app_id = steam_functions::get_app_id_or_throw();
|
||||
// TODO: Check what it returns in koalageddon mode
|
||||
LOG_INFO("Detected App ID: {}", app_id)
|
||||
} catch (const Exception& ex) {
|
||||
LOG_ERROR("Failed to get app ID: {}", ex.what())
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto total_success = true;
|
||||
const auto app_id_str = std::to_string(app_id);
|
||||
|
||||
const auto fetch_from_steam = [&]() {
|
||||
Vector<AppId_t> dlcs;
|
||||
|
||||
try {
|
||||
// TODO: Refactor into api namespace
|
||||
const auto url = fmt::format("https://store.steampowered.com/dlc/{}/ajaxgetdlclist", app_id_str);
|
||||
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_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);
|
||||
|
||||
LOG_TRACE("Steam response: \n{}", json.dump(2))
|
||||
|
||||
const auto response = json.get<SteamResponse>();
|
||||
|
||||
if (response.success != 1) {
|
||||
throw std::runtime_error("Web API responded with 'success' != 1");
|
||||
}
|
||||
|
||||
for (const auto& dlc: response.dlcs) {
|
||||
dlcs.emplace_back(dlc.app_id);
|
||||
}
|
||||
return response.dlcs;
|
||||
} catch (const Exception& e) {
|
||||
LOG_ERROR("Failed to fetch dlc list from steam api: {}", e.what())
|
||||
total_success = false;
|
||||
LOG_ERROR("Failed to fetch dlc list from Steam: {}", e.what())
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
return dlcs;
|
||||
};
|
||||
|
||||
const auto fetch_from_github = [&]() {
|
||||
Vector<AppId_t> dlcs;
|
||||
/**
|
||||
* @param app_id
|
||||
* @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 (not app_id) {
|
||||
// No app id means we are operating in game mode.
|
||||
// Hence, we need to use utility functions to get app id.
|
||||
try {
|
||||
const String url = "https://raw.githubusercontent.com/acidicoala/public-entitlements/main/steam/v1/dlc.json";
|
||||
const auto json = koalabox::http_client::fetch_json(url);
|
||||
const auto response = json.get<GitHubResponse>();
|
||||
|
||||
if (response.contains(app_id_str)) {
|
||||
dlcs = response.at(app_id_str);
|
||||
app_id = steam_functions::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;
|
||||
}
|
||||
} catch (const Exception& e) {
|
||||
LOG_ERROR("Failed to fetch extra dlc list from github api: {}", e.what())
|
||||
total_success = false;
|
||||
}
|
||||
|
||||
return dlcs;
|
||||
};
|
||||
|
||||
const auto steam_dlcs = fetch_from_steam();
|
||||
const auto github_dlcs = fetch_from_github();
|
||||
// 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)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Any of the sources might fail, so we try to get optimal result
|
||||
// by combining results from all the sources into a single set.
|
||||
Set<AppId_t> combined_dlcs;
|
||||
combined_dlcs.insert(steam_dlcs.begin(), steam_dlcs.end());
|
||||
combined_dlcs.insert(github_dlcs.begin(), github_dlcs.end());
|
||||
// There is no need to insert cached entries if both steam and GitHub requests were successful.
|
||||
if (!total_success) {
|
||||
const auto cache_dlcs = smoke_api::app_cache::get_dlc_ids(app_id);
|
||||
combined_dlcs.insert(cached_dlcs.begin(), cached_dlcs.end());
|
||||
// by aggregating results from all the sources into a single set.
|
||||
Vector<DLC> aggregated_dlcs;
|
||||
|
||||
const auto append_dlcs = [&](const Vector<DLC>& source, const String& source_name) {
|
||||
LOG_DEBUG("App ID {} has {} DLCs defined in {}", app_id, source.size(), source_name)
|
||||
aggregated_dlcs < append > source;
|
||||
};
|
||||
|
||||
append_dlcs(config::get_extra_dlcs(app_id), "local config");
|
||||
|
||||
const auto github_dlcs = fetch_from_github(app_id);
|
||||
if (github_dlcs) {
|
||||
append_dlcs(*github_dlcs, "GitHub repository");
|
||||
}
|
||||
|
||||
// We then transfer that set into a list because we need DLCs to be accessible via index.
|
||||
cached_dlcs.clear();
|
||||
cached_dlcs.insert(cached_dlcs.begin(), combined_dlcs.begin(), combined_dlcs.end());
|
||||
const auto steam_dlcs = fetch_from_steam(app_id);
|
||||
if (steam_dlcs) {
|
||||
append_dlcs(*steam_dlcs, "Steam API");
|
||||
}
|
||||
|
||||
smoke_api::app_cache::save_dlc_ids(app_id, cached_dlcs);
|
||||
if (github_dlcs && steam_dlcs) {
|
||||
fully_fetched.insert(app_id);
|
||||
} else {
|
||||
append_dlcs(smoke_api::app_cache::get_dlcs(app_id), "disk cache");
|
||||
}
|
||||
|
||||
return total_success;
|
||||
// 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);
|
||||
}
|
||||
|
||||
String get_app_id_log(const AppId_t app_id) {
|
||||
@@ -157,35 +155,17 @@ namespace steam_apps {
|
||||
}
|
||||
|
||||
const auto original_count = original_function();
|
||||
original_dlc_count_map[app_id] = original_count;
|
||||
LOG_DEBUG("{} -> Original DLC count: {}", function_name, original_count)
|
||||
|
||||
if (original_count < MAX_DLC) {
|
||||
return total_count(original_count);
|
||||
}
|
||||
|
||||
// We need to fetch DLC IDs from all possible sources at this point
|
||||
LOG_DEBUG("Game has {} or more DLCs. Fetching DLCs from remote sources.", original_count)
|
||||
|
||||
const auto injected_count = static_cast<int>(config::instance.extra_dlc_ids.size());
|
||||
LOG_DEBUG("{} -> Injected DLC count: {}", function_name, injected_count)
|
||||
fetch_and_cache_dlcs(app_id);
|
||||
|
||||
// Maintain a list of app_ids for which we have already fetched and cached DLC IDs
|
||||
static Set<AppId_t> cached_apps;
|
||||
if (!cached_apps.contains(app_id)) {
|
||||
static std::mutex mutex;
|
||||
const std::lock_guard<std::mutex> guard(mutex);
|
||||
|
||||
LOG_DEBUG("Game has {} or more DLCs. Fetching DLCs from remote sources.", MAX_DLC)
|
||||
|
||||
if (fetch_and_cache_dlcs(app_id)) {
|
||||
cached_apps.insert(app_id);
|
||||
}
|
||||
}
|
||||
|
||||
const auto cached_count = static_cast<int>(cached_dlcs.size());
|
||||
LOG_DEBUG("{} -> Cached DLC count: {}", function_name, cached_count)
|
||||
|
||||
return total_count(injected_count + cached_count);
|
||||
return total_count(static_cast<int>(app_dlcs[app_id].size()));
|
||||
} catch (const Exception& e) {
|
||||
LOG_ERROR(" Uncaught exception: {}", function_name, e.what())
|
||||
return 0;
|
||||
@@ -200,45 +180,46 @@ namespace steam_apps {
|
||||
bool* pbAvailable,
|
||||
char* pchName,
|
||||
int cchNameBufferSize,
|
||||
const Function<bool()>& original_function
|
||||
const Function<bool()>& original_function,
|
||||
const Function<bool(AppId_t)>& is_originally_unlocked
|
||||
) {
|
||||
try {
|
||||
LOG_DEBUG("{} -> {}index: {:>3}", function_name, get_app_id_log(app_id), iDLC)
|
||||
|
||||
const auto print_dlc_info = [&](const String& tag) {
|
||||
LOG_INFO(
|
||||
"{} -> [{:12}] {}index: {:>3}, DLC ID: {:>8}, available: {:5}, name: '{}'",
|
||||
R"({} -> [{:^12}] {}index: {:>3}, DLC ID: {:>8}, available: {:5}, name: "{}")",
|
||||
function_name, tag, get_app_id_log(app_id), iDLC, *pDlcId, *pbAvailable, pchName
|
||||
)
|
||||
};
|
||||
|
||||
const auto inject_dlc = [&](const String& tag, const Vector<AppId_t>& dlc_ids, const int index) {
|
||||
const auto dlc_id = dlc_ids[index];
|
||||
|
||||
const auto inject_dlc = [&](const DLC& dlc) {
|
||||
// Fill the output pointers
|
||||
*pDlcId = dlc_id;
|
||||
*pbAvailable = config::is_dlc_unlocked(app_id, dlc_id, []() { return true; });
|
||||
*pDlcId = dlc.get_id();
|
||||
*pbAvailable = config::is_dlc_unlocked(
|
||||
app_id, *pDlcId, [&]() {
|
||||
return is_originally_unlocked(*pDlcId);
|
||||
}
|
||||
);
|
||||
|
||||
auto name = fmt::format("DLC #{} with ID: {} ", iDLC, dlc_id);
|
||||
name = name.substr(0, cchNameBufferSize);
|
||||
*name.rbegin() = '\0';
|
||||
auto name = dlc.get_name();
|
||||
name = name.substr(0, cchNameBufferSize + 1);
|
||||
memcpy_s(pchName, cchNameBufferSize, name.c_str(), name.size());
|
||||
|
||||
print_dlc_info(tag);
|
||||
return true;
|
||||
};
|
||||
|
||||
const auto get_original_dlc_count = [](const AppId_t& app_id) {
|
||||
if (original_dlc_count_map.contains(app_id)) {
|
||||
return original_dlc_count_map[app_id];
|
||||
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;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
LOG_WARN("{} -> Out of bounds DLC index: {}", function_name, iDLC)
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto original_count = get_original_dlc_count(app_id);
|
||||
|
||||
// Original count less than MAX_DLC implies that we need to redirect the call to original function.
|
||||
|
||||
if (original_count < MAX_DLC) {
|
||||
const auto success = original_function();
|
||||
|
||||
if (success) {
|
||||
@@ -247,36 +228,8 @@ namespace steam_apps {
|
||||
} else {
|
||||
LOG_WARN("{} -> original call failed for index: {}", function_name, iDLC)
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// We must have had cached DLC IDs at this point.
|
||||
// It does not matter if we begin the list with injected DLC IDs or cached ones.
|
||||
// However, we must be consistent at all times. Hence, the convention will be that
|
||||
// injected DLCs will be followed by cached DLCs in the following manner:
|
||||
// [injected-dlc-0, injected-dlc-1, ..., cached-dlc-0, cached-dlc-1, ...]
|
||||
|
||||
if (iDLC < 0) {
|
||||
LOG_WARN("{} -> Out of bounds DLC index: {}", function_name, iDLC)
|
||||
}
|
||||
|
||||
const int local_dlc_count = static_cast<int>(config::instance.extra_dlc_ids.size());
|
||||
if (iDLC < local_dlc_count) {
|
||||
return inject_dlc("local config", config::instance.extra_dlc_ids, iDLC);
|
||||
}
|
||||
|
||||
const auto adjusted_index = iDLC - local_dlc_count;
|
||||
const int cached_dlc_count = static_cast<int>(cached_dlcs.size());
|
||||
if (iDLC < cached_dlc_count) {
|
||||
return inject_dlc("memory cache", cached_dlcs, adjusted_index);
|
||||
}
|
||||
|
||||
LOG_ERROR(
|
||||
"{} -> Out of bounds DLC index: {}, local dlc count: {}, cached dlc count: {}",
|
||||
function_name, iDLC, local_dlc_count, cached_dlc_count
|
||||
)
|
||||
|
||||
return false;
|
||||
} catch (const Exception& e) {
|
||||
LOG_ERROR("{} -> Uncaught exception: {}", function_name, e.what())
|
||||
return false;
|
||||
|
||||
@@ -26,7 +26,8 @@ namespace steam_apps {
|
||||
bool* pbAvailable,
|
||||
char* pchName,
|
||||
int cchNameBufferSize,
|
||||
const Function<bool()>& original_function
|
||||
const Function<bool()>& original_function,
|
||||
const Function<bool(AppId_t)>& is_originally_unlocked // Aux function to resolve original dlc status
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace steam_inventory {
|
||||
return status;
|
||||
}
|
||||
|
||||
// TODO: investigate if we can get app id in koalageddon mode
|
||||
bool GetResultItems(
|
||||
const String& function_name,
|
||||
const SteamInventoryResult_t resultHandle,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <steam_impl/steam_apps.hpp>
|
||||
#include <core/macros.hpp>
|
||||
#include <core/types.hpp>
|
||||
#include <steam_impl/steam_apps.hpp>
|
||||
#include <steam_functions/steam_functions.hpp>
|
||||
|
||||
VIRTUAL(bool) IClientAppManager_IsAppDlcInstalled(PARAMS(AppId_t app_id, AppId_t dlc_id)) {
|
||||
return steam_apps::IsDlcUnlocked(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <core/macros.hpp>
|
||||
#include <core/types.hpp>
|
||||
#include <steam_impl/steam_apps.hpp>
|
||||
#include <koalageddon/koalageddon.hpp>
|
||||
#include <steam_functions/steam_functions.hpp>
|
||||
|
||||
VIRTUAL(int) IClientApps_GetDLCCount(PARAMS(AppId_t appId)) {
|
||||
return steam_apps::GetDLCCount(
|
||||
@@ -23,12 +25,22 @@ VIRTUAL(bool) IClientApps_BGetDLCDataByIndex(
|
||||
)
|
||||
) {
|
||||
return steam_apps::GetDLCDataByIndex(
|
||||
__func__, appID, iDLC, pDlcID, pbAvailable, pchName, cchNameBufferSize, [&]() {
|
||||
__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) {
|
||||
if (koalageddon::client_app_manager_interface) {
|
||||
IClientAppManager_IsAppDlcInstalled(koalageddon::client_app_manager_interface, EDX, appID, dlc_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Would never happen in practice, as the interfaces would be instantiated almost simultaneously
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user