mirror of
https://github.com/acidicoala/SmokeAPI.git
synced 2024-11-04 12:00:24 +00:00
Refactored DLC unlocking logic
This commit is contained in:
parent
b04c96a36d
commit
011f3fac5d
@ -37,6 +37,8 @@ set(
|
||||
SMOKE_API_SOURCES
|
||||
src/core/cache.cpp
|
||||
src/core/cache.hpp
|
||||
src/core/config.cpp
|
||||
src/core/config.hpp
|
||||
src/core/globals.cpp
|
||||
src/core/globals.hpp
|
||||
src/core/macros.hpp
|
||||
|
2
KoalaBox
2
KoalaBox
@ -1 +1 @@
|
||||
Subproject commit 418cb437a7eda9c7e13cdcdbe6e538fa9e4a9281
|
||||
Subproject commit 53731ddc6b82281df820884f2d2424ee3a62df54
|
@ -76,9 +76,9 @@ SmokeAPI does not require any manual configuration. By default, it uses the most
|
||||
| `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 | `[]` |
|
||||
| `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_dlc_ids` | When game requests list of all DLCs from Steam and the number of registered DLCs is greater than 64, Steam may not return all of them. In this case, SmokeAPI will fetch all released DLCs from Web API. In some games, however (like Monster Hunter: World), web api also doesn't return all possible DLCs. To address this issue, you can specify the missing DLC IDs¹ in this option. For some games (including MH:W), however, it is not necessary because SmokeAPI will also automatically fetch a [manually maintained list of DLC IDs] that are missing from web api | List of Integers | `[]` |
|
||||
| `auto_inject_inventory` | Toggles whether SmokeAPI should automatically inject a list of all registered inventory items, when a game queries user inventory | Boolean | `true` |
|
||||
| `inventory_items` | A list of inventory items IDs¹ that will be added in addition to the automatically injected items | List of Integers | `[]` |
|
||||
| `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` |
|
||||
|
||||
¹ 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.
|
||||
@ -91,7 +91,7 @@ SmokeAPI does not require any manual configuration. By default, it uses the most
|
||||
|
||||
### How SmokeAPI works in games with large number of DLCs
|
||||
|
||||
Some games that have a lot of DLCs begin ownership verification by querying the Steamworks API for a list of all available DLCs. Once the game receives the list, it will go over each item and check the ownership. The issue arises from the fact that response from Steamworks SDK may max out at 64, depending on how much unowned DLC the user has. To alleviate this issue, SmokeAPI will make a web request to Steam API for a full list of DLCs, which works well most of the time. Unfortunately, even the web API does not solve all of our problems, because it will only return DLCs that are available in Steam store. This means that DLCs without a dedicated store offer, such as pre-order DLCs will be left out. That's where the `dlc_ids` config option comes into play. You can specify those missing DLC IDs there, and SmokeAPI will make them available to the game. However, this introduces the need for manual configuration, which goes against the ideals of this project. To remedy this issue SmokeAPI will also fetch [this document] stored in a GitHub repository. It contains all the DLC IDs missing from Steam store. The document is hand-crafted using data from https://steamdb.com. This enables SmokeAPI to unlock all DLCs without any config file at all. Feel free to report games that have more than 64 DLCs,
|
||||
Some games that have a lot of DLCs begin ownership verification by querying the Steamworks API for a list of all available DLCs. Once the game receives the list, it will go over each item and check the ownership. The issue arises from the fact that response from Steamworks SDK may max out at 64, depending on how much unowned DLC the user has. To alleviate this issue, SmokeAPI will make a web request to Steam API for a full list of DLCs, which works well most of the time. Unfortunately, even the web API does not solve all of our problems, because it will only return DLCs that are available in Steam store. This means that DLCs without a dedicated store offer, such as pre-order DLCs will be left out. That's where the `extra_dlc_ids` config option comes into play. You can specify those missing DLC IDs there, and SmokeAPI will make them available to the game. However, this introduces the need for manual configuration, which goes against the ideals of this project. To remedy this issue SmokeAPI will also fetch [this document] stored in a GitHub repository. It contains all the DLC IDs missing from Steam store. The document is hand-crafted using data from https://steamdb.com. This enables SmokeAPI to unlock all DLCs without any config file at all. Feel free to report games that have more than 64 DLCs,
|
||||
*and* have DLCs without a dedicated store page. They will be added to the list of missing DLC IDs to facilitate configless operation.
|
||||
|
||||
[this document]: https://github.com/acidicoala/public-entitlements/blob/main/steam/v1/dlc.json
|
||||
|
@ -2,10 +2,12 @@
|
||||
"$version": 2,
|
||||
"logging": true,
|
||||
"unlock_family_sharing": true,
|
||||
"unlock_all": true,
|
||||
"override": [],
|
||||
"dlc_ids": [],
|
||||
"default_app_status": "unlocked",
|
||||
"default_dlc_status": "unlocked",
|
||||
"override_app_status": {},
|
||||
"override_dlc_status": {},
|
||||
"extra_dlc_ids": [],
|
||||
"auto_inject_inventory": true,
|
||||
"inventory_items": [],
|
||||
"extra_inventory_items": [],
|
||||
"koalageddon_config": null
|
||||
}
|
||||
|
49
src/core/config.cpp
Normal file
49
src/core/config.cpp
Normal file
@ -0,0 +1,49 @@
|
||||
#include <core/config.hpp>
|
||||
#include <core/paths.hpp>
|
||||
#include <koalabox/config_parser.hpp>
|
||||
|
||||
namespace config {
|
||||
Config instance; // NOLINT(cert-err58-cpp)
|
||||
|
||||
void init() {
|
||||
instance = config_parser::parse<Config>(paths::get_config_path());
|
||||
}
|
||||
|
||||
AppStatus get_app_status(uint32_t app_id) {
|
||||
if (app_id == 0) {
|
||||
// 0 is a special internal value reserved for cases where we don't know app_id.
|
||||
// This is typically the case in non-koalageddon modes, hence we treat it as unlocked.
|
||||
return AppStatus::UNLOCKED;
|
||||
}
|
||||
|
||||
const auto app_id_key = std::to_string(app_id);
|
||||
|
||||
if (instance.override_app_status.contains(app_id_key)) {
|
||||
return instance.override_app_status[app_id_key];
|
||||
}
|
||||
|
||||
return instance.default_app_status;
|
||||
}
|
||||
|
||||
DlcStatus get_dlc_status(uint32_t dlc_id) {
|
||||
const auto dlc_id_key = std::to_string(dlc_id);
|
||||
|
||||
if (instance.override_dlc_status.contains(dlc_id_key)) {
|
||||
return instance.override_dlc_status[dlc_id_key];
|
||||
}
|
||||
|
||||
return instance.default_dlc_status;
|
||||
}
|
||||
|
||||
bool is_dlc_unlocked(uint32_t app_id, uint32_t dlc_id, const std::function<bool()>& original_function) {
|
||||
const auto app_status = config::get_app_status(app_id);
|
||||
const auto dlc_status = config::get_dlc_status(dlc_id);
|
||||
|
||||
const auto app_unlocked = app_status == config::AppStatus::UNLOCKED;
|
||||
const auto dlc_unlocked = dlc_status == config::DlcStatus::UNLOCKED ||
|
||||
dlc_status != config::DlcStatus::LOCKED &&
|
||||
original_function();
|
||||
|
||||
return app_unlocked && dlc_unlocked;
|
||||
}
|
||||
}
|
71
src/core/config.hpp
Normal file
71
src/core/config.hpp
Normal file
@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <koalabox/koalabox.hpp>
|
||||
|
||||
namespace config {
|
||||
using namespace koalabox;
|
||||
|
||||
enum class AppStatus {
|
||||
LOCKED,
|
||||
UNLOCKED,
|
||||
UNDEFINED
|
||||
};
|
||||
|
||||
NLOHMANN_JSON_SERIALIZE_ENUM(AppStatus, {
|
||||
{ AppStatus::UNDEFINED, nullptr },
|
||||
{ AppStatus::LOCKED, "locked" },
|
||||
{ AppStatus::UNLOCKED, "unlocked" },
|
||||
})
|
||||
|
||||
enum class DlcStatus {
|
||||
LOCKED,
|
||||
UNLOCKED,
|
||||
ORIGINAL,
|
||||
UNDEFINED
|
||||
};
|
||||
|
||||
NLOHMANN_JSON_SERIALIZE_ENUM(DlcStatus, {
|
||||
{ DlcStatus::UNDEFINED, nullptr },
|
||||
{ DlcStatus::LOCKED, "locked" },
|
||||
{ DlcStatus::UNLOCKED, "unlocked" },
|
||||
{ DlcStatus::ORIGINAL, "original" },
|
||||
})
|
||||
|
||||
struct Config {
|
||||
uint32_t $version = 2;
|
||||
bool logging = false;
|
||||
bool unlock_family_sharing = true;
|
||||
AppStatus default_app_status = AppStatus::UNLOCKED;
|
||||
DlcStatus default_dlc_status = DlcStatus::UNLOCKED;
|
||||
Map<String, AppStatus> override_app_status;
|
||||
Map<String, DlcStatus> override_dlc_status;
|
||||
Vector<uint32_t> extra_dlc_ids;
|
||||
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
|
||||
nlohmann::json koalageddon_config;
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(
|
||||
Config, $version, // NOLINT(misc-const-correctness)
|
||||
logging,
|
||||
unlock_family_sharing,
|
||||
default_app_status,
|
||||
default_dlc_status,
|
||||
override_app_status,
|
||||
override_dlc_status,
|
||||
extra_dlc_ids,
|
||||
auto_inject_inventory,
|
||||
extra_inventory_items,
|
||||
koalageddon_config
|
||||
)
|
||||
};
|
||||
|
||||
extern Config instance;
|
||||
|
||||
void init();
|
||||
|
||||
AppStatus get_app_status(uint32_t app_id);
|
||||
DlcStatus get_dlc_status(uint32_t dlc_id);
|
||||
bool is_dlc_unlocked(uint32_t app_id, uint32_t dlc_id, const std::function<bool()>& original_function);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
#include <build_config.h>
|
||||
#include <koalageddon/koalageddon.hpp>
|
||||
#include <build_config.h>
|
||||
#include <core/cache.hpp>
|
||||
#include <core/config.hpp>
|
||||
#include <smoke_api/smoke_api.hpp>
|
||||
#include <koalabox/dll_monitor.hpp>
|
||||
#include <koalabox/http_client.hpp>
|
||||
@ -12,10 +13,10 @@ namespace koalageddon {
|
||||
* @return A string representing the source of the config.
|
||||
*/
|
||||
String init_koalageddon_config() {
|
||||
if (!smoke_api::config.koalageddon_config.is_null()) {
|
||||
if (!config::instance.koalageddon_config.is_null()) {
|
||||
try {
|
||||
// First try to read a local config override
|
||||
config = smoke_api::config.koalageddon_config.get<decltype(config)>();
|
||||
config = config::instance.koalageddon_config.get<decltype(config)>();
|
||||
|
||||
return "local config override";
|
||||
} catch (const Exception& ex) {
|
||||
@ -63,7 +64,7 @@ namespace koalageddon {
|
||||
static auto init_count = 0;
|
||||
if (util::strings_are_equal(name, VSTDLIB_DLL)) {
|
||||
// VStdLib DLL handles Family Sharing functions
|
||||
if (smoke_api::config.unlock_family_sharing) {
|
||||
if (config::instance.unlock_family_sharing) {
|
||||
init_vstdlib_hooks();
|
||||
}
|
||||
init_count++;
|
||||
|
@ -1,23 +1,21 @@
|
||||
#include <smoke_api/smoke_api.hpp>
|
||||
#include <build_config.h>
|
||||
#include <core/config.hpp>
|
||||
#include <core/globals.hpp>
|
||||
#include <core/paths.hpp>
|
||||
#include <steam_functions/steam_functions.hpp>
|
||||
#include <build_config.h>
|
||||
|
||||
#include <koalabox/config_parser.hpp>
|
||||
#include <koalabox/dll_monitor.hpp>
|
||||
#include <koalabox/file_logger.hpp>
|
||||
#include <koalabox/hook.hpp>
|
||||
#include <koalabox/loader.hpp>
|
||||
#include <koalabox/win_util.hpp>
|
||||
#include <core/globals.hpp>
|
||||
|
||||
#ifndef _WIN64
|
||||
#include <koalageddon/koalageddon.hpp>
|
||||
#endif
|
||||
|
||||
namespace smoke_api {
|
||||
Config config = {}; // NOLINT(cert-err58-cpp)
|
||||
|
||||
HMODULE original_library = nullptr;
|
||||
|
||||
HMODULE self_module = nullptr;
|
||||
@ -85,13 +83,13 @@ namespace smoke_api {
|
||||
|
||||
koalabox::project_name = PROJECT_NAME;
|
||||
|
||||
config = config_parser::parse<Config>(paths::get_config_path());
|
||||
config::init();
|
||||
|
||||
const auto exe_path = Path(win_util::get_module_file_name_or_throw(nullptr));
|
||||
const auto exe_name = exe_path.filename().string();
|
||||
const auto exe_bitness = util::is_x64() ? 64 : 32;
|
||||
|
||||
if (config.logging) {
|
||||
if (config::instance.logging) {
|
||||
logger = file_logger::create(paths::get_log_path());
|
||||
}
|
||||
|
||||
@ -138,10 +136,4 @@ namespace smoke_api {
|
||||
logger->error("Shutdown error: {}", ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Support for app_id for koalageddon mode
|
||||
bool should_unlock(uint32_t app_id) {
|
||||
return config.unlock_all != config.override.contains(app_id);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,34 +24,6 @@
|
||||
namespace smoke_api {
|
||||
using namespace koalabox;
|
||||
|
||||
struct Config {
|
||||
uint32_t $version = 2;
|
||||
bool logging = false;
|
||||
bool unlock_family_sharing = true;
|
||||
bool unlock_all = true;
|
||||
Set<uint32_t> override;
|
||||
Vector<uint32_t> dlc_ids;
|
||||
bool auto_inject_inventory = true;
|
||||
Vector<uint32_t> inventory_items;
|
||||
|
||||
// Have to use general json type here since library doesn't support std::optional
|
||||
nlohmann::json koalageddon_config;
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(
|
||||
Config, $version, // NOLINT(misc-const-correctness)
|
||||
logging,
|
||||
unlock_family_sharing,
|
||||
unlock_all,
|
||||
override,
|
||||
dlc_ids,
|
||||
auto_inject_inventory,
|
||||
inventory_items,
|
||||
koalageddon_config
|
||||
)
|
||||
};
|
||||
|
||||
extern Config config;
|
||||
|
||||
extern HMODULE self_module;
|
||||
|
||||
extern HMODULE original_library;
|
||||
@ -62,6 +34,4 @@ namespace smoke_api {
|
||||
|
||||
void shutdown();
|
||||
|
||||
bool should_unlock(uint32_t app_id);
|
||||
|
||||
}
|
||||
|
@ -8,12 +8,20 @@ using namespace smoke_api;
|
||||
|
||||
// ISteamApps
|
||||
|
||||
DLL_EXPORT(bool) SteamAPI_ISteamApps_BIsSubscribedApp(ISteamApps*, AppId_t appID) {
|
||||
return steam_apps::IsDlcUnlocked(__func__, 0, appID);
|
||||
DLL_EXPORT(bool) SteamAPI_ISteamApps_BIsSubscribedApp(ISteamApps* self, AppId_t appID) {
|
||||
return steam_apps::IsDlcUnlocked(__func__, 0, appID, [&]() {
|
||||
GET_ORIGINAL_FUNCTION(SteamAPI_ISteamApps_BIsSubscribedApp)
|
||||
|
||||
return SteamAPI_ISteamApps_BIsSubscribedApp_o(self, appID);
|
||||
});
|
||||
}
|
||||
|
||||
DLL_EXPORT(bool) SteamAPI_ISteamApps_BIsDlcInstalled(ISteamApps*, AppId_t appID) {
|
||||
return steam_apps::IsDlcUnlocked(__func__, 0, appID);
|
||||
DLL_EXPORT(bool) SteamAPI_ISteamApps_BIsDlcInstalled(ISteamApps* self, AppId_t appID) {
|
||||
return steam_apps::IsDlcUnlocked(__func__, 0, appID, [&]() {
|
||||
GET_ORIGINAL_FUNCTION(SteamAPI_ISteamApps_BIsDlcInstalled)
|
||||
|
||||
return SteamAPI_ISteamApps_BIsDlcInstalled_o(self, appID);
|
||||
});
|
||||
}
|
||||
|
||||
DLL_EXPORT(int) SteamAPI_ISteamApps_GetDLCCount(ISteamApps* self) {
|
||||
|
@ -4,11 +4,19 @@
|
||||
using namespace smoke_api;
|
||||
|
||||
VIRTUAL(bool) ISteamApps_BIsSubscribedApp(PARAMS(AppId_t appID)) { // NOLINT(misc-unused-parameters)
|
||||
return steam_apps::IsDlcUnlocked(__func__, 0, appID);
|
||||
return steam_apps::IsDlcUnlocked(__func__, 0, appID, [&]() {
|
||||
GET_ORIGINAL_FUNCTION(ISteamApps_BIsSubscribedApp)
|
||||
|
||||
return ISteamApps_BIsSubscribedApp_o(ARGS(appID));
|
||||
});
|
||||
}
|
||||
|
||||
VIRTUAL(bool) ISteamApps_BIsDlcInstalled(PARAMS(AppId_t appID)) { // NOLINT(misc-unused-parameters)
|
||||
return steam_apps::IsDlcUnlocked(__func__, 0, appID);
|
||||
return steam_apps::IsDlcUnlocked(__func__, 0, appID, [&]() {
|
||||
GET_ORIGINAL_FUNCTION(ISteamApps_BIsDlcInstalled)
|
||||
|
||||
return ISteamApps_BIsDlcInstalled_o(ARGS(appID));
|
||||
});
|
||||
}
|
||||
|
||||
VIRTUAL(int) ISteamApps_GetDLCCount(PARAMS()) {
|
||||
|
@ -3,118 +3,120 @@
|
||||
#include <koalabox/io.hpp>
|
||||
#include <koalabox/http_client.hpp>
|
||||
#include <core/cache.hpp>
|
||||
#include <core/config.hpp>
|
||||
#include <smoke_api/smoke_api.hpp>
|
||||
|
||||
using namespace smoke_api;
|
||||
|
||||
/// 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;
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
try {
|
||||
app_id = steam_functions::get_app_id_or_throw();
|
||||
// TODO: Check what it returns in koalageddon mode
|
||||
logger->info("Detected App ID: {}", app_id);
|
||||
} catch (const Exception& ex) {
|
||||
logger->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 {
|
||||
const auto url = fmt::format("https://store.steampowered.com/dlc/{}/ajaxgetdlclist", app_id_str);
|
||||
const auto json = http_client::fetch_json(url);
|
||||
|
||||
if (json["success"] != 1) {
|
||||
throw util::exception("Web API responded with 'success' != 1");
|
||||
}
|
||||
|
||||
for (const auto& dlc: json["dlcs"]) {
|
||||
const auto app_id = dlc["appid"].get<String>();
|
||||
dlcs.emplace_back(std::stoi(app_id));
|
||||
}
|
||||
} catch (const Exception& e) {
|
||||
logger->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 = http_client::fetch_json(url);
|
||||
|
||||
if (json.contains(app_id_str)) {
|
||||
dlcs = json[app_id_str].get<decltype(dlcs)>();
|
||||
}
|
||||
} catch (const Exception& e) {
|
||||
logger->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();
|
||||
|
||||
// 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 = cache::get_dlc_ids(app_id);
|
||||
combined_dlcs.insert(cached_dlcs.begin(), cached_dlcs.end());
|
||||
}
|
||||
|
||||
// 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());
|
||||
|
||||
cache::save_dlc_ids(app_id, cached_dlcs);
|
||||
|
||||
return total_success;
|
||||
}
|
||||
|
||||
String get_app_id_log(const AppId_t app_id) {
|
||||
return app_id ? fmt::format("App ID: {}, ", app_id) : "";
|
||||
}
|
||||
|
||||
namespace steam_apps {
|
||||
using namespace smoke_api;
|
||||
|
||||
bool IsDlcUnlocked(const String& function_name, AppId_t app_id, AppId_t dlc_id) {
|
||||
/// 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;
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
try {
|
||||
app_id = steam_functions::get_app_id_or_throw();
|
||||
// TODO: Check what it returns in koalageddon mode
|
||||
logger->info("Detected App ID: {}", app_id);
|
||||
} catch (const Exception& ex) {
|
||||
logger->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 json = http_client::fetch_json(url);
|
||||
|
||||
if (json["success"] != 1) {
|
||||
throw util::exception("Web API responded with 'success' != 1");
|
||||
}
|
||||
|
||||
for (const auto& dlc: json["dlcs"]) {
|
||||
const auto app_id = dlc["appid"].get<String>();
|
||||
dlcs.emplace_back(std::stoi(app_id));
|
||||
}
|
||||
} catch (const Exception& e) {
|
||||
logger->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 = http_client::fetch_json(url);
|
||||
|
||||
if (json.contains(app_id_str)) {
|
||||
dlcs = json[app_id_str].get<decltype(dlcs)>();
|
||||
}
|
||||
} catch (const Exception& e) {
|
||||
logger->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();
|
||||
|
||||
// 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 = cache::get_dlc_ids(app_id);
|
||||
combined_dlcs.insert(cached_dlcs.begin(), cached_dlcs.end());
|
||||
}
|
||||
|
||||
// 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());
|
||||
|
||||
cache::save_dlc_ids(app_id, cached_dlcs);
|
||||
|
||||
return total_success;
|
||||
}
|
||||
|
||||
String get_app_id_log(const AppId_t app_id) {
|
||||
return app_id ? fmt::format("App ID: {}, ", app_id) : "";
|
||||
}
|
||||
|
||||
bool IsDlcUnlocked(
|
||||
const String& function_name,
|
||||
AppId_t app_id, AppId_t dlc_id,
|
||||
const std::function<bool()>& original_function
|
||||
) {
|
||||
try {
|
||||
const auto app_id_unlocked = not app_id or should_unlock(app_id); // true if app_id == 0
|
||||
const auto dlc_id_unlocked = should_unlock(dlc_id);
|
||||
const auto unlocked = config::is_dlc_unlocked(app_id, dlc_id, original_function);
|
||||
|
||||
const auto installed = app_id_unlocked and dlc_id_unlocked;
|
||||
logger->info("{} -> {}DLC ID: {}, Unlocked: {}", function_name, get_app_id_log(app_id), dlc_id, unlocked);
|
||||
|
||||
logger->info("{} -> {}DLC ID: {}, Unlocked: {}", function_name, get_app_id_log(app_id), dlc_id, installed);
|
||||
|
||||
return installed;
|
||||
return unlocked;
|
||||
} catch (const Exception& e) {
|
||||
logger->error("{} -> Uncaught exception: {}", function_name, e.what());
|
||||
return false;
|
||||
@ -142,7 +144,7 @@ namespace steam_apps {
|
||||
|
||||
// We need to fetch DLC IDs from all possible sources at this point
|
||||
|
||||
const auto injected_count = static_cast<int>(config.dlc_ids.size());
|
||||
const auto injected_count = static_cast<int>(config::instance.extra_dlc_ids.size());
|
||||
logger->debug("{} -> Injected DLC count: {}", function_name, injected_count);
|
||||
|
||||
// Maintain a list of app_ids for which we have already fetched and cached DLC IDs
|
||||
@ -191,7 +193,7 @@ namespace steam_apps {
|
||||
|
||||
// Fill the output pointers
|
||||
*pDlcId = dlc_id;
|
||||
*pbAvailable = should_unlock(dlc_id);
|
||||
*pbAvailable = config::is_dlc_unlocked(app_id, dlc_id, []() { return true; });
|
||||
|
||||
auto name = fmt::format("DLC #{} with ID: {} ", iDLC, dlc_id);
|
||||
name = name.substr(0, cchNameBufferSize);
|
||||
@ -218,7 +220,7 @@ namespace steam_apps {
|
||||
const auto success = original_function();
|
||||
|
||||
if (success) {
|
||||
*pbAvailable = should_unlock(*pDlcId);
|
||||
*pbAvailable = config::is_dlc_unlocked(app_id, *pDlcId, [&]() { return *pbAvailable; });
|
||||
print_dlc_info("original");
|
||||
} else {
|
||||
logger->warn("{} -> original call failed for index: {}", function_name, iDLC);
|
||||
@ -236,9 +238,9 @@ namespace steam_apps {
|
||||
logger->warn("{} -> Out of bounds DLC index: {}", function_name, iDLC);
|
||||
}
|
||||
|
||||
const int local_dlc_count = static_cast<int>(config.dlc_ids.size());
|
||||
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.dlc_ids, iDLC);
|
||||
return inject_dlc("local config", config::instance.extra_dlc_ids, iDLC);
|
||||
}
|
||||
|
||||
const auto adjusted_index = iDLC - local_dlc_count;
|
||||
|
@ -3,9 +3,18 @@
|
||||
namespace steam_apps {
|
||||
using namespace koalabox;
|
||||
|
||||
bool IsDlcUnlocked(const String& function_name, AppId_t app_id, AppId_t dlc_id);
|
||||
bool IsDlcUnlocked(
|
||||
const String& function_name,
|
||||
AppId_t app_id,
|
||||
AppId_t dlc_id,
|
||||
const std::function<bool()>& original_function
|
||||
);
|
||||
|
||||
int GetDLCCount(const String& function_name, AppId_t app_id, const std::function<int()>& original_function);
|
||||
int GetDLCCount(
|
||||
const String& function_name,
|
||||
AppId_t app_id,
|
||||
const std::function<int()>& original_function
|
||||
);
|
||||
|
||||
bool GetDLCDataByIndex(
|
||||
const String& dlc_id,
|
||||
|
@ -1,5 +1,5 @@
|
||||
#include <smoke_api/smoke_api.hpp>
|
||||
#include <steam_impl/steam_inventory.hpp>
|
||||
#include <core/config.hpp>
|
||||
|
||||
namespace steam_inventory {
|
||||
|
||||
@ -51,11 +51,11 @@ namespace steam_inventory {
|
||||
);
|
||||
|
||||
static uint32_t original_count = 0;
|
||||
const auto injected_count = smoke_api::config.inventory_items.size();
|
||||
const auto injected_count = config::instance.extra_inventory_items.size();
|
||||
|
||||
// Automatically get inventory items from steam
|
||||
static Vector<SteamItemDef_t> auto_inventory_items;
|
||||
if (smoke_api::config.auto_inject_inventory) {
|
||||
if (config::instance.auto_inject_inventory) {
|
||||
static std::once_flag flag;
|
||||
std::call_once(flag, [&]() {
|
||||
uint32_t count = 0;
|
||||
@ -102,7 +102,7 @@ namespace steam_inventory {
|
||||
|
||||
for (int i = 0; i < injected_count; i++) {
|
||||
auto& item = pOutItemsArray[original_count + auto_injected_count + i];
|
||||
const auto item_def_id = smoke_api::config.inventory_items[i];
|
||||
const auto item_def_id = config::instance.extra_inventory_items[i];
|
||||
|
||||
item = new_item(item_def_id);
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
#include <smoke_api/smoke_api.hpp>
|
||||
#include <koalabox/koalabox.hpp>
|
||||
#include <steam_functions/steam_functions.hpp>
|
||||
|
||||
using namespace smoke_api;
|
||||
|
||||
namespace steam_inventory {
|
||||
using namespace koalabox;
|
||||
|
||||
EResult GetResultStatus(
|
||||
const String& function_name,
|
||||
|
@ -1,5 +1,5 @@
|
||||
#include <steam_impl/steam_user.hpp>
|
||||
#include <smoke_api/smoke_api.hpp>
|
||||
#include <core/config.hpp>
|
||||
|
||||
namespace steam_user {
|
||||
|
||||
@ -15,7 +15,9 @@ namespace steam_user {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto has_license = smoke_api::should_unlock(appID);
|
||||
const auto has_license = config::is_dlc_unlocked(0, appID, [&]() {
|
||||
return result == k_EUserHasLicenseResultHasLicense;
|
||||
});
|
||||
|
||||
logger->info("{} -> App ID: {}, HasLicense: {}", function_name, appID, has_license);
|
||||
|
||||
|
@ -22,129 +22,4 @@ enum EUserHasLicenseForAppResult {
|
||||
k_EUserHasLicenseResultNoAuth = 2, // User has not been authenticated
|
||||
};
|
||||
|
||||
enum EResult {
|
||||
k_EResultNone = 0, // no result
|
||||
k_EResultOK = 1, // success
|
||||
k_EResultFail = 2, // generic failure
|
||||
k_EResultNoConnection = 3, // no/failed network connection
|
||||
// k_EResultNoConnectionRetry = 4, // OBSOLETE - removed
|
||||
k_EResultInvalidPassword = 5, // password/ticket is invalid
|
||||
k_EResultLoggedInElsewhere = 6, // same user logged in elsewhere
|
||||
k_EResultInvalidProtocolVer = 7, // protocol version is incorrect
|
||||
k_EResultInvalidParam = 8, // a parameter is incorrect
|
||||
k_EResultFileNotFound = 9, // file was not found
|
||||
k_EResultBusy = 10, // called method busy - action not taken
|
||||
k_EResultInvalidState = 11, // called object was in an invalid state
|
||||
k_EResultInvalidName = 12, // name is invalid
|
||||
k_EResultInvalidEmail = 13, // email is invalid
|
||||
k_EResultDuplicateName = 14, // name is not unique
|
||||
k_EResultAccessDenied = 15, // access is denied
|
||||
k_EResultTimeout = 16, // operation timed out
|
||||
k_EResultBanned = 17, // VAC2 banned
|
||||
k_EResultAccountNotFound = 18, // account not found
|
||||
k_EResultInvalidSteamID = 19, // steamID is invalid
|
||||
k_EResultServiceUnavailable = 20, // The requested service is currently unavailable
|
||||
k_EResultNotLoggedOn = 21, // The user is not logged on
|
||||
k_EResultPending = 22, // Request is pending (may be in process, or waiting on third party)
|
||||
k_EResultEncryptionFailure = 23, // Encryption or Decryption failed
|
||||
k_EResultInsufficientPrivilege = 24, // Insufficient privilege
|
||||
k_EResultLimitExceeded = 25, // Too much of a good thing
|
||||
k_EResultRevoked = 26, // Access has been revoked (used for revoked guest passes)
|
||||
k_EResultExpired = 27, // License/Guest pass the user is trying to access is expired
|
||||
k_EResultAlreadyRedeemed = 28, // Guest pass has already been redeemed by account, cannot be acked again
|
||||
k_EResultDuplicateRequest = 29, // The request is a duplicate and the action has already occurred in the past, ignored this time
|
||||
k_EResultAlreadyOwned = 30, // All the games in this guest pass redemption request are already owned by the user
|
||||
k_EResultIPNotFound = 31, // IP address not found
|
||||
k_EResultPersistFailed = 32, // failed to write change to the data store
|
||||
k_EResultLockingFailed = 33, // failed to acquire access lock for this operation
|
||||
k_EResultLogonSessionReplaced = 34,
|
||||
k_EResultConnectFailed = 35,
|
||||
k_EResultHandshakeFailed = 36,
|
||||
k_EResultIOFailure = 37,
|
||||
k_EResultRemoteDisconnect = 38,
|
||||
k_EResultShoppingCartNotFound = 39, // failed to find the shopping cart requested
|
||||
k_EResultBlocked = 40, // a user didn't allow it
|
||||
k_EResultIgnored = 41, // target is ignoring sender
|
||||
k_EResultNoMatch = 42, // nothing matching the request found
|
||||
k_EResultAccountDisabled = 43,
|
||||
k_EResultServiceReadOnly = 44, // this service is not accepting content changes right now
|
||||
k_EResultAccountNotFeatured = 45, // account doesn't have value, so this feature isn't available
|
||||
k_EResultAdministratorOK = 46, // allowed to take this action, but only because requester is admin
|
||||
k_EResultContentVersion = 47, // A Version mismatch in content transmitted within the Steam protocol.
|
||||
k_EResultTryAnotherCM = 48, // The current CM can't service the user making a request, user should try another.
|
||||
k_EResultPasswordRequiredToKickSession = 49,// You are already logged in elsewhere, this cached credential login has failed.
|
||||
k_EResultAlreadyLoggedInElsewhere = 50, // You are already logged in elsewhere, you must wait
|
||||
k_EResultSuspended = 51, // Long running operation (content download) suspended/paused
|
||||
k_EResultCancelled = 52, // Operation canceled (typically by user: content download)
|
||||
k_EResultDataCorruption = 53, // Operation canceled because data is ill formed or unrecoverable
|
||||
k_EResultDiskFull = 54, // Operation canceled - not enough disk space.
|
||||
k_EResultRemoteCallFailed = 55, // an remote call or IPC call failed
|
||||
k_EResultPasswordUnset = 56, // Password could not be verified as it's unset server side
|
||||
k_EResultExternalAccountUnlinked = 57, // External account (PSN, Facebook...) is not linked to a Steam account
|
||||
k_EResultPSNTicketInvalid = 58, // PSN ticket was invalid
|
||||
k_EResultExternalAccountAlreadyLinked = 59, // External account (PSN, Facebook...) is already linked to some other account, must explicitly request to replace/delete the link first
|
||||
k_EResultRemoteFileConflict = 60, // The sync cannot resume due to a conflict between the local and remote files
|
||||
k_EResultIllegalPassword = 61, // The requested new password is not legal
|
||||
k_EResultSameAsPreviousValue = 62, // new value is the same as the old one ( secret question and answer )
|
||||
k_EResultAccountLogonDenied = 63, // account login denied due to 2nd factor authentication failure
|
||||
k_EResultCannotUseOldPassword = 64, // The requested new password is not legal
|
||||
k_EResultInvalidLoginAuthCode = 65, // account login denied due to auth code invalid
|
||||
k_EResultAccountLogonDeniedNoMail = 66, // account login denied due to 2nd factor auth failure - and no mail has been sent
|
||||
k_EResultHardwareNotCapableOfIPT = 67, //
|
||||
k_EResultIPTInitError = 68, //
|
||||
k_EResultParentalControlRestricted = 69, // operation failed due to parental control restrictions for current user
|
||||
k_EResultFacebookQueryError = 70, // Facebook query returned an error
|
||||
k_EResultExpiredLoginAuthCode = 71, // account login denied due to auth code expired
|
||||
k_EResultIPLoginRestrictionFailed = 72,
|
||||
k_EResultAccountLockedDown = 73,
|
||||
k_EResultAccountLogonDeniedVerifiedEmailRequired = 74,
|
||||
k_EResultNoMatchingURL = 75,
|
||||
k_EResultBadResponse = 76, // parse failure, missing field, etc.
|
||||
k_EResultRequirePasswordReEntry = 77, // The user cannot complete the action until they re-enter their password
|
||||
k_EResultValueOutOfRange = 78, // the value entered is outside the acceptable range
|
||||
k_EResultUnexpectedError = 79, // something happened that we didn't expect to ever happen
|
||||
k_EResultDisabled = 80, // The requested service has been configured to be unavailable
|
||||
k_EResultInvalidCEGSubmission = 81, // The set of files submitted to the CEG server are not valid !
|
||||
k_EResultRestrictedDevice = 82, // The device being used is not allowed to perform this action
|
||||
k_EResultRegionLocked = 83, // The action could not be complete because it is region restricted
|
||||
k_EResultRateLimitExceeded = 84, // Temporary rate limit exceeded, try again later, different from k_EResultLimitExceeded which may be permanent
|
||||
k_EResultAccountLoginDeniedNeedTwoFactor = 85, // Need two-factor code to login
|
||||
k_EResultItemDeleted = 86, // The thing we're trying to access has been deleted
|
||||
k_EResultAccountLoginDeniedThrottle = 87, // login attempt failed, try to throttle response to possible attacker
|
||||
k_EResultTwoFactorCodeMismatch = 88, // two factor code mismatch
|
||||
k_EResultTwoFactorActivationCodeMismatch = 89, // activation code for two-factor didn't match
|
||||
k_EResultAccountAssociatedToMultiplePartners = 90, // account has been associated with multiple partners
|
||||
k_EResultNotModified = 91, // data not modified
|
||||
k_EResultNoMobileDevice = 92, // the account does not have a mobile device associated with it
|
||||
k_EResultTimeNotSynced = 93, // the time presented is out of range or tolerance
|
||||
k_EResultSmsCodeFailed = 94, // SMS code failure (no match, none pending, etc.)
|
||||
k_EResultAccountLimitExceeded = 95, // Too many accounts access this resource
|
||||
k_EResultAccountActivityLimitExceeded = 96, // Too many changes to this account
|
||||
k_EResultPhoneActivityLimitExceeded = 97, // Too many changes to this phone
|
||||
k_EResultRefundToWallet = 98, // Cannot refund to payment method, must use wallet
|
||||
k_EResultEmailSendFailure = 99, // Cannot send an email
|
||||
k_EResultNotSettled = 100, // Can't perform operation till payment has settled
|
||||
k_EResultNeedCaptcha = 101, // Needs to provide a valid captcha
|
||||
k_EResultGSLTDenied = 102, // a game server login token owned by this token's owner has been banned
|
||||
k_EResultGSOwnerDenied = 103, // game server owner is denied for other reason (account lock, community ban, vac ban, missing phone)
|
||||
k_EResultInvalidItemType = 104, // the type of thing we were requested to act on is invalid
|
||||
k_EResultIPBanned = 105, // the ip address has been banned from taking this action
|
||||
k_EResultGSLTExpired = 106, // this token has expired from disuse; can be reset for use
|
||||
k_EResultInsufficientFunds = 107, // user doesn't have enough wallet funds to complete the action
|
||||
k_EResultTooManyPending = 108, // There are too many of this thing pending already
|
||||
k_EResultNoSiteLicensesFound = 109, // No site licenses found
|
||||
k_EResultWGNetworkSendExceeded = 110, // the WG couldn't send a response because we exceeded max network send size
|
||||
k_EResultAccountNotFriends = 111, // the user is not mutually friends
|
||||
k_EResultLimitedUserAccount = 112, // the user is limited
|
||||
k_EResultCantRemoveItem = 113, // item can't be removed
|
||||
k_EResultAccountDeleted = 114, // account has been deleted
|
||||
k_EResultExistingUserCancelledLicense = 115, // A license for this already exists, but cancelled
|
||||
k_EResultCommunityCooldown = 116, // access is denied because of a community cooldown (probably from support profile data resets)
|
||||
k_EResultNoLauncherSpecified = 117, // No launcher was specified, but a launcher was needed to choose correct realm for operation.
|
||||
k_EResultMustAgreeToSSA = 118, // User must agree to china SSA or global SSA before login
|
||||
k_EResultLauncherMigrated = 119, // The specified launcher type is no longer supported; the user should be directed elsewhere
|
||||
k_EResultSteamRealmMismatch = 120, // The user's realm does not match the realm of the requested resource
|
||||
k_EResultInvalidSignature = 121, // signature check did not match
|
||||
k_EResultParseFailure = 122, // Failed to parse input
|
||||
k_EResultNoVerifiedPhone = 123, // account does not have a verified phone number
|
||||
};
|
||||
typedef uint32_t EResult;
|
||||
|
@ -3,11 +3,10 @@
|
||||
|
||||
using namespace smoke_api;
|
||||
|
||||
VIRTUAL(bool) IClientAppManager_IsAppDlcInstalled(
|
||||
PARAMS( // NOLINT(misc-unused-parameters)
|
||||
AppId_t app_id,
|
||||
AppId_t dlc_id
|
||||
)
|
||||
) {
|
||||
return steam_apps::IsDlcUnlocked(__func__, app_id, dlc_id);
|
||||
VIRTUAL(bool) IClientAppManager_IsAppDlcInstalled(PARAMS(AppId_t app_id, AppId_t dlc_id)) {
|
||||
return steam_apps::IsDlcUnlocked(__func__, app_id, dlc_id, [&]() {
|
||||
GET_ORIGINAL_VIRTUAL_FUNCTION(IClientAppManager_IsAppDlcInstalled)
|
||||
|
||||
return IClientAppManager_IsAppDlcInstalled_o(ARGS(app_id, dlc_id));
|
||||
});
|
||||
}
|
||||
|
@ -3,6 +3,10 @@
|
||||
|
||||
using namespace smoke_api;
|
||||
|
||||
VIRTUAL(bool) IClientUser_BIsSubscribedApp(PARAMS(AppId_t app_id)) { // NOLINT(misc-unused-parameters)
|
||||
return steam_apps::IsDlcUnlocked(__func__, 0, app_id);
|
||||
VIRTUAL(bool) IClientUser_BIsSubscribedApp(PARAMS(AppId_t app_id)) {
|
||||
return steam_apps::IsDlcUnlocked(__func__, 0, app_id, [&]() {
|
||||
GET_ORIGINAL_VIRTUAL_FUNCTION(IClientUser_BIsSubscribedApp)
|
||||
|
||||
return IClientUser_BIsSubscribedApp_o(ARGS(app_id));
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user