diff --git a/KoalaBox b/KoalaBox index 2e44fec..63b3efc 160000 --- a/KoalaBox +++ b/KoalaBox @@ -1 +1 @@ -Subproject commit 2e44fecbe5dbb6709e0dacc1bc66b368a63a69f9 +Subproject commit 63b3efc76a860aa11252aa69a1e08dd6bb4691e9 diff --git a/res/SmokeAPI.json b/res/SmokeAPI.json index 3ce5bc0..2bd4d80 100644 --- a/res/SmokeAPI.json +++ b/res/SmokeAPI.json @@ -1,6 +1,7 @@ { "$version": 2, "logging": true, + "unlock_family_sharing": true, "unlock_all": true, "override": [], "dlc_ids": [], diff --git a/src/smoke_api/smoke_api.cpp b/src/smoke_api/smoke_api.cpp index a965e02..28c0fd5 100644 --- a/src/smoke_api/smoke_api.cpp +++ b/src/smoke_api/smoke_api.cpp @@ -121,6 +121,7 @@ namespace smoke_api { } } + // FIXME: Support for app_id for koalageddon mode bool should_unlock(uint32_t app_id) { return config.unlock_all != config.override.contains(app_id); } diff --git a/src/steam_impl/steam_apps.cpp b/src/steam_impl/steam_apps.cpp index 254c572..3605073 100644 --- a/src/steam_impl/steam_apps.cpp +++ b/src/steam_impl/steam_apps.cpp @@ -2,43 +2,32 @@ #include #include +#include #include using namespace smoke_api; -constexpr auto max_dlc = 64; +/// 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/cache. +constexpr auto MAX_DLC = 64; Vector cached_dlcs; -int original_dlc_count = 0; +// Key: App ID, Value: DLC ID +Map original_dlc_count_map; // NOLINT(cert-err58-cpp) +// FIXME: Path in koalageddon mode Path get_cache_path() { static const auto path = self_directory / "SmokeAPI.cache.json"; return path; } -void read_from_cache(const String& app_id_str) { - try { - const auto text = io::read_file(get_cache_path()); - if (text.empty()) { - return; - } - - auto json = nlohmann::json::parse(text); - - cached_dlcs = json[app_id_str]["dlc"].get(); - - logger->debug("Read {} DLCs from cache", cached_dlcs.size()); - } catch (const Exception& ex) { - logger->error("Error reading DLCs from cache: {}", ex.what()); - } - -} - -void save_to_cache(const String& app_id_str) { +void save_cache_to_disk(const String& app_id_str) { try { logger->debug("Saving {} DLCs to cache", cached_dlcs.size()); + // TODO: Combine this with existing cache const nlohmann::json json = { {app_id_str, { {"dlc", cached_dlcs} @@ -51,85 +40,107 @@ void save_to_cache(const String& app_id_str) { } } -void fetch_and_cache_dlcs(AppId_t app_id) { +/** + * @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; + return false; } } + auto total_success = true; const auto app_id_str = std::to_string(app_id); - const auto fetch_from_steam = [&]() { - const auto url = fmt::format("https://store.steampowered.com/dlc/{}/ajaxgetdlclist", app_id_str); - const auto res = cpr::Get(cpr::Url{url}); - - if (res.status_code != cpr::status::HTTP_OK) { - throw util::exception( - "Steam Web API didn't responded with HTTP_OK result. Code: {}, Error: {},\n" - "Headers:\n{}\nBody:\n{}", - res.status_code, (int) res.error.code, res.raw_header, res.text - ); - } + const auto read_cache_from_disk = [&]() { + Vector dlcs; - const auto json = nlohmann::json::parse(res.text); + try { + const auto text = io::read_file(get_cache_path()); + + if (text.empty()) { + return dlcs; + } + auto json = nlohmann::json::parse(text); - if (json["success"] != 1) { - throw util::exception("Web API responded with 'success': 1."); + dlcs = json[app_id_str]["dlc"].get(); + + logger->debug("Read {} DLCs from cache", dlcs.size()); + } catch (const Exception& ex) { + logger->error("Error reading DLCs from cache: {}", ex.what()); + total_success = false; } + return dlcs; + }; + + const auto fetch_from_steam = [&]() { Vector dlcs; - for (const auto& dlc: json["dlcs"]) { - const auto app_id = dlc["appid"].get(); - dlcs.emplace_back(std::stoi(app_id)); + 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(); + 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 = [&]() { - const String url = "https://raw.githubusercontent.com/acidicoala/public-entitlements/main/steam/v1/dlc.json"; - const auto res = cpr::Get(cpr::Url{url}); - - if (res.status_code != cpr::status::HTTP_OK) { - throw util::exception( - "Github Web API didn't responded with HTTP_OK result. Code: {}, Error: {},\n" - "Headers:\n{}\nBody:\n{}", - res.status_code, (int) res.error.code, res.raw_header, res.text - ); - } + Vector dlcs; - const auto json = nlohmann::json::parse(res.text); + 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)) { - return json[app_id_str].get(); + if (json.contains(app_id_str)) { + dlcs = json[app_id_str].get(); + } + } catch (const Exception& e) { + logger->error("Failed to fetch extra dlc list from github api: {}", e.what()); + total_success = false; } - return Vector{}; + return dlcs; }; - try { - read_from_cache(app_id_str); + const auto cache_dlcs = read_cache_from_disk(); + const auto steam_dlcs = fetch_from_steam(); + const auto github_dlcs = fetch_from_github(); - auto list1 = fetch_from_steam(); - auto list2 = fetch_from_github(); - list1.insert(list1.end(), list2.begin(), list2.end()); - Set fetched_dlcs(list1.begin(), list1.end()); + // 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 combined_dlcs; + combined_dlcs.insert(cached_dlcs.begin(), cached_dlcs.end()); + combined_dlcs.insert(steam_dlcs.begin(), steam_dlcs.end()); + combined_dlcs.insert(github_dlcs.begin(), github_dlcs.end()); - if (fetched_dlcs.size() > cached_dlcs.size()) { - cached_dlcs = Vector(fetched_dlcs.begin(), fetched_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()); - save_to_cache(app_id_str); - } catch (const Exception& ex) { - logger->error("Failed to fetch DLC: {}", ex.what()); - } + save_cache_to_disk(app_id_str); + + return total_success; } String get_app_id_log(const AppId_t app_id) { @@ -139,21 +150,23 @@ String get_app_id_log(const AppId_t app_id) { namespace steam_apps { bool IsDlcUnlocked(const String& function_name, AppId_t app_id, AppId_t dlc_id) { - 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); + 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 installed = app_id_unlocked and dlc_id_unlocked; + 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, installed); + logger->info("{} -> {}DLC ID: {}, Unlocked: {}", function_name, get_app_id_log(app_id), dlc_id, installed); - return installed; + return installed; + } catch (const Exception& e) { + logger->error("{} -> Uncaught exception: {}", function_name, e.what()); + return false; + } } -// std::mutex section; int GetDLCCount(const String& function_name, const AppId_t app_id, const std::function& original_function) { try { -// std::lock_guard guard(section); - const auto total_count = [&](int count) { logger->info("{} -> Responding with DLC count: {}", function_name, count); return count; @@ -163,31 +176,38 @@ namespace steam_apps { logger->debug("{} -> App ID: {}", function_name, app_id); } - original_dlc_count = original_function(); - logger->debug("{} -> Original DLC count: {}", function_name, original_dlc_count); + const auto original_count = original_function(); + original_dlc_count_map[app_id] = original_count; + logger->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 const auto injected_count = static_cast(config.dlc_ids.size()); logger->debug("{} -> Injected DLC count: {}", function_name, injected_count); - if (original_dlc_count < max_dlc) { - return total_count(original_dlc_count + injected_count); - } + // Maintain a list of app_ids for which we have already fetched and cached DLC IDs + static Set cached_apps; + if (!cached_apps.contains(app_id)) { + static std::mutex mutex; + const std::lock_guard guard(mutex); - // Steamworks may max out this 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 fetch full list of IDs from web api. - static std::once_flag flag; - std::call_once(flag, [&]() { - logger->debug("Game has {} or more DLCs. Fetching DLCs from a web API.", max_dlc); - fetch_and_cache_dlcs(app_id); - }); + logger->debug("Game has {} or more DLCs. Fetching DLCs from a web API.", MAX_DLC); + + if (fetch_and_cache_dlcs(app_id)) { + cached_apps.insert(app_id); + } + } - const auto fetched_count = static_cast(cached_dlcs.size()); - logger->debug("{} -> Fetched/cached DLC count: {}", function_name, fetched_count); + const auto cached_count = static_cast(cached_dlcs.size()); + logger->debug("{} -> Cached DLC count: {}", function_name, cached_count); - return total_count(fetched_count + injected_count); - } catch (const Exception& ex) { - logger->error("{} -> {}", function_name, ex.what()); + return total_count(injected_count + cached_count); + } catch (const Exception& e) { + logger->error("{} -> Uncaught exception: {}", function_name, e.what()); return 0; } } @@ -202,76 +222,84 @@ namespace steam_apps { int cchNameBufferSize, const std::function& original_function ) { - const auto print_dlc_info = [&](const String& tag) { - logger->info( - "{} -> [{}] {}index: {}, DLC ID: {}, available: {}, name: '{}'", - function_name, tag, get_app_id_log(app_id), iDLC, *pDlcId, *pbAvailable, pchName - ); - }; + try { + const auto print_dlc_info = [&](const String& tag) { + logger->info( + "{} -> [{}] {}index: {}, DLC ID: {}, available: {}, name: '{}'", + function_name, tag, get_app_id_log(app_id), iDLC, *pDlcId, *pbAvailable, pchName + ); + }; - const auto fill_dlc_info = [&](const AppId_t id) { - *pDlcId = id; - *pbAvailable = should_unlock(id); + const auto inject_dlc = [&](const String& tag, const Vector& dlc_ids, const int index) { + const auto dlc_id = dlc_ids[index]; - auto name = fmt::format("DLC #{} with ID: {} ", iDLC, id); - name = name.substr(0, cchNameBufferSize); - *name.rbegin() = '\0'; - memcpy_s(pchName, cchNameBufferSize, name.c_str(), name.size()); - }; + // Fill the output pointers + *pDlcId = dlc_id; + *pbAvailable = should_unlock(dlc_id); - const auto inject_dlc = [&](const int index) { - if (index >= config.dlc_ids.size()) { - logger->error("{} -> Out of bounds injected index: {}", function_name, index); - return false; - } + auto name = fmt::format("DLC #{} with ID: {} ", iDLC, dlc_id); + name = name.substr(0, cchNameBufferSize); + *name.rbegin() = '\0'; + memcpy_s(pchName, cchNameBufferSize, name.c_str(), name.size()); - const auto dlc_id = config.dlc_ids[index]; - fill_dlc_info(dlc_id); - print_dlc_info("injected"); - return true; - }; + print_dlc_info(tag); + return true; + }; - // Original response - if (cached_dlcs.empty()) { - // Original DLC index - if (iDLC < original_dlc_count) { + 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]; + } + + 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 = should_unlock(*pDlcId); print_dlc_info("original"); } else { - logger->warn("{} -> original function failed for index: {}", function_name, iDLC); + logger->warn("{} -> original call failed for index: {}", function_name, iDLC); } return success; } - // Injected DLC index (after original) - const auto index = iDLC - original_dlc_count; - return inject_dlc(index); - } + // 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 + // cached DLC should always follow the injected DLCs as follows: + // [injected-dlc-0, injected-dlc-1, ..., cached-dlc-0, cached-dlc-1, ...] + + if (iDLC < 0) { + logger->warn("{} -> Out of bounds DLC index: {}", function_name, iDLC); + } + + const int local_dlc_count = static_cast(config.dlc_ids.size()); + if (iDLC < local_dlc_count) { + return inject_dlc("local config", config.dlc_ids, iDLC); + } + + const auto adjusted_index = iDLC - local_dlc_count; + const int cached_dlc_count = static_cast(cached_dlcs.size()); + if (iDLC < cached_dlc_count) { + return inject_dlc("memory cache", cached_dlcs, adjusted_index); + } - // Cached response - const auto total_size = cached_dlcs.size() + config.dlc_ids.size(); - if (iDLC < 0 or iDLC >= total_size) { logger->error( - "{} -> Game accessed out of bounds DLC index: {}. Total size: {}", - function_name, iDLC, total_size + "{} -> Out of bounds DLC index: {}, local dlc count: {}, cached dlc count: {}", + function_name, iDLC, local_dlc_count, cached_dlc_count ); - return false; - } - // Cached index - if (iDLC < cached_dlcs.size()) { - const auto dlc_id = cached_dlcs[iDLC]; - fill_dlc_info(dlc_id); - print_dlc_info("cached"); - return true; + return false; + } catch (const Exception& e) { + logger->error("{} -> Uncaught exception: {}", function_name, e.what()); + return false; } - - // Injected DLC index (after cached) - - const auto index = iDLC - static_cast(cached_dlcs.size()); - return inject_dlc(index); } } diff --git a/src/steam_impl/steam_apps.hpp b/src/steam_impl/steam_apps.hpp index 796acfe..90b8479 100644 --- a/src/steam_impl/steam_apps.hpp +++ b/src/steam_impl/steam_apps.hpp @@ -9,8 +9,8 @@ namespace steam_apps { int GetDLCCount(const String& function_name, AppId_t app_id, const std::function& original_function); bool GetDLCDataByIndex( - const String& function_name, - AppId_t app_id, + const String& dlc_id, + AppId_t dlc_ids, int iDLC, AppId_t* pDlcId, bool* pbAvailable,