Refactored DLC unlocking logic

This commit is contained in:
acidicoala 2023-01-06 05:18:13 +03:00
parent b04c96a36d
commit 011f3fac5d
No known key found for this signature in database
GPG Key ID: D24C6065B49C645B
19 changed files with 310 additions and 317 deletions

View File

@ -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

@ -1 +1 @@
Subproject commit 418cb437a7eda9c7e13cdcdbe6e538fa9e4a9281
Subproject commit 53731ddc6b82281df820884f2d2424ee3a62df54

View File

@ -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

View File

@ -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
View 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
View 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);
}

View File

@ -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++;

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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) {

View File

@ -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()) {

View File

@ -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;

View File

@ -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,

View File

@ -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);

View File

@ -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,

View File

@ -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);

View File

@ -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;

View File

@ -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));
});
}

View File

@ -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));
});
}