mirror of
https://github.com/acidicoala/SmokeAPI.git
synced 2026-01-28 23:42:52 -05:00
Inventory logic re-write
This commit is contained in:
@@ -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)
|
||||
|
||||
/**
|
||||
@@ -55,10 +57,10 @@ namespace koalageddon {
|
||||
|
||||
void init() {
|
||||
std::thread(
|
||||
[]() {
|
||||
const auto kg_config_source = init_koalageddon_config();
|
||||
LOG_INFO("Loaded Koalageddon config from the {}", kg_config_source)
|
||||
}
|
||||
[]() {
|
||||
const auto kg_config_source = init_koalageddon_config();
|
||||
LOG_INFO("Loaded Koalageddon config from the {}", kg_config_source)
|
||||
}
|
||||
).detach();
|
||||
|
||||
koalabox::dll_monitor::init_listener(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -10,42 +10,41 @@ namespace koalageddon::vstdlib {
|
||||
|
||||
VIRTUAL(bool) SharedLicensesLockStatus(PARAMS(void* arg)
|
||||
) {
|
||||
LOG_DEBUG("{}(this={}, arg={})", __func__, THIS, arg)
|
||||
ARGS();
|
||||
return true;
|
||||
}
|
||||
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)
|
||||
|
||||
std::optional<Vector<DLC>> fetch_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_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");
|
||||
}
|
||||
|
||||
return response.dlcs;
|
||||
} catch (const Exception& e) {
|
||||
LOG_ERROR("Failed to fetch dlc list from Steam: {}", e.what())
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
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 {
|
||||
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;
|
||||
app_dlcs[app_id] = {}; // Dummy value to avoid checking for presence on each access
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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 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");
|
||||
}
|
||||
|
||||
for (const auto& dlc: response.dlcs) {
|
||||
dlcs.emplace_back(dlc.app_id);
|
||||
}
|
||||
} catch (const Exception& e) {
|
||||
LOG_ERROR("Failed to fetch dlc list from steam api: {}", e.what())
|
||||
total_success = false;
|
||||
}
|
||||
|
||||
return dlcs;
|
||||
};
|
||||
|
||||
const auto fetch_from_github = [&]() {
|
||||
Vector<AppId_t> dlcs;
|
||||
|
||||
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);
|
||||
}
|
||||
} 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,83 +180,56 @@ 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;
|
||||
};
|
||||
|
||||
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) {
|
||||
*pbAvailable = config::is_dlc_unlocked(app_id, *pDlcId, [&]() { return *pbAvailable; });
|
||||
print_dlc_info("original");
|
||||
} 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)
|
||||
return false;
|
||||
}
|
||||
|
||||
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 success = original_function();
|
||||
|
||||
if (success) {
|
||||
*pbAvailable = config::is_dlc_unlocked(app_id, *pDlcId, [&]() { return *pbAvailable; });
|
||||
print_dlc_info("original");
|
||||
} else {
|
||||
LOG_WARN("{} -> original call failed for index: {}", function_name, 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;
|
||||
return success;
|
||||
} 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