From b6c7df40b6ae56bd2495765d58bc28519b416643 Mon Sep 17 00:00:00 2001 From: otavepto Date: Sat, 23 Mar 2024 07:22:47 +0200 Subject: [PATCH] * implemented the missing interface `ISteamGameServerStats`, allowing game servers to exchange user stats with players * add rmCallback() to networking * refactor gameserver_stats into a separate .cpp file --- CHANGELOG.md | 6 +- dll/dll/network.h | 14 +- dll/dll/settings.h | 8 +- dll/dll/steam_gameserverstats.h | 46 +- dll/dll/steam_user_stats.h | 1446 ++++++----------------------- dll/net.proto | 54 ++ dll/network.cpp | 38 +- dll/settings_parser.cpp | 6 +- dll/steam_client.cpp | 8 +- dll/steam_gameserver.cpp | 6 +- dll/steam_gameserverstats.cpp | 462 +++++++++- dll/steam_user_stats.cpp | 1527 +++++++++++++++++++++++++++++++ 12 files changed, 2412 insertions(+), 1209 deletions(-) create mode 100644 dll/steam_user_stats.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 02b9ed5d..cb091e07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,16 @@ - the above command introduced the ability to run without root - if the script was ran without root, and `-packages_skip` wasn't specified, the script will attempt to detect and use the built-in tool `sudo` if it was available +* implemented the missing interface `ISteamGameServerStats`, allowing game servers to exchange user stats with players * for windows: updated stub drm patterns and added a workaround for older variants, this increases the compatibility, but makes it easier to be detected * new stub dll `GameOverlayRenderer` for the experiemntal steamclient setup, some apps verify the existence of this dll, either on disk, or inside their memory space. - not recommended to ignore it -* allow overlay invitations to obscure game input to be able to accept/reject the request + **not recommended** to ignore it +* added new function `rmCallbacks()` for the networking, to be able to cleanup callbacks on object destruction * added missing example file `disable_lobby_creation.txt` in `steam_settings` folder + updated release `README` * for windows build script: prevent permissive language extensions via the compiler flag `/permissive-` +* allow overlay invitations to obscure game input to be able to accept/reject the request --- diff --git a/dll/dll/network.h b/dll/dll/network.h index f370a4bc..74df2b67 100644 --- a/dll/dll/network.h +++ b/dll/dll/network.h @@ -62,12 +62,13 @@ enum Callback_Ids { CALLBACK_ID_NETWORKING_SOCKETS, CALLBACK_ID_STEAM_MESSAGES, CALLBACK_ID_NETWORKING_MESSAGES, + CALLBACK_ID_GAMESERVER_STATS, CALLBACK_IDS_MAX }; struct Network_Callback_Container { - std::vector callbacks; + std::vector callbacks{}; }; struct TCP_Socket { @@ -131,12 +132,23 @@ public: void addListenId(CSteamID id); void setAppID(uint32 appid); void Run(); + + // send to a specific user, if 0 was passed to set_dest_id() then this will be broadcasted to all users on the network bool sendTo(Common_Message *msg, bool reliable, Connection *conn = NULL); + + // send to all users whose account type is Individual, no need to call set_dest_id(), this is done automatically bool sendToAllIndividuals(Common_Message *msg, bool reliable); + + // send to all active/current connections, no need to call set_dest_id(), this is done automatically bool sendToAll(Common_Message *msg, bool reliable); + + // send to active/current connections with specific ip/port, no need to call set_dest_id(), this is done automatically + //TODO: actually send to ip/port bool sendToIPPort(Common_Message *msg, uint32 ip, uint16 port, bool reliable); bool setCallback(Callback_Ids id, CSteamID steam_id, void (*message_callback)(void *object, Common_Message *msg), void *object); + void rmCallback(Callback_Ids id, CSteamID steam_id, void (*message_callback)(void *object, Common_Message *msg), void *object); + uint32 getIP(CSteamID id); uint32 getOwnIP(); diff --git a/dll/dll/settings.h b/dll/dll/settings.h index df6b397b..fefad045 100644 --- a/dll/dll/settings.h +++ b/dll/dll/settings.h @@ -78,14 +78,8 @@ struct Leaderboard_config { enum ELeaderboardDisplayType display_type; }; -enum Stat_Type { - STAT_TYPE_INT, - STAT_TYPE_FLOAT, - STAT_TYPE_AVGRATE -}; - struct Stat_config { - enum Stat_Type type; + GameServerStats_Messages::StatInfo::Stat_Type type; union { float default_value_float; int32 default_value_int; diff --git a/dll/dll/steam_gameserverstats.h b/dll/dll/steam_gameserverstats.h index 8efde54d..e38a0fd0 100644 --- a/dll/dll/steam_gameserverstats.h +++ b/dll/dll/steam_gameserverstats.h @@ -26,8 +26,52 @@ class Steam_GameServerStats : public ISteamGameServerStats class Networking *network; class SteamCallResults *callback_results; class SteamCallBacks *callbacks; + class RunEveryRunCB *run_every_runcb; + + struct RequestAllStats { + std::chrono::high_resolution_clock::time_point created{}; + SteamAPICall_t steamAPICall{}; + CSteamID steamIDUser{}; + + bool timeout = false; + }; + + struct CachedStat { + bool dirty = false; // true means it was changed on the server and should be sent to the user + GameServerStats_Messages::StatInfo stat{}; + }; + struct CachedAchievement { + bool dirty = false; // true means it was changed on the server and should be sent to the user + GameServerStats_Messages::AchievementInfo ach{}; + }; + + struct UserData { + std::map stats{}; + std::map achievements{}; + }; + + std::vector pending_RequestUserStats{}; + std::map all_users_data{}; + + CachedStat* find_stat(CSteamID steamIDUser, const std::string &key); + CachedAchievement* find_ach(CSteamID steamIDUser, const std::string &key); + + void remove_timedout_userstats_requests(); + void collect_and_send_updated_user_stats(); + void steam_run_callback(); + + // reponses from player + void network_callback_initial_stats(Common_Message *msg); + void network_callback_updated_stats(Common_Message *msg); + void network_callback(Common_Message *msg); + + static void steam_gameserverstats_network_callback(void *object, Common_Message *msg); + static void steam_gameserverstats_run_every_runcb(void *object); + public: - Steam_GameServerStats(class Settings *settings, class Networking *network, class SteamCallResults *callback_results, class SteamCallBacks *callbacks); + Steam_GameServerStats(class Settings *settings, class Networking *network, class SteamCallResults *callback_results, class SteamCallBacks *callbacks, class RunEveryRunCB *run_every_runcb); + ~Steam_GameServerStats(); + // downloads stats for the user // returns a GSStatsReceived_t callback when completed // if the user has no stats, GSStatsReceived_t.m_eResult will be set to k_EResultFail diff --git a/dll/dll/steam_user_stats.h b/dll/dll/steam_user_stats.h index 213b7da9..0a3a9155 100644 --- a/dll/dll/steam_user_stats.h +++ b/dll/dll/steam_user_stats.h @@ -74,1187 +74,313 @@ public: static constexpr auto achievements_user_file = "achievements.json"; private: + template + struct InternalSetResult { + bool success = false; + bool notify_server = false; + std::string internal_name{}; + T current_val{}; + }; - Local_Storage *local_storage; - Settings *settings; - SteamCallResults *callback_results; - class SteamCallBacks *callbacks; - class Steam_Overlay* overlay; + Local_Storage *local_storage{}; + Settings *settings{}; + SteamCallResults *callback_results{}; + class SteamCallBacks *callbacks{}; + class Networking *network{}; + class RunEveryRunCB *run_every_runcb{}; + class Steam_Overlay* overlay{}; - std::vector leaderboards; + std::vector leaderboards{}; - nlohmann::json defined_achievements; - nlohmann::json user_achievements; - std::vector sorted_achievement_names; - std::map stats_cache_int; - std::map stats_cache_float; + nlohmann::json defined_achievements{}; + nlohmann::json user_achievements{}; + std::vector sorted_achievement_names{}; + std::map stats_cache_int{}; + std::map stats_cache_float{}; - std::map> achievement_stat_trigger; + std::map> achievement_stat_trigger{}; + + GameServerStats_Messages::AllStats pending_server_updates{}; -unsigned int find_leaderboard(std::string name) -{ - unsigned index = 1; - for (auto &leaderboard : leaderboards) { - if (leaderboard.name == name) return index; - ++index; - } - return 0; -} + unsigned int find_leaderboard(std::string name); -nlohmann::detail::iter_impl defined_achievements_find(const std::string &key) -{ - return std::find_if( - defined_achievements.begin(), defined_achievements.end(), - [&key](const nlohmann::json& item) { - const std::string &name = static_cast( item.value("name", std::string()) ); - return key.size() == name.size() && - std::equal( - name.begin(), name.end(), key.begin(), - [](char a, char b) { return std::tolower(a) == std::tolower(b); } - ); - } - ); -} + nlohmann::detail::iter_impl defined_achievements_find(const std::string &key); -void load_achievements_db() -{ - std::string file_path = Local_Storage::get_game_settings_path() + achievements_user_file; - local_storage->load_json(file_path, defined_achievements); -} + void load_achievements_db(); -void load_achievements() -{ - local_storage->load_json_file("", achievements_user_file, user_achievements); -} + void load_achievements(); -void save_achievements() -{ - local_storage->write_json_file("", achievements_user_file, user_achievements); -} + void save_achievements(); -void save_leaderboard_score(Steam_Leaderboard *leaderboard) -{ - std::vector output; - uint64_t steam_id = leaderboard->self_score.steam_id.ConvertToUint64(); - output.push_back(steam_id & 0xFFFFFFFF); - output.push_back(steam_id >> 32); + void save_leaderboard_score(Steam_Leaderboard *leaderboard); - output.push_back(leaderboard->self_score.score); - output.push_back(leaderboard->self_score.score_details.size()); - for (auto &s : leaderboard->self_score.score_details) { - output.push_back(s); - } + std::vector load_leaderboard_scores(std::string name); - std::string leaderboard_name = common_helpers::ascii_to_lowercase(leaderboard->name); - local_storage->store_data(Local_Storage::leaderboard_storage_folder, leaderboard_name, (char* )output.data(), sizeof(uint32_t) * output.size()); -} + std::string get_value_for_language(nlohmann::json &json, std::string key, std::string language); -std::vector load_leaderboard_scores(std::string name) -{ - std::vector out; + // change stats/achievements without sending back to server + InternalSetResult set_stat_internal( const char *pchName, int32 nData ); + InternalSetResult> set_stat_internal( const char *pchName, float fData ); + InternalSetResult> update_avg_rate_stat_internal( const char *pchName, float flCountThisSession, double dSessionLength ); + InternalSetResult set_achievement_internal( const char *pchName ); + InternalSetResult clear_achievement_internal( const char *pchName ); - std::string leaderboard_name = common_helpers::ascii_to_lowercase(name); - unsigned size = local_storage->file_size(Local_Storage::leaderboard_storage_folder, leaderboard_name); - if (size == 0 || (size % sizeof(uint32_t)) != 0) return out; - std::vector output(size / sizeof(uint32_t)); - if (local_storage->get_data(Local_Storage::leaderboard_storage_folder, leaderboard_name, (char* )output.data(), size) != size) return out; + void send_updated_stats(); + void steam_run_callback(); - unsigned i = 0; - while (true) { - if ((i + 4) > output.size()) break; + // requests from server + void network_callback_initial_stats(Common_Message *msg); + void network_callback_updated_stats(Common_Message *msg); + void network_callback(Common_Message *msg); - Steam_Leaderboard_Score score; - score.steam_id = CSteamID((uint64)output[i] + (((uint64)output[i + 1]) << 32)); - i += 2; - score.score = output[i]; - i += 1; - unsigned count = output[i]; - i += 1; - - if ((i + count) > output.size()) break; - - for (unsigned j = 0; j < count; ++j) { - score.score_details.push_back(output[i]); - i += 1; - } - - PRINT_DEBUG("Steam_User_Stats::loaded leaderboard score %llu %u\n", score.steam_id.ConvertToUint64(), score.score); - out.push_back(score); - } - - return out; -} - -std::string get_value_for_language(nlohmann::json &json, std::string key, std::string language) -{ - auto x = json.find(key); - if (x == json.end()) return ""; - if (x.value().is_string()) { - return x.value().get(); - } else if (x.value().is_object()) { - auto l = x.value().find(language); - if (l != x.value().end()) { - return l.value().get(); - } - - l = x.value().find("english"); - if (l != x.value().end()) { - return l.value().get(); - } - - l = x.value().begin(); - if (l != x.value().end()) { - if (l.key() == "token") { - std::string token_value = l.value().get(); - l++; - if (l != x.value().end()) { - return l.value().get(); - } - - return token_value; - } - - return l.value().get(); - } - } - - return ""; -} + static void steam_user_stats_network_callback(void *object, Common_Message *msg); + static void steam_user_stats_run_every_runcb(void *object); public: -Steam_User_Stats(Settings *settings, Local_Storage *local_storage, class SteamCallResults *callback_results, class SteamCallBacks *callbacks, Steam_Overlay* overlay): - settings(settings), - local_storage(local_storage), - callback_results(callback_results), - callbacks(callbacks), - defined_achievements(nlohmann::json::object()), - user_achievements(nlohmann::json::object()), - overlay(overlay) -{ - load_achievements_db(); // achievements db - load_achievements(); // achievements per user - - auto x = defined_achievements.begin(); - while (x != defined_achievements.end()) { - - if (!x->contains("name")) { - x = defined_achievements.erase(x); - } else { - ++x; - } - } - - for (auto & it : defined_achievements) { - try { - std::string name = static_cast(it["name"]); - sorted_achievement_names.push_back(name); - if (user_achievements.find(name) == user_achievements.end()) { - user_achievements[name]["earned"] = false; - user_achievements[name]["earned_time"] = static_cast(0); - } - - achievement_trigger trig; - trig.name = name; - trig.value_operation = static_cast(it["progress"]["value"]["operation"]); - std::string stat_name = common_helpers::ascii_to_lowercase(static_cast(it["progress"]["value"]["operand1"])); - trig.min_value = static_cast(it["progress"]["min_val"]); - trig.max_value = static_cast(it["progress"]["max_val"]); - achievement_stat_trigger[stat_name].push_back(trig); - } catch (...) {} - - try { - it["hidden"] = std::to_string(it["hidden"].get()); - } catch (...) {} - - it["displayName"] = get_value_for_language(it, "displayName", settings->get_language()); - it["description"] = get_value_for_language(it, "description", settings->get_language()); - } - - //TODO: not sure if the sort is actually case insensitive, ach names seem to be treated by steam as case insensitive so I assume they are. - //need to find a game with achievements of different case names to confirm - std::sort(sorted_achievement_names.begin(), sorted_achievement_names.end(), [](const std::string lhs, const std::string rhs){ - const auto result = std::mismatch(lhs.cbegin(), lhs.cend(), rhs.cbegin(), rhs.cend(), [](const unsigned char lhs, const unsigned char rhs){return std::tolower(lhs) == std::tolower(rhs);}); - return result.second != rhs.cend() && (result.first == lhs.cend() || std::tolower(*result.first) < std::tolower(*result.second));} - ); -} - -// Ask the server to send down this user's data and achievements for this game -STEAM_CALL_BACK( UserStatsReceived_t ) -bool RequestCurrentStats() -{ - PRINT_DEBUG("Steam_User_Stats::RequestCurrentStats\n"); - std::lock_guard lock(global_mutex); - - UserStatsReceived_t data{}; - data.m_nGameID = settings->get_local_game_id().ToUint64(); - data.m_eResult = k_EResultOK; - data.m_steamIDUser = settings->get_local_steam_id(); - callbacks->addCBResult(data.k_iCallback, &data, sizeof(data), 0.1); - return true; -} - - -// Data accessors -bool GetStat( const char *pchName, int32 *pData ) -{ - PRINT_DEBUG("Steam_User_Stats::GetStat '%s' %p\n", pchName, pData); - std::lock_guard lock(global_mutex); - - if (!pchName) return false; - std::string stat_name = common_helpers::ascii_to_lowercase(pchName); - - const auto &stats_config = settings->getStats(); - auto stats_data = stats_config.find(stat_name); - if (stats_config.end() == stats_data) return false; - if (stats_data->second.type != Stat_Type::STAT_TYPE_INT) return false; - - auto cached_stat = stats_cache_int.find(stat_name); - if (cached_stat != stats_cache_int.end()) { - if (pData) *pData = cached_stat->second; - return true; - } - - int32 output = 0; - int read_data = local_storage->get_data(Local_Storage::stats_storage_folder, stat_name, (char* )&output, sizeof(output)); - if (read_data == sizeof(int32)) { - stats_cache_int[stat_name] = output; - if (pData) *pData = output; - return true; - } - - stats_cache_int[stat_name] = stats_data->second.default_value_int; - if (pData) *pData = stats_data->second.default_value_int; - return true; -} - -bool GetStat( const char *pchName, float *pData ) -{ - PRINT_DEBUG("Steam_User_Stats::GetStat '%s' %p\n", pchName, pData); - std::lock_guard lock(global_mutex); - - if (!pchName) return false; - std::string stat_name = common_helpers::ascii_to_lowercase(pchName); - - const auto &stats_config = settings->getStats(); - auto stats_data = stats_config.find(stat_name); - if (stats_config.end() == stats_data) return false; - if (stats_data->second.type == Stat_Type::STAT_TYPE_INT) return false; - - auto cached_stat = stats_cache_float.find(stat_name); - if (cached_stat != stats_cache_float.end()) { - if (pData) *pData = cached_stat->second; - return true; - } - - float output = 0.0; - int read_data = local_storage->get_data(Local_Storage::stats_storage_folder, stat_name, (char* )&output, sizeof(output)); - if (read_data == sizeof(float)) { - stats_cache_float[stat_name] = output; - if (pData) *pData = output; - return true; - } - - stats_cache_float[stat_name] = stats_data->second.default_value_float; - if (pData) *pData = stats_data->second.default_value_float; - return true; -} - - -// Set / update data -bool SetStat( const char *pchName, int32 nData ) -{ - PRINT_DEBUG("Steam_User_Stats::SetStat '%s' = %i\n", pchName, nData); - std::lock_guard lock(global_mutex); - - if (!pchName) return false; - std::string stat_name = common_helpers::ascii_to_lowercase(pchName); - - const auto &stats_config = settings->getStats(); - auto stats_data = stats_config.find(stat_name); - if (stats_config.end() == stats_data) return false; - if (stats_data->second.type != Stat_Type::STAT_TYPE_INT) return false; - - auto cached_stat = stats_cache_int.find(stat_name); - if (cached_stat != stats_cache_int.end()) { - if (cached_stat->second == nData) return true; - } - - auto stat_trigger = achievement_stat_trigger.find(stat_name); - if (stat_trigger != achievement_stat_trigger.end()) { - for (auto &t : stat_trigger->second) { - if (t.check_triggered(nData)) { - SetAchievement(t.name.c_str()); - } - } - } - - if (local_storage->store_data(Local_Storage::stats_storage_folder, stat_name, (char* )&nData, sizeof(nData)) == sizeof(nData)) { - stats_cache_int[stat_name] = nData; - return true; - } - - return false; -} - -bool SetStat( const char *pchName, float fData ) -{ - PRINT_DEBUG("Steam_User_Stats::SetStat '%s' = %f\n", pchName, fData); - std::lock_guard lock(global_mutex); - - if (!pchName) return false; - std::string stat_name = common_helpers::ascii_to_lowercase(pchName); - - const auto &stats_config = settings->getStats(); - auto stats_data = stats_config.find(stat_name); - if (stats_config.end() == stats_data) return false; - if (stats_data->second.type == Stat_Type::STAT_TYPE_INT) return false; - - auto cached_stat = stats_cache_float.find(stat_name); - if (cached_stat != stats_cache_float.end()) { - if (cached_stat->second == fData) return true; - } - - auto stat_trigger = achievement_stat_trigger.find(stat_name); - if (stat_trigger != achievement_stat_trigger.end()) { - for (auto &t : stat_trigger->second) { - if (t.check_triggered(fData)) { - SetAchievement(t.name.c_str()); - } - } - } - - if (local_storage->store_data(Local_Storage::stats_storage_folder, stat_name, (char* )&fData, sizeof(fData)) == sizeof(fData)) { - stats_cache_float[stat_name] = fData; - return true; - } - - return false; -} - -bool UpdateAvgRateStat( const char *pchName, float flCountThisSession, double dSessionLength ) -{ - PRINT_DEBUG("Steam_User_Stats::UpdateAvgRateStat %s\n", pchName); - std::lock_guard lock(global_mutex); - - if (!pchName) return false; - std::string stat_name = common_helpers::ascii_to_lowercase(pchName); - - const auto &stats_config = settings->getStats(); - auto stats_data = stats_config.find(stat_name); - if (stats_config.end() == stats_data) return false; - if (stats_data->second.type == Stat_Type::STAT_TYPE_INT) return false; - - char data[sizeof(float) + sizeof(float) + sizeof(double)]; - int read_data = local_storage->get_data(Local_Storage::stats_storage_folder, stat_name, (char* )data, sizeof(*data)); - float oldcount = 0; - double oldsessionlength = 0; - if (read_data == sizeof(data)) { - memcpy(&oldcount, data + sizeof(float), sizeof(oldcount)); - memcpy(&oldsessionlength, data + sizeof(float) + sizeof(double), sizeof(oldsessionlength)); - } - - oldcount += flCountThisSession; - oldsessionlength += dSessionLength; - - float average = oldcount / oldsessionlength; - memcpy(data, &average, sizeof(average)); - memcpy(data + sizeof(float), &oldcount, sizeof(oldcount)); - memcpy(data + sizeof(float) * 2, &oldsessionlength, sizeof(oldsessionlength)); - - if (local_storage->store_data(Local_Storage::stats_storage_folder, stat_name, data, sizeof(data)) == sizeof(data)) { - stats_cache_float[stat_name] = average; - return true; - } - - return false; -} - - -// Achievement flag accessors -bool GetAchievement( const char *pchName, bool *pbAchieved ) -{ - PRINT_DEBUG("Steam_User_Stats::GetAchievement '%s'\n", pchName); - std::lock_guard lock(global_mutex); - - if (!pchName) return false; - - nlohmann::detail::iter_impl it = defined_achievements.end(); - try { - it = defined_achievements_find(pchName); - } catch(...) { } - if (defined_achievements.end() == it) return false; - - // according to docs, the function returns true if the achievement was found, - // regardless achieved or not - if (!pbAchieved) return true; - - *pbAchieved = false; - try { - std::string pch_name = it->value("name", std::string()); - auto ach = user_achievements.find(pch_name); - if (user_achievements.end() != ach) { - *pbAchieved = ach->value("earned", false); - } - } catch (...) { } - - return true; -} - -bool SetAchievement( const char *pchName ) -{ - PRINT_DEBUG("Steam_User_Stats::SetAchievement '%s'\n", pchName); - std::lock_guard lock(global_mutex); - - if (!pchName) return false; - - if (settings->achievement_bypass) return true; - - nlohmann::detail::iter_impl it = defined_achievements.end(); - try { - it = defined_achievements_find(pchName); - } catch(...) { } - if (defined_achievements.end() == it) return false; - - try { - std::string pch_name = it->value("name", std::string()); - auto ach = user_achievements.find(pch_name); - if (user_achievements.end() == ach || ach->value("earned", false) == false) { - user_achievements[pch_name]["earned"] = true; - user_achievements[pch_name]["earned_time"] = - std::chrono::duration_cast>(std::chrono::system_clock::now().time_since_epoch()).count(); - - save_achievements(); - - if(!settings->disable_overlay) overlay->AddAchievementNotification(it.value()); - - } - } catch (...) {} - - return true; -} - -bool ClearAchievement( const char *pchName ) -{ - PRINT_DEBUG("Steam_User_Stats::ClearAchievement %s\n", pchName); - std::lock_guard lock(global_mutex); - - if (!pchName) return false; - - nlohmann::detail::iter_impl it = defined_achievements.end(); - try { - it = defined_achievements_find(pchName); - } catch(...) { } - if (defined_achievements.end() == it) return false; - - try { - std::string pch_name = it->value("name", std::string()); - auto ach = user_achievements.find(pch_name); - // assume "earned" is true in case the json obj exists, but the key is absent - // assume "earned_time" is UINT32_MAX in case the json obj exists, but the key is absent - if (user_achievements.end() == ach || - ach->value("earned", true) == true || - ach->value("earned_time", static_cast(UINT32_MAX)) == UINT32_MAX) { - - user_achievements[pch_name]["earned"] = false; - user_achievements[pch_name]["earned_time"] = static_cast(0); - save_achievements(); - } - } catch (...) {} - - return true; -} - - -// Get the achievement status, and the time it was unlocked if unlocked. -// If the return value is true, but the unlock time is zero, that means it was unlocked before Steam -// began tracking achievement unlock times (December 2009). Time is seconds since January 1, 1970. -bool GetAchievementAndUnlockTime( const char *pchName, bool *pbAchieved, uint32 *punUnlockTime ) -{ - PRINT_DEBUG("Steam_User_Stats::GetAchievementAndUnlockTime\n"); - std::lock_guard lock(global_mutex); - - if (!pchName) return false; - - nlohmann::detail::iter_impl it = defined_achievements.end(); - try { - it = defined_achievements_find(pchName); - } catch(...) { } - if (defined_achievements.end() == it) return false; - - if (pbAchieved) *pbAchieved = false; - if (punUnlockTime) *punUnlockTime = 0; - - try { - std::string pch_name = it->value("name", std::string()); - auto ach = user_achievements.find(pch_name); - if (user_achievements.end() != ach) { - if (pbAchieved) *pbAchieved = ach->value("earned", false); - if (punUnlockTime) *punUnlockTime = ach->value("earned_time", static_cast(0)); - } - } catch (...) {} - - return true; -} - - -// Store the current data on the server, will get a callback when set -// And one callback for every new achievement -// -// If the callback has a result of k_EResultInvalidParam, one or more stats -// uploaded has been rejected, either because they broke constraints -// or were out of date. In this case the server sends back updated values. -// The stats should be re-iterated to keep in sync. -bool StoreStats() -{ - PRINT_DEBUG("Steam_User_Stats::StoreStats\n"); - std::lock_guard lock(global_mutex); - - UserStatsStored_t data{}; - data.m_eResult = k_EResultOK; - data.m_nGameID = settings->get_local_game_id().ToUint64(); - callbacks->addCBResult(data.k_iCallback, &data, sizeof(data), 0.01); - - return true; -} - - -// Achievement / GroupAchievement metadata - -// Gets the icon of the achievement, which is a handle to be used in ISteamUtils::GetImageRGBA(), or 0 if none set. -// A return value of 0 may indicate we are still fetching data, and you can wait for the UserAchievementIconFetched_t callback -// which will notify you when the bits are ready. If the callback still returns zero, then there is no image set for the -// specified achievement. -int GetAchievementIcon( const char *pchName ) -{ - PRINT_DEBUG("TODO Steam_User_Stats::GetAchievementIcon\n"); - std::lock_guard lock(global_mutex); - if (!pchName) return 0; - - return 0; -} - -std::string get_achievement_icon_name( const char *pchName, bool pbAchieved ) -{ - std::lock_guard lock(global_mutex); - if (!pchName) return ""; - - nlohmann::detail::iter_impl it = defined_achievements.end(); - try { - it = defined_achievements_find(pchName); - } catch(...) { } - if (defined_achievements.end() == it) return ""; - - try { - if (pbAchieved) return it.value()["icon"].get(); - - std::string locked_icon = it.value().value("icon_gray", std::string()); - if (locked_icon.size()) return locked_icon; - else return it.value().value("icongray", std::string()); // old format - } catch (...) {} - - return ""; -} - - -// Get general attributes for an achievement. Accepts the following keys: -// - "name" and "desc" for retrieving the localized achievement name and description (returned in UTF8) -// - "hidden" for retrieving if an achievement is hidden (returns "0" when not hidden, "1" when hidden) -const char * GetAchievementDisplayAttribute( const char *pchName, const char *pchKey ) -{ - PRINT_DEBUG("Steam_User_Stats::GetAchievementDisplayAttribute [%s] [%s]\n", pchName, pchKey); - std::lock_guard lock(global_mutex); - - if (!pchName || !pchKey || !pchKey[0]) return ""; - - nlohmann::detail::iter_impl it = defined_achievements.end(); - try { - it = defined_achievements_find(pchName); - } catch(...) { } - if (defined_achievements.end() == it) return ""; - - if (strncmp(pchKey, "name", sizeof("name")) == 0) { - try { - return it.value()["displayName"].get_ptr()->c_str(); - } catch (...) {} - } else if (strncmp(pchKey, "desc", sizeof("desc")) == 0) { - try { - return it.value()["description"].get_ptr()->c_str(); - } catch (...) {} - } else if (strncmp(pchKey, "hidden", sizeof("hidden")) == 0) { - try { - return it.value()["hidden"].get_ptr()->c_str(); - } catch (...) {} - } - - return ""; -} - - -// Achievement progress - triggers an AchievementProgress callback, that is all. -// Calling this w/ N out of N progress will NOT set the achievement, the game must still do that. -bool IndicateAchievementProgress( const char *pchName, uint32 nCurProgress, uint32 nMaxProgress ) -{ - PRINT_DEBUG("Steam_User_Stats::IndicateAchievementProgress %s\n", pchName); - std::lock_guard lock(global_mutex); - - if (!pchName) return false; - if (nCurProgress >= nMaxProgress) return false; - auto ach_name = std::string(pchName); - - // find in achievements.json - nlohmann::detail::iter_impl it = defined_achievements.end(); - try { - it = defined_achievements_find(ach_name); - } catch(...) { } - if (defined_achievements.end() == it) return false; - - // get actual name from achievements.json - std::string actual_ach_name{}; - try { - actual_ach_name = it->value("name", std::string()); - } catch (...) { } - if (actual_ach_name.empty()) { // fallback - actual_ach_name = ach_name; - } - - // check if already achieved - bool achieved = false; - try { - auto ach = user_achievements.find(actual_ach_name); - if (ach != user_achievements.end()) { - achieved = ach->value("earned", false); - } - } catch (...) { } - if (achieved) return false; - - // save new progress - try { - user_achievements[actual_ach_name]["progress"] = nCurProgress; - user_achievements[actual_ach_name]["max_progress"] = nMaxProgress; - save_achievements(); - } catch (...) {} - - UserAchievementStored_t data{}; - data.m_nGameID = settings->get_local_game_id().ToUint64(); - data.m_bGroupAchievement = false; - data.m_nCurProgress = nCurProgress; - data.m_nMaxProgress = nMaxProgress; - ach_name.copy(data.m_rgchAchievementName, sizeof(data.m_rgchAchievementName) - 1); - - callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); - return true; -} - - -// Used for iterating achievements. In general games should not need these functions because they should have a -// list of existing achievements compiled into them -uint32 GetNumAchievements() -{ - PRINT_DEBUG("Steam_User_Stats::GetNumAchievements\n"); - std::lock_guard lock(global_mutex); - return (uint32)defined_achievements.size(); -} - -// Get achievement name iAchievement in [0,GetNumAchievements) -const char * GetAchievementName( uint32 iAchievement ) -{ - PRINT_DEBUG("Steam_User_Stats::GetAchievementName\n"); - std::lock_guard lock(global_mutex); - if (iAchievement >= sorted_achievement_names.size()) { - return ""; - } - - return sorted_achievement_names[iAchievement].c_str(); -} - - -// Friends stats & achievements - -// downloads stats for the user -// returns a UserStatsReceived_t received when completed -// if the other user has no stats, UserStatsReceived_t.m_eResult will be set to k_EResultFail -// these stats won't be auto-updated; you'll need to call RequestUserStats() again to refresh any data -STEAM_CALL_RESULT( UserStatsReceived_t ) -SteamAPICall_t RequestUserStats( CSteamID steamIDUser ) -{ - PRINT_DEBUG("Steam_User_Stats::RequestUserStats %llu\n", steamIDUser.ConvertToUint64()); - std::lock_guard lock(global_mutex); - - // Enable this to allow hot reload achievements status - //if (steamIDUser == settings->get_local_steam_id()) { - // load_achievements(); - //} - - - UserStatsReceived_t data; - data.m_nGameID = settings->get_local_game_id().ToUint64(); - data.m_eResult = k_EResultOK; - data.m_steamIDUser = steamIDUser; - return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data), 0.1); -} - - -// requests stat information for a user, usable after a successful call to RequestUserStats() -bool GetUserStat( CSteamID steamIDUser, const char *pchName, int32 *pData ) -{ - PRINT_DEBUG("Steam_User_Stats::GetUserStat %s %llu\n", pchName, steamIDUser.ConvertToUint64()); - std::lock_guard lock(global_mutex); - - if (pchName == nullptr) return false; - - if (steamIDUser == settings->get_local_steam_id()) { - GetStat(pchName, pData); - } else { - *pData = 0; - } - - return true; -} - -bool GetUserStat( CSteamID steamIDUser, const char *pchName, float *pData ) -{ - PRINT_DEBUG("Steam_User_Stats::GetUserStat %s %llu\n", pchName, steamIDUser.ConvertToUint64()); - std::lock_guard lock(global_mutex); - - if (pchName == nullptr) return false; - - if (steamIDUser == settings->get_local_steam_id()) { - GetStat(pchName, pData); - } else { - *pData = 0; - } - - return true; -} - -bool GetUserAchievement( CSteamID steamIDUser, const char *pchName, bool *pbAchieved ) -{ - PRINT_DEBUG("Steam_User_Stats::GetUserAchievement %s\n", pchName); - std::lock_guard lock(global_mutex); - - if (!pchName) return false; - - if (steamIDUser == settings->get_local_steam_id()) { - return GetAchievement(pchName, pbAchieved); - } - - return false; -} - -// See notes for GetAchievementAndUnlockTime above -bool GetUserAchievementAndUnlockTime( CSteamID steamIDUser, const char *pchName, bool *pbAchieved, uint32 *punUnlockTime ) -{ - PRINT_DEBUG("Steam_User_Stats::GetUserAchievementAndUnlockTime %s\n", pchName); - std::lock_guard lock(global_mutex); - - if (pchName == nullptr) return false; - - if (steamIDUser == settings->get_local_steam_id()) { - return GetAchievementAndUnlockTime(pchName, pbAchieved, punUnlockTime); - } - return false; -} - - -// Reset stats -bool ResetAllStats( bool bAchievementsToo ) -{ - PRINT_DEBUG("Steam_User_Stats::ResetAllStats\n"); - std::lock_guard lock(global_mutex); - //TODO - if (bAchievementsToo) { - std::for_each(user_achievements.begin(), user_achievements.end(), [](nlohmann::json& v) { - v["earned"] = false; - v["earned_time"] = 0; - }); - } - - return true; -} - - -// Leaderboard functions - -// asks the Steam back-end for a leaderboard by name, and will create it if it's not yet -// This call is asynchronous, with the result returned in LeaderboardFindResult_t -STEAM_CALL_RESULT(LeaderboardFindResult_t) -SteamAPICall_t FindOrCreateLeaderboard( const char *pchLeaderboardName, ELeaderboardSortMethod eLeaderboardSortMethod, ELeaderboardDisplayType eLeaderboardDisplayType ) -{ - PRINT_DEBUG("Steam_User_Stats::FindOrCreateLeaderboard %s\n", pchLeaderboardName); - std::lock_guard lock(global_mutex); - if (!pchLeaderboardName) { - LeaderboardFindResult_t data; - data.m_hSteamLeaderboard = 0; - data.m_bLeaderboardFound = 0; - return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); - } - - unsigned int leader = find_leaderboard(pchLeaderboardName); - if (!leader) { - struct Steam_Leaderboard leaderboard; - leaderboard.name = std::string(pchLeaderboardName); - leaderboard.sort_method = eLeaderboardSortMethod; - leaderboard.display_type = eLeaderboardDisplayType; - leaderboard.self_score.score = eLeaderboardSortMethod == k_ELeaderboardSortMethodAscending ? INT_MAX : INT_MIN; - - std::vector scores = load_leaderboard_scores(pchLeaderboardName); - for (auto &s : scores) { - if (s.steam_id == settings->get_local_steam_id()) { - leaderboard.self_score = s; - } - } - - leaderboards.push_back(leaderboard); - leader = leaderboards.size(); - } - - LeaderboardFindResult_t data; - data.m_hSteamLeaderboard = leader; - data.m_bLeaderboardFound = 1; - return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); -} - - -// as above, but won't create the leaderboard if it's not found -// This call is asynchronous, with the result returned in LeaderboardFindResult_t -STEAM_CALL_RESULT( LeaderboardFindResult_t ) -SteamAPICall_t FindLeaderboard( const char *pchLeaderboardName ) -{ - PRINT_DEBUG("Steam_User_Stats::FindLeaderboard %s\n", pchLeaderboardName); - std::lock_guard lock(global_mutex); - if (!pchLeaderboardName) { - LeaderboardFindResult_t data; - data.m_hSteamLeaderboard = 0; - data.m_bLeaderboardFound = 0; - return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); - } - - auto settings_Leaderboards = settings->getLeaderboards(); - if (settings_Leaderboards.count(pchLeaderboardName)) { - auto config = settings_Leaderboards[pchLeaderboardName]; - return FindOrCreateLeaderboard(pchLeaderboardName, config.sort_method, config.display_type); - } else if (settings->createUnknownLeaderboards()) { - return FindOrCreateLeaderboard(pchLeaderboardName, k_ELeaderboardSortMethodDescending, k_ELeaderboardDisplayTypeNumeric); - } else { - LeaderboardFindResult_t data; - data.m_hSteamLeaderboard = find_leaderboard(pchLeaderboardName);; - data.m_bLeaderboardFound = !!data.m_hSteamLeaderboard; - return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); - } -} - - -// returns the name of a leaderboard -const char * GetLeaderboardName( SteamLeaderboard_t hSteamLeaderboard ) -{ - PRINT_DEBUG("Steam_User_Stats::GetLeaderboardName\n"); - std::lock_guard lock(global_mutex); - - if (hSteamLeaderboard > leaderboards.size() || hSteamLeaderboard <= 0) return ""; - return leaderboards[hSteamLeaderboard - 1].name.c_str(); -} - - -// returns the total number of entries in a leaderboard, as of the last request -int GetLeaderboardEntryCount( SteamLeaderboard_t hSteamLeaderboard ) -{ - PRINT_DEBUG("Steam_User_Stats::GetLeaderboardEntryCount\n"); - return 0; -} - - -// returns the sort method of the leaderboard -ELeaderboardSortMethod GetLeaderboardSortMethod( SteamLeaderboard_t hSteamLeaderboard ) -{ - PRINT_DEBUG("Steam_User_Stats::GetLeaderboardSortMethod\n"); - std::lock_guard lock(global_mutex); - if (hSteamLeaderboard > leaderboards.size() || hSteamLeaderboard <= 0) return k_ELeaderboardSortMethodNone; - return leaderboards[hSteamLeaderboard - 1].sort_method; -} - - -// returns the display type of the leaderboard -ELeaderboardDisplayType GetLeaderboardDisplayType( SteamLeaderboard_t hSteamLeaderboard ) -{ - PRINT_DEBUG("Steam_User_Stats::GetLeaderboardDisplayType\n"); - std::lock_guard lock(global_mutex); - if (hSteamLeaderboard > leaderboards.size() || hSteamLeaderboard <= 0) return k_ELeaderboardDisplayTypeNone; - return leaderboards[hSteamLeaderboard - 1].display_type; -} - - -// Asks the Steam back-end for a set of rows in the leaderboard. -// This call is asynchronous, with the result returned in LeaderboardScoresDownloaded_t -// LeaderboardScoresDownloaded_t will contain a handle to pull the results from GetDownloadedLeaderboardEntries() (below) -// You can ask for more entries than exist, and it will return as many as do exist. -// k_ELeaderboardDataRequestGlobal requests rows in the leaderboard from the full table, with nRangeStart & nRangeEnd in the range [1, TotalEntries] -// k_ELeaderboardDataRequestGlobalAroundUser requests rows around the current user, nRangeStart being negate -// e.g. DownloadLeaderboardEntries( hLeaderboard, k_ELeaderboardDataRequestGlobalAroundUser, -3, 3 ) will return 7 rows, 3 before the user, 3 after -// k_ELeaderboardDataRequestFriends requests all the rows for friends of the current user -STEAM_CALL_RESULT( LeaderboardScoresDownloaded_t ) -SteamAPICall_t DownloadLeaderboardEntries( SteamLeaderboard_t hSteamLeaderboard, ELeaderboardDataRequest eLeaderboardDataRequest, int nRangeStart, int nRangeEnd ) -{ - PRINT_DEBUG("Steam_User_Stats::DownloadLeaderboardEntries %llu %i %i %i\n", hSteamLeaderboard, eLeaderboardDataRequest, nRangeStart, nRangeEnd); - std::lock_guard lock(global_mutex); - if (hSteamLeaderboard > leaderboards.size() || hSteamLeaderboard <= 0) return k_uAPICallInvalid; //might return callresult even if hSteamLeaderboard is invalid - - LeaderboardScoresDownloaded_t data; - data.m_hSteamLeaderboard = hSteamLeaderboard; - data.m_hSteamLeaderboardEntries = hSteamLeaderboard; - data.m_cEntryCount = leaderboards[hSteamLeaderboard - 1].self_score.steam_id.IsValid(); - return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); -} - -// as above, but downloads leaderboard entries for an arbitrary set of users - ELeaderboardDataRequest is k_ELeaderboardDataRequestUsers -// if a user doesn't have a leaderboard entry, they won't be included in the result -// a max of 100 users can be downloaded at a time, with only one outstanding call at a time -STEAM_METHOD_DESC(Downloads leaderboard entries for an arbitrary set of users - ELeaderboardDataRequest is k_ELeaderboardDataRequestUsers) -STEAM_CALL_RESULT( LeaderboardScoresDownloaded_t ) -SteamAPICall_t DownloadLeaderboardEntriesForUsers( SteamLeaderboard_t hSteamLeaderboard, - STEAM_ARRAY_COUNT_D(cUsers, Array of users to retrieve) CSteamID *prgUsers, int cUsers ) -{ - PRINT_DEBUG("Steam_User_Stats::DownloadLeaderboardEntriesForUsers %i %llu\n", cUsers, cUsers > 0 ? prgUsers[0].ConvertToUint64() : 0); - std::lock_guard lock(global_mutex); - if (hSteamLeaderboard > leaderboards.size() || hSteamLeaderboard <= 0) return k_uAPICallInvalid; //might return callresult even if hSteamLeaderboard is invalid - - bool get_for_current_id = false; - for (int i = 0; i < cUsers; ++i) { - if (prgUsers[i] == settings->get_local_steam_id()) { - get_for_current_id = true; - } - } - - LeaderboardScoresDownloaded_t data; - data.m_hSteamLeaderboard = hSteamLeaderboard; - data.m_hSteamLeaderboardEntries = hSteamLeaderboard; - data.m_cEntryCount = get_for_current_id && leaderboards[hSteamLeaderboard - 1].self_score.steam_id.IsValid(); - return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); -} - - -// Returns data about a single leaderboard entry -// use a for loop from 0 to LeaderboardScoresDownloaded_t::m_cEntryCount to get all the downloaded entries -// e.g. -// void OnLeaderboardScoresDownloaded( LeaderboardScoresDownloaded_t *pLeaderboardScoresDownloaded ) -// { -// for ( int index = 0; index < pLeaderboardScoresDownloaded->m_cEntryCount; index++ ) -// { -// LeaderboardEntry_t leaderboardEntry; -// int32 details[3]; // we know this is how many we've stored previously -// GetDownloadedLeaderboardEntry( pLeaderboardScoresDownloaded->m_hSteamLeaderboardEntries, index, &leaderboardEntry, details, 3 ); -// assert( leaderboardEntry.m_cDetails == 3 ); -// ... -// } -// once you've accessed all the entries, the data will be free'd, and the SteamLeaderboardEntries_t handle will become invalid -bool GetDownloadedLeaderboardEntry( SteamLeaderboardEntries_t hSteamLeaderboardEntries, int index, LeaderboardEntry_t *pLeaderboardEntry, int32 *pDetails, int cDetailsMax ) -{ - PRINT_DEBUG("Steam_User_Stats::GetDownloadedLeaderboardEntry\n"); - std::lock_guard lock(global_mutex); - if (hSteamLeaderboardEntries > leaderboards.size() || hSteamLeaderboardEntries <= 0) return false; - if (index > 0) return false; - - LeaderboardEntry_t entry = {}; - entry.m_steamIDUser = leaderboards[hSteamLeaderboardEntries - 1].self_score.steam_id; - entry.m_nGlobalRank = 1; - entry.m_nScore = leaderboards[hSteamLeaderboardEntries - 1].self_score.score; - for (int i = 0; i < leaderboards[hSteamLeaderboardEntries - 1].self_score.score_details.size() && i < cDetailsMax; ++i) { - pDetails[i] = leaderboards[hSteamLeaderboardEntries - 1].self_score.score_details[i]; - } - - *pLeaderboardEntry = entry; - return true; -} - - -// Uploads a user score to the Steam back-end. -// This call is asynchronous, with the result returned in LeaderboardScoreUploaded_t -// Details are extra game-defined information regarding how the user got that score -// pScoreDetails points to an array of int32's, cScoreDetailsCount is the number of int32's in the list -STEAM_CALL_RESULT( LeaderboardScoreUploaded_t ) -SteamAPICall_t UploadLeaderboardScore( SteamLeaderboard_t hSteamLeaderboard, ELeaderboardUploadScoreMethod eLeaderboardUploadScoreMethod, int32 nScore, const int32 *pScoreDetails, int cScoreDetailsCount ) -{ - PRINT_DEBUG("Steam_User_Stats::UploadLeaderboardScore %i\n", nScore); - std::lock_guard lock(global_mutex); - if (hSteamLeaderboard > leaderboards.size() || hSteamLeaderboard <= 0) return k_uAPICallInvalid; //TODO: might return callresult even if hSteamLeaderboard is invalid - - Steam_Leaderboard_Score score; - score.score = nScore; - score.steam_id = settings->get_local_steam_id(); - for (int i = 0; i < cScoreDetailsCount; ++i) { - score.score_details.push_back(pScoreDetails[i]); - } - - bool changed = false; - if (eLeaderboardUploadScoreMethod == k_ELeaderboardUploadScoreMethodKeepBest) { - if (leaderboards[hSteamLeaderboard - 1].sort_method == k_ELeaderboardSortMethodAscending - ? leaderboards[hSteamLeaderboard - 1].self_score.score >= score.score - : leaderboards[hSteamLeaderboard - 1].self_score.score <= score.score) { - leaderboards[hSteamLeaderboard - 1].self_score = score; - changed = true; - } - } else { - if (leaderboards[hSteamLeaderboard - 1].self_score.score != score.score) changed = true; - leaderboards[hSteamLeaderboard - 1].self_score = score; - } - - if (changed) { - save_leaderboard_score(&(leaderboards[hSteamLeaderboard - 1])); - } - - LeaderboardScoreUploaded_t data; - data.m_bSuccess = 1; //needs to be success or DOA6 freezes when uploading score. - //data.m_bSuccess = 0; - data.m_hSteamLeaderboard = hSteamLeaderboard; - data.m_nScore = nScore; - data.m_bScoreChanged = changed; - data.m_nGlobalRankNew = 1; - data.m_nGlobalRankPrevious = 0; - return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); -} - -SteamAPICall_t UploadLeaderboardScore( SteamLeaderboard_t hSteamLeaderboard, int32 nScore, int32 *pScoreDetails, int cScoreDetailsCount ) -{ - PRINT_DEBUG("UploadLeaderboardScore old\n"); - return UploadLeaderboardScore(hSteamLeaderboard, k_ELeaderboardUploadScoreMethodKeepBest, nScore, pScoreDetails, cScoreDetailsCount); -} - - -// Attaches a piece of user generated content the user's entry on a leaderboard. -// hContent is a handle to a piece of user generated content that was shared using ISteamUserRemoteStorage::FileShare(). -// This call is asynchronous, with the result returned in LeaderboardUGCSet_t. -STEAM_CALL_RESULT( LeaderboardUGCSet_t ) -SteamAPICall_t AttachLeaderboardUGC( SteamLeaderboard_t hSteamLeaderboard, UGCHandle_t hUGC ) -{ - PRINT_DEBUG("Steam_User_Stats::AttachLeaderboardUGC\n"); - std::lock_guard lock(global_mutex); - LeaderboardUGCSet_t data = {}; - if (hSteamLeaderboard > leaderboards.size() || hSteamLeaderboard <= 0) { - data.m_eResult = k_EResultFail; - } else { - data.m_eResult = k_EResultOK; - } - - data.m_hSteamLeaderboard = hSteamLeaderboard; - return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); -} - - -// Retrieves the number of players currently playing your game (online + offline) -// This call is asynchronous, with the result returned in NumberOfCurrentPlayers_t -STEAM_CALL_RESULT( NumberOfCurrentPlayers_t ) -SteamAPICall_t GetNumberOfCurrentPlayers() -{ - PRINT_DEBUG("Steam_User_Stats::GetNumberOfCurrentPlayers\n"); - std::lock_guard lock(global_mutex); - NumberOfCurrentPlayers_t data; - data.m_bSuccess = 1; - data.m_cPlayers = 69; - return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); -} - - -// Requests that Steam fetch data on the percentage of players who have received each achievement -// for the game globally. -// This call is asynchronous, with the result returned in GlobalAchievementPercentagesReady_t. -STEAM_CALL_RESULT( GlobalAchievementPercentagesReady_t ) -SteamAPICall_t RequestGlobalAchievementPercentages() -{ - PRINT_DEBUG("Steam_User_Stats::RequestGlobalAchievementPercentages\n"); - return 0; -} - - -// Get the info on the most achieved achievement for the game, returns an iterator index you can use to fetch -// the next most achieved afterwards. Will return -1 if there is no data on achievement -// percentages (ie, you haven't called RequestGlobalAchievementPercentages and waited on the callback). -int GetMostAchievedAchievementInfo( char *pchName, uint32 unNameBufLen, float *pflPercent, bool *pbAchieved ) -{ - PRINT_DEBUG("Steam_User_Stats::GetMostAchievedAchievementInfo\n"); - return -1; -} - - -// Get the info on the next most achieved achievement for the game. Call this after GetMostAchievedAchievementInfo or another -// GetNextMostAchievedAchievementInfo call passing the iterator from the previous call. Returns -1 after the last -// achievement has been iterated. -int GetNextMostAchievedAchievementInfo( int iIteratorPrevious, char *pchName, uint32 unNameBufLen, float *pflPercent, bool *pbAchieved ) -{ - PRINT_DEBUG("Steam_User_Stats::GetNextMostAchievedAchievementInfo\n"); - return -1; -} - - -// Returns the percentage of users who have achieved the specified achievement. -bool GetAchievementAchievedPercent( const char *pchName, float *pflPercent ) -{ - PRINT_DEBUG("Steam_User_Stats::GetAchievementAchievedPercent\n"); - return false; -} - - -// Requests global stats data, which is available for stats marked as "aggregated". -// This call is asynchronous, with the results returned in GlobalStatsReceived_t. -// nHistoryDays specifies how many days of day-by-day history to retrieve in addition -// to the overall totals. The limit is 60. -STEAM_CALL_RESULT( GlobalStatsReceived_t ) -SteamAPICall_t RequestGlobalStats( int nHistoryDays ) -{ - PRINT_DEBUG("Steam_User_Stats::RequestGlobalStats %i\n", nHistoryDays); - std::lock_guard lock(global_mutex); - GlobalStatsReceived_t data{}; - data.m_nGameID = settings->get_local_game_id().ToUint64(); - data.m_eResult = k_EResultOK; - return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); -} - - -// Gets the lifetime totals for an aggregated stat -bool GetGlobalStat( const char *pchStatName, int64 *pData ) -{ - PRINT_DEBUG("Steam_User_Stats::GetGlobalStat %s\n", pchStatName); - return false; -} - -bool GetGlobalStat( const char *pchStatName, double *pData ) -{ - PRINT_DEBUG("Steam_User_Stats::GetGlobalStat %s\n", pchStatName); - return false; -} - - -// Gets history for an aggregated stat. pData will be filled with daily values, starting with today. -// So when called, pData[0] will be today, pData[1] will be yesterday, and pData[2] will be two days ago, -// etc. cubData is the size in bytes of the pubData buffer. Returns the number of -// elements actually set. -int32 GetGlobalStatHistory( const char *pchStatName, STEAM_ARRAY_COUNT(cubData) int64 *pData, uint32 cubData ) -{ - PRINT_DEBUG("Steam_User_Stats::GetGlobalStatHistory int64 %s\n", pchStatName); - return 0; -} - -int32 GetGlobalStatHistory( const char *pchStatName, STEAM_ARRAY_COUNT(cubData) double *pData, uint32 cubData ) -{ - PRINT_DEBUG("Steam_User_Stats::GetGlobalStatHistory double %s\n", pchStatName); - return 0; -} - -// For achievements that have related Progress stats, use this to query what the bounds of that progress are. -// You may want this info to selectively call IndicateAchievementProgress when appropriate milestones of progress -// have been made, to show a progress notification to the user. -bool GetAchievementProgressLimits( const char *pchName, int32 *pnMinProgress, int32 *pnMaxProgress ) -{ - PRINT_DEBUG("Steam_User_Stats::GetAchievementProgressLimits int\n"); - return false; -} - -bool GetAchievementProgressLimits( const char *pchName, float *pfMinProgress, float *pfMaxProgress ) -{ - PRINT_DEBUG("Steam_User_Stats::GetAchievementProgressLimits float\n"); - return false; -} + Steam_User_Stats(Settings *settings, class Networking *network, Local_Storage *local_storage, class SteamCallResults *callback_results, class SteamCallBacks *callbacks, class RunEveryRunCB *run_every_runcb, Steam_Overlay* overlay); + ~Steam_User_Stats(); + // Ask the server to send down this user's data and achievements for this game + STEAM_CALL_BACK( UserStatsReceived_t ) + bool RequestCurrentStats(); + + + // Data accessors + bool GetStat( const char *pchName, int32 *pData ); + + bool GetStat( const char *pchName, float *pData ); + + + // Set / update data + bool SetStat( const char *pchName, int32 nData ); + + bool SetStat( const char *pchName, float fData ); + + bool UpdateAvgRateStat( const char *pchName, float flCountThisSession, double dSessionLength ); + + + // Achievement flag accessors + bool GetAchievement( const char *pchName, bool *pbAchieved ); + + bool SetAchievement( const char *pchName ); + + bool ClearAchievement( const char *pchName ); + + + // Get the achievement status, and the time it was unlocked if unlocked. + // If the return value is true, but the unlock time is zero, that means it was unlocked before Steam + // began tracking achievement unlock times (December 2009). Time is seconds since January 1, 1970. + bool GetAchievementAndUnlockTime( const char *pchName, bool *pbAchieved, uint32 *punUnlockTime ); + + + // Store the current data on the server, will get a callback when set + // And one callback for every new achievement + // + // If the callback has a result of k_EResultInvalidParam, one or more stats + // uploaded has been rejected, either because they broke constraints + // or were out of date. In this case the server sends back updated values. + // The stats should be re-iterated to keep in sync. + bool StoreStats(); + + + // Achievement / GroupAchievement metadata + + // Gets the icon of the achievement, which is a handle to be used in ISteamUtils::GetImageRGBA(), or 0 if none set. + // A return value of 0 may indicate we are still fetching data, and you can wait for the UserAchievementIconFetched_t callback + // which will notify you when the bits are ready. If the callback still returns zero, then there is no image set for the + // specified achievement. + int GetAchievementIcon( const char *pchName ); + + std::string get_achievement_icon_name( const char *pchName, bool pbAchieved ); + + + // Get general attributes for an achievement. Accepts the following keys: + // - "name" and "desc" for retrieving the localized achievement name and description (returned in UTF8) + // - "hidden" for retrieving if an achievement is hidden (returns "0" when not hidden, "1" when hidden) + const char * GetAchievementDisplayAttribute( const char *pchName, const char *pchKey ); + + + // Achievement progress - triggers an AchievementProgress callback, that is all. + // Calling this w/ N out of N progress will NOT set the achievement, the game must still do that. + bool IndicateAchievementProgress( const char *pchName, uint32 nCurProgress, uint32 nMaxProgress ); + + + // Used for iterating achievements. In general games should not need these functions because they should have a + // list of existing achievements compiled into them + uint32 GetNumAchievements(); + + // Get achievement name iAchievement in [0,GetNumAchievements) + const char * GetAchievementName( uint32 iAchievement ); + + + // Friends stats & achievements + + // downloads stats for the user + // returns a UserStatsReceived_t received when completed + // if the other user has no stats, UserStatsReceived_t.m_eResult will be set to k_EResultFail + // these stats won't be auto-updated; you'll need to call RequestUserStats() again to refresh any data + STEAM_CALL_RESULT( UserStatsReceived_t ) + SteamAPICall_t RequestUserStats( CSteamID steamIDUser ); + + + // requests stat information for a user, usable after a successful call to RequestUserStats() + bool GetUserStat( CSteamID steamIDUser, const char *pchName, int32 *pData ); + + bool GetUserStat( CSteamID steamIDUser, const char *pchName, float *pData ); + + bool GetUserAchievement( CSteamID steamIDUser, const char *pchName, bool *pbAchieved ); + + // See notes for GetAchievementAndUnlockTime above + bool GetUserAchievementAndUnlockTime( CSteamID steamIDUser, const char *pchName, bool *pbAchieved, uint32 *punUnlockTime ); + + + // Reset stats + bool ResetAllStats( bool bAchievementsToo ); + + + // Leaderboard functions + + // asks the Steam back-end for a leaderboard by name, and will create it if it's not yet + // This call is asynchronous, with the result returned in LeaderboardFindResult_t + STEAM_CALL_RESULT(LeaderboardFindResult_t) + SteamAPICall_t FindOrCreateLeaderboard( const char *pchLeaderboardName, ELeaderboardSortMethod eLeaderboardSortMethod, ELeaderboardDisplayType eLeaderboardDisplayType ); + + + // as above, but won't create the leaderboard if it's not found + // This call is asynchronous, with the result returned in LeaderboardFindResult_t + STEAM_CALL_RESULT( LeaderboardFindResult_t ) + SteamAPICall_t FindLeaderboard( const char *pchLeaderboardName ); + + + // returns the name of a leaderboard + const char * GetLeaderboardName( SteamLeaderboard_t hSteamLeaderboard ); + + + // returns the total number of entries in a leaderboard, as of the last request + int GetLeaderboardEntryCount( SteamLeaderboard_t hSteamLeaderboard ); + + + // returns the sort method of the leaderboard + ELeaderboardSortMethod GetLeaderboardSortMethod( SteamLeaderboard_t hSteamLeaderboard ); + + + // returns the display type of the leaderboard + ELeaderboardDisplayType GetLeaderboardDisplayType( SteamLeaderboard_t hSteamLeaderboard ); + + + // Asks the Steam back-end for a set of rows in the leaderboard. + // This call is asynchronous, with the result returned in LeaderboardScoresDownloaded_t + // LeaderboardScoresDownloaded_t will contain a handle to pull the results from GetDownloadedLeaderboardEntries() (below) + // You can ask for more entries than exist, and it will return as many as do exist. + // k_ELeaderboardDataRequestGlobal requests rows in the leaderboard from the full table, with nRangeStart & nRangeEnd in the range [1, TotalEntries] + // k_ELeaderboardDataRequestGlobalAroundUser requests rows around the current user, nRangeStart being negate + // e.g. DownloadLeaderboardEntries( hLeaderboard, k_ELeaderboardDataRequestGlobalAroundUser, -3, 3 ) will return 7 rows, 3 before the user, 3 after + // k_ELeaderboardDataRequestFriends requests all the rows for friends of the current user + STEAM_CALL_RESULT( LeaderboardScoresDownloaded_t ) + SteamAPICall_t DownloadLeaderboardEntries( SteamLeaderboard_t hSteamLeaderboard, ELeaderboardDataRequest eLeaderboardDataRequest, int nRangeStart, int nRangeEnd ); + + // as above, but downloads leaderboard entries for an arbitrary set of users - ELeaderboardDataRequest is k_ELeaderboardDataRequestUsers + // if a user doesn't have a leaderboard entry, they won't be included in the result + // a max of 100 users can be downloaded at a time, with only one outstanding call at a time + STEAM_METHOD_DESC(Downloads leaderboard entries for an arbitrary set of users - ELeaderboardDataRequest is k_ELeaderboardDataRequestUsers) + STEAM_CALL_RESULT( LeaderboardScoresDownloaded_t ) + SteamAPICall_t DownloadLeaderboardEntriesForUsers( SteamLeaderboard_t hSteamLeaderboard, + STEAM_ARRAY_COUNT_D(cUsers, Array of users to retrieve) CSteamID *prgUsers, int cUsers ); + + + // Returns data about a single leaderboard entry + // use a for loop from 0 to LeaderboardScoresDownloaded_t::m_cEntryCount to get all the downloaded entries + // e.g. + // void OnLeaderboardScoresDownloaded( LeaderboardScoresDownloaded_t *pLeaderboardScoresDownloaded ) + // { + // for ( int index = 0; index < pLeaderboardScoresDownloaded->m_cEntryCount; index++ ) + // { + // LeaderboardEntry_t leaderboardEntry; + // int32 details[3]; // we know this is how many we've stored previously + // GetDownloadedLeaderboardEntry( pLeaderboardScoresDownloaded->m_hSteamLeaderboardEntries, index, &leaderboardEntry, details, 3 ); + // assert( leaderboardEntry.m_cDetails == 3 ); + // ... + // } + // once you've accessed all the entries, the data will be free'd, and the SteamLeaderboardEntries_t handle will become invalid + bool GetDownloadedLeaderboardEntry( SteamLeaderboardEntries_t hSteamLeaderboardEntries, int index, LeaderboardEntry_t *pLeaderboardEntry, int32 *pDetails, int cDetailsMax ); + + + // Uploads a user score to the Steam back-end. + // This call is asynchronous, with the result returned in LeaderboardScoreUploaded_t + // Details are extra game-defined information regarding how the user got that score + // pScoreDetails points to an array of int32's, cScoreDetailsCount is the number of int32's in the list + STEAM_CALL_RESULT( LeaderboardScoreUploaded_t ) + SteamAPICall_t UploadLeaderboardScore( SteamLeaderboard_t hSteamLeaderboard, ELeaderboardUploadScoreMethod eLeaderboardUploadScoreMethod, int32 nScore, const int32 *pScoreDetails, int cScoreDetailsCount ); + + SteamAPICall_t UploadLeaderboardScore( SteamLeaderboard_t hSteamLeaderboard, int32 nScore, int32 *pScoreDetails, int cScoreDetailsCount ); + + + // Attaches a piece of user generated content the user's entry on a leaderboard. + // hContent is a handle to a piece of user generated content that was shared using ISteamUserRemoteStorage::FileShare(). + // This call is asynchronous, with the result returned in LeaderboardUGCSet_t. + STEAM_CALL_RESULT( LeaderboardUGCSet_t ) + SteamAPICall_t AttachLeaderboardUGC( SteamLeaderboard_t hSteamLeaderboard, UGCHandle_t hUGC ); + + + // Retrieves the number of players currently playing your game (online + offline) + // This call is asynchronous, with the result returned in NumberOfCurrentPlayers_t + STEAM_CALL_RESULT( NumberOfCurrentPlayers_t ) + SteamAPICall_t GetNumberOfCurrentPlayers(); + + + // Requests that Steam fetch data on the percentage of players who have received each achievement + // for the game globally. + // This call is asynchronous, with the result returned in GlobalAchievementPercentagesReady_t. + STEAM_CALL_RESULT( GlobalAchievementPercentagesReady_t ) + SteamAPICall_t RequestGlobalAchievementPercentages(); + + + // Get the info on the most achieved achievement for the game, returns an iterator index you can use to fetch + // the next most achieved afterwards. Will return -1 if there is no data on achievement + // percentages (ie, you haven't called RequestGlobalAchievementPercentages and waited on the callback). + int GetMostAchievedAchievementInfo( char *pchName, uint32 unNameBufLen, float *pflPercent, bool *pbAchieved ); + + + // Get the info on the next most achieved achievement for the game. Call this after GetMostAchievedAchievementInfo or another + // GetNextMostAchievedAchievementInfo call passing the iterator from the previous call. Returns -1 after the last + // achievement has been iterated. + int GetNextMostAchievedAchievementInfo( int iIteratorPrevious, char *pchName, uint32 unNameBufLen, float *pflPercent, bool *pbAchieved ); + + + // Returns the percentage of users who have achieved the specified achievement. + bool GetAchievementAchievedPercent( const char *pchName, float *pflPercent ); + + + // Requests global stats data, which is available for stats marked as "aggregated". + // This call is asynchronous, with the results returned in GlobalStatsReceived_t. + // nHistoryDays specifies how many days of day-by-day history to retrieve in addition + // to the overall totals. The limit is 60. + STEAM_CALL_RESULT( GlobalStatsReceived_t ) + SteamAPICall_t RequestGlobalStats( int nHistoryDays ); + + + // Gets the lifetime totals for an aggregated stat + bool GetGlobalStat( const char *pchStatName, int64 *pData ); + + bool GetGlobalStat( const char *pchStatName, double *pData ); + + + // Gets history for an aggregated stat. pData will be filled with daily values, starting with today. + // So when called, pData[0] will be today, pData[1] will be yesterday, and pData[2] will be two days ago, + // etc. cubData is the size in bytes of the pubData buffer. Returns the number of + // elements actually set. + int32 GetGlobalStatHistory( const char *pchStatName, STEAM_ARRAY_COUNT(cubData) int64 *pData, uint32 cubData ); + + int32 GetGlobalStatHistory( const char *pchStatName, STEAM_ARRAY_COUNT(cubData) double *pData, uint32 cubData ); + + // For achievements that have related Progress stats, use this to query what the bounds of that progress are. + // You may want this info to selectively call IndicateAchievementProgress when appropriate milestones of progress + // have been made, to show a progress notification to the user. + bool GetAchievementProgressLimits( const char *pchName, int32 *pnMinProgress, int32 *pnMaxProgress ); + + bool GetAchievementProgressLimits( const char *pchName, float *pfMinProgress, float *pfMaxProgress ); }; diff --git a/dll/net.proto b/dll/net.proto index ac49e59b..7e5b36f9 100644 --- a/dll/net.proto +++ b/dll/net.proto @@ -215,6 +215,59 @@ message Steam_Messages { } } +message GameServerStats_Messages { + // --- baisc definitions + message StatInfo { + enum Stat_Type { + STAT_TYPE_INT = 0; + STAT_TYPE_FLOAT = 1; + STAT_TYPE_AVGRATE = 2; + } + message AvgStatInfo { + float count_this_session = 1; + double session_length = 2; + } + + Stat_Type stat_type = 1; + oneof stat_value { + float value_float = 2; + int32 value_int = 3; + } + optional AvgStatInfo value_avg = 4; // only set when type != INT + } + message AchievementInfo { + bool achieved = 1; + } + + // --- requests & responses objects + // this is used when updating stats, from server or user, bi-directional + message AllStats { + map user_stats = 1; + map user_achievements = 2; + } + // sent from server as a request, response sent by the user + message InitialAllStats { + uint64 steam_api_call = 1; + + // optional because the server send doesn't send any data, just steam api call id + optional AllStats all_data = 2; + } + // Request_: from Steam_GameServerStats + // Response_: from Steam_User_Stats + enum Types { + Request_AllUserStats = 0; + Response_AllUserStats = 1; + + UpdateUserStats = 2; // sent by both sides + } + + Types type = 1; + oneof data_messages { + InitialAllStats initial_user_stats = 2; + AllStats update_user_stats = 3; + } +} + message Common_Message { uint64 source_id = 1; // SteamID64 of the sender uint64 dest_id = 2; // SteamID64 of the target receiver @@ -232,6 +285,7 @@ message Common_Message { Networking_Sockets networking_sockets = 13; Steam_Messages steam_messages = 14; Networking_Messages networking_messages = 15; + GameServerStats_Messages gameserver_stats_messages = 16; } uint32 source_ip = 128; diff --git a/dll/network.cpp b/dll/network.cpp index 2e0a7302..20d531cc 100644 --- a/dll/network.cpp +++ b/dll/network.cpp @@ -584,6 +584,12 @@ void Networking::do_callbacks_message(Common_Message *msg) PRINT_DEBUG("Networking has_networking_messages\n"); run_callbacks(CALLBACK_ID_NETWORKING_MESSAGES, msg); } + + if (msg->has_gameserver_stats_messages()) { + PRINT_DEBUG("Networking has_gameserver_stats\n"); + run_callbacks(CALLBACK_ID_GAMESERVER_STATS, msg); + } + } bool Networking::handle_tcp(Common_Message *msg, struct TCP_Socket &socket) @@ -1181,7 +1187,7 @@ bool Networking::sendToIPPort(Common_Message *msg, uint32 ip, uint16 port, bool { bool is_local_ip = ((ip >> 24) == 0x7F); uint32_t local_ip = getIP(ids.front()); - PRINT_DEBUG("sendToIPPort %X %u %X\n", ip, is_local_ip, local_ip); + PRINT_DEBUG("Networking::sendToIPPort %X %u %X\n", ip, is_local_ip, local_ip); //TODO: actually send to ip/port for (auto &conn: connections) { if (ntohl(conn.tcp_ip_port.ip) == ip || (is_local_ip && ntohl(conn.tcp_ip_port.ip) == local_ip)) { @@ -1215,9 +1221,9 @@ bool Networking::sendTo(Common_Message *msg, bool reliable, Connection *conn) bool ret = false; CSteamID dest_id((uint64)msg->dest_id()); if (std::find(ids.begin(), ids.end(), dest_id) != ids.end()) { - PRINT_DEBUG("Sending to self\n"); + PRINT_DEBUG("Networking sending to self\n"); if (!conn) { - PRINT_DEBUG("local send\n"); + PRINT_DEBUG("Networking local send\n"); local_send.push_back(*msg); ret = true; } @@ -1278,7 +1284,11 @@ bool Networking::sendToAll(Common_Message *msg, bool reliable) void Networking::run_callbacks(Callback_Ids id, Common_Message *msg) { for (auto &cb : callbacks[id].callbacks) { - if (cb.steam_id.ConvertToUint64() == 0 || msg->dest_id() == 0 || cb.steam_id.ConvertToUint64() == msg->dest_id()) { + uint64 callback_allowed_steamid = cb.steam_id.ConvertToUint64(); + uint64 message_destination_steamid = msg->dest_id(); + if (callback_allowed_steamid == 0 || // callback wants to receive all messages (callback for broadcast) + message_destination_steamid == 0 || // message was broadcasted to all (broadcast message) + callback_allowed_steamid == message_destination_steamid) { // callback destination is the same as the message destination cb.message_callback(cb.object, msg); } } @@ -1305,7 +1315,7 @@ bool Networking::setCallback(Callback_Ids id, CSteamID steam_id, void (*message_ { if (id >= CALLBACK_IDS_MAX) return false; - struct Network_Callback nc; + struct Network_Callback nc{}; nc.message_callback = message_callback; nc.object = object; nc.steam_id = steam_id; @@ -1314,6 +1324,24 @@ bool Networking::setCallback(Callback_Ids id, CSteamID steam_id, void (*message_ return true; } +void Networking::rmCallback(Callback_Ids id, CSteamID steam_id, void (*message_callback)(void *object, Common_Message *msg), void *object) +{ + if (id >= CALLBACK_IDS_MAX) return; + + auto &target_cb = callbacks[id].callbacks; + auto itrm = std::remove_if( + target_cb.begin(), + target_cb.end(), + [=, &steam_id](const struct Network_Callback &item) { + return item.message_callback == message_callback && + item.object == object && + item.steam_id == steam_id; + } + ); + + target_cb.erase(itrm, target_cb.end()); +} + uint32 Networking::getOwnIP() { return own_ip; diff --git a/dll/settings_parser.cpp b/dll/settings_parser.cpp index 2d17d079..9b62c1e1 100644 --- a/dll/settings_parser.cpp +++ b/dll/settings_parser.cpp @@ -666,13 +666,13 @@ static void parse_stats(class Settings *settings_client, Settings *settings_serv try { if (stat_type == "float") { - config.type = Stat_Type::STAT_TYPE_FLOAT; + config.type = GameServerStats_Messages::StatInfo::STAT_TYPE_FLOAT; config.default_value_float = std::stof(stat_default_value); } else if (stat_type == "int") { - config.type = Stat_Type::STAT_TYPE_INT; + config.type = GameServerStats_Messages::StatInfo::STAT_TYPE_INT; config.default_value_int = std::stol(stat_default_value); } else if (stat_type == "avgrate") { - config.type = Stat_Type::STAT_TYPE_AVGRATE; + config.type = GameServerStats_Messages::StatInfo::STAT_TYPE_AVGRATE; config.default_value_float = std::stof(stat_default_value); } else { PRINT_DEBUG("Error adding stat %s, type %s isn't valid\n", stat_name.c_str(), stat_type.c_str()); diff --git a/dll/steam_client.cpp b/dll/steam_client.cpp index d5a5ebbc..9eb036dd 100644 --- a/dll/steam_client.cpp +++ b/dll/steam_client.cpp @@ -81,7 +81,7 @@ Steam_Client::Steam_Client() steam_matchmaking = new Steam_Matchmaking(settings_client, network, callback_results_client, callbacks_client, run_every_runcb); steam_matchmaking_servers = new Steam_Matchmaking_Servers(settings_client, network); - steam_user_stats = new Steam_User_Stats(settings_client, local_storage, callback_results_client, callbacks_client, steam_overlay); + steam_user_stats = new Steam_User_Stats(settings_client, network, local_storage, callback_results_client, callbacks_client, run_every_runcb, steam_overlay); steam_apps = new Steam_Apps(settings_client, callback_results_client); steam_networking = new Steam_Networking(settings_client, network, callbacks_client, run_every_runcb); steam_remote_storage = new Steam_Remote_Storage(settings_client, ugc_bridge, local_storage, callback_results_client); @@ -110,7 +110,7 @@ Steam_Client::Steam_Client() PRINT_DEBUG("client init gameserver\n"); steam_gameserver = new Steam_GameServer(settings_server, network, callbacks_server); steam_gameserver_utils = new Steam_Utils(settings_server, callback_results_server, steam_overlay); - steam_gameserverstats = new Steam_GameServerStats(settings_server, network, callback_results_server, callbacks_server); + steam_gameserverstats = new Steam_GameServerStats(settings_server, network, callback_results_server, callbacks_server, run_every_runcb); steam_gameserver_networking = new Steam_Networking(settings_server, network, callbacks_server, run_every_runcb); steam_gameserver_http = new Steam_HTTP(settings_server, network, callback_results_server, callbacks_server); steam_gameserver_inventory = new Steam_Inventory(settings_server, callback_results_server, callbacks_server, run_every_runcb, local_storage); @@ -128,7 +128,7 @@ Steam_Client::Steam_Client() gameserver_has_ipv6_functions = false; last_cb_run = 0; - PRINT_DEBUG("Steam_Client init end ----------\n"); + PRINT_DEBUG("Steam_Client init end *********\n"); } Steam_Client::~Steam_Client() @@ -1914,7 +1914,7 @@ void Steam_Client::RunCallbacks(bool runClientCB, bool runGameserverCB, bool run callbacks_client->runCallBacks(); last_cb_run = std::chrono::duration_cast>(std::chrono::system_clock::now().time_since_epoch()).count(); - PRINT_DEBUG("Steam_Client::RunCallbacks done ------------------------------------------------------\n"); + PRINT_DEBUG("Steam_Client::RunCallbacks done ******************************************************\n"); } void Steam_Client::DestroyAllInterfaces() diff --git a/dll/steam_gameserver.cpp b/dll/steam_gameserver.cpp index 8014dff9..e03219dc 100644 --- a/dll/steam_gameserver.cpp +++ b/dll/steam_gameserver.cpp @@ -56,8 +56,12 @@ bool Steam_GameServer::InitGameServer( uint32 unIP, uint16 usGamePort, uint16 us std::string version(pchVersionString); version.erase(std::remove(version.begin(), version.end(), ' '), version.end()); version.erase(std::remove(version.begin(), version.end(), '.'), version.end()); + PRINT_DEBUG("Steam_GameServer::InitGameServer version trimmed '%s'\n", version.c_str()); + try { - server_data.set_version(std::stoi(version)); + auto ver = std::stoul(version); + server_data.set_version(ver); + PRINT_DEBUG("Steam_GameServer::InitGameServer set version to %lu\n", ver); } catch (...) { PRINT_DEBUG("Steam_GameServer::InitGameServer: not a number: %s\n", pchVersionString); server_data.set_version(0); diff --git a/dll/steam_gameserverstats.cpp b/dll/steam_gameserverstats.cpp index c000dcd2..bc562af2 100644 --- a/dll/steam_gameserverstats.cpp +++ b/dll/steam_gameserverstats.cpp @@ -17,14 +17,90 @@ #include "dll/steam_gameserverstats.h" -Steam_GameServerStats::Steam_GameServerStats(class Settings *settings, class Networking *network, class SteamCallResults *callback_results, class SteamCallBacks *callbacks) +// TODO not sure what's the real value +#define PENDING_RequestUserStats_TIMEOUT 7.0 + + +void Steam_GameServerStats::steam_gameserverstats_network_callback(void *object, Common_Message *msg) +{ + // PRINT_DEBUG("Steam_GameServerStats::steam_gameserverstats_network_callback\n"); + + auto steam_gameserverstats = (Steam_GameServerStats *)object; + steam_gameserverstats->network_callback(msg); +} + +void Steam_GameServerStats::steam_gameserverstats_run_every_runcb(void *object) +{ + // PRINT_DEBUG("Steam_GameServerStats::steam_gameserverstats_run_every_runcb\n"); + + auto steam_gameserverstats = (Steam_GameServerStats *)object; + steam_gameserverstats->steam_run_callback(); +} + +Steam_GameServerStats::CachedStat* Steam_GameServerStats::find_stat(CSteamID steamIDUser, const std::string &key) +{ + auto it_data = all_users_data.find(steamIDUser.ConvertToUint64()); + if (all_users_data.end() == it_data) return {}; // no user + + auto it_stat = std::find_if( + it_data->second.stats.begin(), it_data->second.stats.end(), + [&key](std::pair &item) { + const std::string &name = item.first; + return key.size() == name.size() && + std::equal( + name.begin(), name.end(), key.begin(), + [](char a, char b) { return std::tolower(a) == std::tolower(b); } + ); + } + ); + + if (it_data->second.stats.end() == it_stat) return {}; // no stat + + return &it_stat->second; +} + +Steam_GameServerStats::CachedAchievement* Steam_GameServerStats::find_ach(CSteamID steamIDUser, const std::string &key) +{ + auto it_data = all_users_data.find(steamIDUser.ConvertToUint64()); + if (all_users_data.end() == it_data) return {}; // no user + + auto it_ach = std::find_if( + it_data->second.achievements.begin(), it_data->second.achievements.end(), + [&key](std::pair &item) { + const std::string &name = item.first; + return key.size() == name.size() && + std::equal( + name.begin(), name.end(), key.begin(), + [](char a, char b) { return std::tolower(a) == std::tolower(b); } + ); + } + ); + + if (it_data->second.achievements.end() == it_ach) return {}; // no user + + return &it_ach->second; +} + +Steam_GameServerStats::Steam_GameServerStats(class Settings *settings, class Networking *network, class SteamCallResults *callback_results, class SteamCallBacks *callbacks, class RunEveryRunCB *run_every_runcb) { this->settings = settings; this->network = network; this->callback_results = callback_results; this->callbacks = callbacks; + this->run_every_runcb = run_every_runcb; + + this->network->setCallback(CALLBACK_ID_GAMESERVER_STATS, settings->get_local_steam_id(), &Steam_GameServerStats::steam_gameserverstats_network_callback, this); + this->run_every_runcb->add(&Steam_GameServerStats::steam_gameserverstats_run_every_runcb, this); + } +Steam_GameServerStats::~Steam_GameServerStats() +{ + this->network->rmCallback(CALLBACK_ID_GAMESERVER_STATS, settings->get_local_steam_id(), &Steam_GameServerStats::steam_gameserverstats_network_callback, this); + this->run_every_runcb->remove(&Steam_GameServerStats::steam_gameserverstats_run_every_runcb, this); +} + + // downloads stats for the user // returns a GSStatsReceived_t callback when completed // if the user has no stats, GSStatsReceived_t.m_eResult will be set to k_EResultFail @@ -33,33 +109,78 @@ Steam_GameServerStats::Steam_GameServerStats(class Settings *settings, class Net STEAM_CALL_RESULT( GSStatsReceived_t ) SteamAPICall_t Steam_GameServerStats::RequestUserStats( CSteamID steamIDUser ) { - PRINT_DEBUG("Steam_GameServerStats::RequestUserStats\n"); + PRINT_DEBUG("Steam_GameServerStats::RequestUserStats %llu\n", (uint64)steamIDUser.ConvertToUint64()); std::lock_guard lock(global_mutex); - GSStatsReceived_t data{}; - data.m_eResult = k_EResultFail;//k_EResultOK; - data.m_steamIDUser = steamIDUser; - return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); + struct RequestAllStats new_request{}; + new_request.created = std::chrono::high_resolution_clock::now(); + new_request.steamAPICall = callback_results->reserveCallResult(); + new_request.steamIDUser = steamIDUser; + + pending_RequestUserStats.push_back(new_request); + + auto initial_stats_msg = new GameServerStats_Messages::InitialAllStats(); + initial_stats_msg->set_steam_api_call(new_request.steamAPICall); + + auto gameserverstats_messages = new GameServerStats_Messages(); + gameserverstats_messages->set_type(GameServerStats_Messages::Request_AllUserStats); + gameserverstats_messages->set_allocated_initial_user_stats(initial_stats_msg); + + Common_Message msg{}; + // https://protobuf.dev/reference/cpp/cpp-generated/#string + // set_allocated_xxx() takes ownership of the allocated object, no need to delete + msg.set_allocated_gameserver_stats_messages(gameserverstats_messages); + msg.set_source_id(settings->get_local_steam_id().ConvertToUint64()); + msg.set_dest_id(new_request.steamIDUser.ConvertToUint64()); + network->sendTo(&msg, true); + + return new_request.steamAPICall; } // requests stat information for a user, usable after a successful call to RequestUserStats() bool Steam_GameServerStats::GetUserStat( CSteamID steamIDUser, const char *pchName, int32 *pData ) { - PRINT_DEBUG("Steam_GameServerStats::GetUserStat\n"); - return false; + PRINT_DEBUG("Steam_GameServerStats::GetUserStat %llu '%s' %p\n", (uint64)steamIDUser.ConvertToUint64(), pchName, pData); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + + auto stat = find_stat(steamIDUser, pchName); + if (!stat) return false; + if (stat->stat.stat_type() != GameServerStats_Messages::StatInfo::STAT_TYPE_INT) return false; + + if (pData) *pData = stat->stat.value_int(); + return true; } bool Steam_GameServerStats::GetUserStat( CSteamID steamIDUser, const char *pchName, float *pData ) { - PRINT_DEBUG("Steam_GameServerStats::GetUserStat\n"); - return false; + PRINT_DEBUG("Steam_GameServerStats::GetUserStat %llu '%s' %p\n", (uint64)steamIDUser.ConvertToUint64(), pchName, pData); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + + auto stat = find_stat(steamIDUser, pchName); + if (!stat) return false; + if (stat->stat.stat_type() == GameServerStats_Messages::StatInfo::STAT_TYPE_INT) return false; + + if (pData) *pData = stat->stat.value_float(); + return true; } bool Steam_GameServerStats::GetUserAchievement( CSteamID steamIDUser, const char *pchName, bool *pbAchieved ) { - PRINT_DEBUG("Steam_GameServerStats::GetUserAchievement\n"); - return false; + PRINT_DEBUG("Steam_GameServerStats::GetUserAchievement %llu '%s' %p\n", (uint64)steamIDUser.ConvertToUint64(), pchName, pbAchieved); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + + auto ach = find_ach(steamIDUser, pchName); + if (!ach) return false; + if (pbAchieved) *pbAchieved = ach->ach.achieved(); + + return true; } @@ -69,33 +190,98 @@ bool Steam_GameServerStats::GetUserAchievement( CSteamID steamIDUser, const char // Set the IP range of your official servers on the Steamworks page bool Steam_GameServerStats::SetUserStat( CSteamID steamIDUser, const char *pchName, int32 nData ) { - PRINT_DEBUG("Steam_GameServerStats::SetUserStat\n"); - return false; + PRINT_DEBUG("Steam_GameServerStats::SetUserStat %llu '%s' %i\n", (uint64)steamIDUser.ConvertToUint64(), pchName, nData); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + + auto stat = find_stat(steamIDUser, pchName); + if (!stat) return false; + if (stat->stat.stat_type() != GameServerStats_Messages::StatInfo::STAT_TYPE_INT) return false; + if (stat->stat.value_int() == nData) return true; // don't waste time + + stat->dirty = true; + stat->stat.set_value_int(nData); + return true; } bool Steam_GameServerStats::SetUserStat( CSteamID steamIDUser, const char *pchName, float fData ) { - PRINT_DEBUG("Steam_GameServerStats::SetUserStat\n"); - return false; + PRINT_DEBUG("Steam_GameServerStats::SetUserStat %llu '%s' %f\n", (uint64)steamIDUser.ConvertToUint64(), pchName, fData); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + + auto stat = find_stat(steamIDUser, pchName); + if (!stat) return false; + if (stat->stat.stat_type() == GameServerStats_Messages::StatInfo::STAT_TYPE_INT) return false; + if (stat->stat.value_float() == fData) return true; // don't waste time + + stat->dirty = true; + stat->stat.set_value_float(fData); // we set the float field in case it's float or avg + return true; } bool Steam_GameServerStats::UpdateUserAvgRateStat( CSteamID steamIDUser, const char *pchName, float flCountThisSession, double dSessionLength ) { - PRINT_DEBUG("Steam_GameServerStats::UpdateUserAvgRateStat\n"); - return false; + PRINT_DEBUG("Steam_GameServerStats::UpdateUserAvgRateStat %llu '%s'\n", (uint64)steamIDUser.ConvertToUint64(), pchName); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + + auto stat = find_stat(steamIDUser, pchName); + if (!stat) return false; + if (stat->stat.stat_type() == GameServerStats_Messages::StatInfo::STAT_TYPE_INT) return false; + // don't waste time + if (stat->stat.has_value_avg() && + stat->stat.value_avg().count_this_session() == flCountThisSession && + stat->stat.value_avg().session_length() == dSessionLength) { + return true; + } + + stat->dirty = true; + + // https://protobuf.dev/reference/cpp/cpp-generated/#string + // set_allocated_xxx() takes ownership of the allocated object, no need to delete + auto avg_info = new GameServerStats_Messages::StatInfo::AvgStatInfo(); + avg_info->set_count_this_session(flCountThisSession); + avg_info->set_session_length(dSessionLength); + stat->stat.set_allocated_value_avg(avg_info); + + return true; } bool Steam_GameServerStats::SetUserAchievement( CSteamID steamIDUser, const char *pchName ) { - PRINT_DEBUG("Steam_GameServerStats::SetUserAchievement\n"); - return false; + PRINT_DEBUG("Steam_GameServerStats::SetUserAchievement %llu '%s'\n", (uint64)steamIDUser.ConvertToUint64(), pchName); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + + auto ach = find_ach(steamIDUser, pchName); + if (!ach) return false; + if (ach->ach.achieved() == true) return true; // don't waste time + + ach->dirty = true; + ach->ach.set_achieved(true); + return true; } bool Steam_GameServerStats::ClearUserAchievement( CSteamID steamIDUser, const char *pchName ) { - PRINT_DEBUG("Steam_GameServerStats::ClearUserAchievement\n"); - return false; + PRINT_DEBUG("Steam_GameServerStats::ClearUserAchievement %llu '%s'\n", (uint64)steamIDUser.ConvertToUint64(), pchName); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + + auto ach = find_ach(steamIDUser, pchName); + if (!ach) return false; + if (ach->ach.achieved() == false) return true; // don't waste time + + ach->dirty = true; + ach->ach.set_achieved(false); + return true; } @@ -108,11 +294,237 @@ bool Steam_GameServerStats::ClearUserAchievement( CSteamID steamIDUser, const ch STEAM_CALL_RESULT( GSStatsStored_t ) SteamAPICall_t Steam_GameServerStats::StoreUserStats( CSteamID steamIDUser ) { + // it's not necessary to send all data here PRINT_DEBUG("Steam_GameServerStats::StoreUserStats\n"); std::lock_guard lock(global_mutex); - GSStatsStored_t data; - data.m_eResult = k_EResultOK; + GSStatsStored_t data{}; + + if (all_users_data.count(steamIDUser.ConvertToUint64())) { + data.m_eResult = EResult::k_EResultOK; + } else { + data.m_eResult = EResult::k_EResultFail; + } data.m_steamIDUser = steamIDUser; - return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); + return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data), 0.01); +} + + + +// --- steam callbacks + +void Steam_GameServerStats::remove_timedout_userstats_requests() +{ + if (pending_RequestUserStats.empty()) return; + + // send all pending RequestUserStats() requests + for (auto &pendReq : pending_RequestUserStats) { + if (check_timedout(pendReq.created, PENDING_RequestUserStats_TIMEOUT)) { + pendReq.timeout = true; + + GSStatsReceived_t data{}; + data.m_eResult = k_EResultTimeout; + data.m_steamIDUser = pendReq.steamIDUser; + callback_results->addCallResult(pendReq.steamAPICall, data.k_iCallback, &data, sizeof(data)); + + PRINT_DEBUG( + "Steam_GameServerStats::steam_run_callback RequestUserStats timeout, %llu\n", + pendReq.steamIDUser.ConvertToUint64() + ); + } + } + + // remove all timedout requests + auto itrm = std::remove_if( + pending_RequestUserStats.begin(), pending_RequestUserStats.end(), + [](const struct RequestAllStats &item) { return item.timeout; } + ); + pending_RequestUserStats.erase(itrm, pending_RequestUserStats.end()); +} + +void Steam_GameServerStats::collect_and_send_updated_user_stats() +{ + std::map updated_users_data{}; // new data to send + + // collect new data + for (auto &this_user : all_users_data) { // foreach user + uint64 user_steamid = this_user.first; + + // collect changed stats + for (auto &user_stat : this_user.second.stats) { + if (user_stat.second.dirty) { + user_stat.second.dirty = false; + updated_users_data[user_steamid].stats[user_stat.first] = user_stat.second; + // clear this to avoid sending it to the user next time + if (user_stat.second.stat.has_value_avg()) user_stat.second.stat.clear_value_avg(); + } + } + + // collect changed achievements + for (auto &user_ach : this_user.second.achievements) { + if (user_ach.second.dirty) { + user_ach.second.dirty = false; + updated_users_data[user_steamid].achievements[user_ach.first] = user_ach.second; + } + } + } + + // send new user stats + for (auto &user_new_data : updated_users_data) { // foreach user + uint64 user_steamid = user_new_data.first; + const auto &new_data = user_new_data.second; + auto updated_stats_msg = new GameServerStats_Messages::AllStats(); + + // copy new stats + auto &updated_stats_map = *updated_stats_msg->mutable_user_stats(); + for (auto &new_stat : new_data.stats) { + updated_stats_map[new_stat.first] = new_stat.second.stat; + + } + + // copy new achievements + auto &updated_achs_map = *updated_stats_msg->mutable_user_achievements(); + for (auto &new_ach : new_data.achievements) { + updated_achs_map[new_ach.first] = new_ach.second.ach; + + } + + auto gameserverstats_msg = new GameServerStats_Messages(); + gameserverstats_msg->set_type(GameServerStats_Messages::UpdateUserStats); + gameserverstats_msg->set_allocated_update_user_stats(updated_stats_msg); + + Common_Message msg{}; + // https://protobuf.dev/reference/cpp/cpp-generated/#string + // set_allocated_xxx() takes ownership of the allocated object, no need to delete + msg.set_allocated_gameserver_stats_messages(gameserverstats_msg); + msg.set_source_id(settings->get_local_steam_id().ConvertToUint64()); + msg.set_dest_id(user_steamid); + network->sendTo(&msg, true); + + PRINT_DEBUG( + "Steam_GameServerStats::collect_and_send_updated_user_stats server sent updated stats %llu: %zu stats, %zu achievements\n", + user_steamid, updated_stats_msg->user_stats().size(), updated_stats_msg->user_achievements().size() + ); + } + +} + +void Steam_GameServerStats::steam_run_callback() +{ + remove_timedout_userstats_requests(); + collect_and_send_updated_user_stats(); +} + + + +// --- networking callbacks + +void Steam_GameServerStats::network_callback_initial_stats(Common_Message *msg) +{ + uint64 user_steamid = msg->source_id(); + + PRINT_DEBUG("Steam_GameServerStats::network_callback_initial_stats player sent all their stats %llu\n", user_steamid); + if (!msg->gameserver_stats_messages().has_initial_user_stats() || + !msg->gameserver_stats_messages().initial_user_stats().has_all_data()) { + PRINT_DEBUG("Steam_GameServerStats::network_callback_initial_stats error empty msg\n"); + return; + } + + const auto &new_data = msg->gameserver_stats_messages().initial_user_stats(); + + // find this pending request + auto it = std::find_if( + pending_RequestUserStats.begin(), pending_RequestUserStats.end(), + [=](const RequestAllStats &item) { + return item.steamAPICall == new_data.steam_api_call() && + item.steamIDUser == user_steamid; + } + ); + if (pending_RequestUserStats.end() == it) { // timeout and already removed + PRINT_DEBUG("Steam_GameServerStats::network_callback_initial_stats error got all player stats but pending request timedout and removed\n"); + return; + } + + // remove this pending request + pending_RequestUserStats.erase(it); + + // copy new stats + auto ¤t_stats = all_users_data[user_steamid].stats; + current_stats.clear(); + for (const auto &new_stat : new_data.all_data().user_stats()) { + current_stats[new_stat.first].stat = new_stat.second; + } + + // copy new achievements + auto ¤t_achievements = all_users_data[user_steamid].achievements; + current_achievements.clear(); + for (const auto &new_ach : new_data.all_data().user_achievements()) { + current_achievements[new_ach.first].ach = new_ach.second; + } + + GSStatsReceived_t data{}; + data.m_eResult = EResult::k_EResultOK; + data.m_steamIDUser = user_steamid; + callback_results->addCallResult(it->steamAPICall, data.k_iCallback, &data, sizeof(data)); + + PRINT_DEBUG( + "Steam_GameServerStats::network_callback_initial_stats server got all player stats %llu: %zu stats, %zu achievements\n", + user_steamid, all_users_data[user_steamid].stats.size(), all_users_data[user_steamid].achievements.size() + ); + + +} + +void Steam_GameServerStats::network_callback_updated_stats(Common_Message *msg) +{ + uint64 user_steamid = msg->source_id(); + + PRINT_DEBUG("Steam_GameServerStats::network_callback_updated_stats player sent updated stats %llu\n", user_steamid); + if (!msg->gameserver_stats_messages().has_update_user_stats()) { + PRINT_DEBUG("Steam_GameServerStats::network_callback_updated_stats error empty msg\n"); + return; + } + + auto ¤t_user_data = all_users_data[user_steamid]; + auto &new_user_data =msg->gameserver_stats_messages().update_user_stats(); + + // update stats + for (auto &new_stat : new_user_data.user_stats()) { + auto ¤t_stat = current_user_data.stats[new_stat.first]; + current_stat.dirty = false; + current_stat.stat = new_stat.second; + } + + // update achievements + for (auto &new_ach : new_user_data.user_achievements()) { + auto ¤t_ach = current_user_data.achievements[new_ach.first]; + current_ach.dirty = false; + current_ach.ach = new_ach.second; + } + + PRINT_DEBUG( + "Steam_GameServerStats::network_callback got updated user stats %llu: %zu stats, %zu achievements\n", + user_steamid, new_user_data.user_stats().size(), new_user_data.user_achievements().size() + ); +} + +// only triggered when we have a message +void Steam_GameServerStats::network_callback(Common_Message *msg) +{ + switch (msg->gameserver_stats_messages().type()) + { + // user sent all their stats + case GameServerStats_Messages::Response_AllUserStats: + network_callback_initial_stats(msg); + break; + + // user has updated/new stats + case GameServerStats_Messages::UpdateUserStats: + network_callback_updated_stats(msg); + break; + + default: + PRINT_DEBUG("Steam_GameServerStats::network_callback unhandled type %i\n", (int)msg->gameserver_stats_messages().type()); + break; + } } diff --git a/dll/steam_user_stats.cpp b/dll/steam_user_stats.cpp new file mode 100644 index 00000000..defbe073 --- /dev/null +++ b/dll/steam_user_stats.cpp @@ -0,0 +1,1527 @@ +/* Copyright (C) 2019 Mr Goldberg + This file is part of the Goldberg Emulator + + The Goldberg Emulator is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + The Goldberg Emulator is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the Goldberg Emulator; if not, see + . */ + +#include "dll/steam_user_stats.h" + + + +unsigned int Steam_User_Stats::find_leaderboard(std::string name) +{ + unsigned index = 1; + for (auto &leaderboard : leaderboards) { + if (leaderboard.name == name) return index; + ++index; + } + + return 0; +} + +nlohmann::detail::iter_impl Steam_User_Stats::defined_achievements_find(const std::string &key) +{ + return std::find_if( + defined_achievements.begin(), defined_achievements.end(), + [&key](const nlohmann::json& item) { + const std::string &name = static_cast( item.value("name", std::string()) ); + return key.size() == name.size() && + std::equal( + name.begin(), name.end(), key.begin(), + [](char a, char b) { return std::tolower(a) == std::tolower(b); } + ); + } + ); +} + +void Steam_User_Stats::load_achievements_db() +{ + std::string file_path = Local_Storage::get_game_settings_path() + achievements_user_file; + local_storage->load_json(file_path, defined_achievements); +} + +void Steam_User_Stats::load_achievements() +{ + local_storage->load_json_file("", achievements_user_file, user_achievements); +} + +void Steam_User_Stats::save_achievements() +{ + local_storage->write_json_file("", achievements_user_file, user_achievements); +} + +void Steam_User_Stats::save_leaderboard_score(Steam_Leaderboard *leaderboard) +{ + std::vector output; + uint64_t steam_id = leaderboard->self_score.steam_id.ConvertToUint64(); + output.push_back(steam_id & 0xFFFFFFFF); + output.push_back(steam_id >> 32); + + output.push_back(leaderboard->self_score.score); + output.push_back(leaderboard->self_score.score_details.size()); + for (auto &s : leaderboard->self_score.score_details) { + output.push_back(s); + } + + std::string leaderboard_name = common_helpers::ascii_to_lowercase(leaderboard->name); + local_storage->store_data(Local_Storage::leaderboard_storage_folder, leaderboard_name, (char* )output.data(), sizeof(uint32_t) * output.size()); +} + +std::vector Steam_User_Stats::load_leaderboard_scores(std::string name) +{ + std::vector out; + + std::string leaderboard_name = common_helpers::ascii_to_lowercase(name); + unsigned size = local_storage->file_size(Local_Storage::leaderboard_storage_folder, leaderboard_name); + if (size == 0 || (size % sizeof(uint32_t)) != 0) return out; + + std::vector output(size / sizeof(uint32_t)); + if (local_storage->get_data(Local_Storage::leaderboard_storage_folder, leaderboard_name, (char* )output.data(), size) != size) return out; + + unsigned i = 0; + while (true) { + if ((i + 4) > output.size()) break; + + Steam_Leaderboard_Score score; + score.steam_id = CSteamID((uint64)output[i] + (((uint64)output[i + 1]) << 32)); + i += 2; + score.score = output[i]; + i += 1; + unsigned count = output[i]; + i += 1; + + if ((i + count) > output.size()) break; + + for (unsigned j = 0; j < count; ++j) { + score.score_details.push_back(output[i]); + i += 1; + } + + PRINT_DEBUG("Steam_User_Stats::loaded leaderboard score %llu %u\n", score.steam_id.ConvertToUint64(), score.score); + out.push_back(score); + } + + return out; +} + +std::string Steam_User_Stats::get_value_for_language(nlohmann::json &json, std::string key, std::string language) +{ + auto x = json.find(key); + if (x == json.end()) return ""; + if (x.value().is_string()) { + return x.value().get(); + } else if (x.value().is_object()) { + auto l = x.value().find(language); + if (l != x.value().end()) { + return l.value().get(); + } + + l = x.value().find("english"); + if (l != x.value().end()) { + return l.value().get(); + } + + l = x.value().begin(); + if (l != x.value().end()) { + if (l.key() == "token") { + std::string token_value = l.value().get(); + l++; + if (l != x.value().end()) { + return l.value().get(); + } + + return token_value; + } + + return l.value().get(); + } + } + + return ""; +} + + +// change stats/achievements without sending back to server +Steam_User_Stats::InternalSetResult Steam_User_Stats::set_stat_internal( const char *pchName, int32 nData ) +{ + PRINT_DEBUG("Steam_User_Stats::set_stat_internal '%s' = %i\n", pchName, nData); + std::lock_guard lock(global_mutex); + Steam_User_Stats::InternalSetResult result{}; + + if (!pchName) return result; + std::string stat_name = common_helpers::ascii_to_lowercase(pchName); + + const auto &stats_config = settings->getStats(); + auto stats_data = stats_config.find(stat_name); + if (stats_config.end() == stats_data) return result; + if (stats_data->second.type != GameServerStats_Messages::StatInfo::STAT_TYPE_INT) return result; + + result.internal_name = stat_name; + result.current_val = nData; + + auto cached_stat = stats_cache_int.find(stat_name); + if (cached_stat != stats_cache_int.end()) { + if (cached_stat->second == nData) { + result.success = true; + return result; + } + } + + auto stat_trigger = achievement_stat_trigger.find(stat_name); + if (stat_trigger != achievement_stat_trigger.end()) { + for (auto &t : stat_trigger->second) { + if (t.check_triggered(nData)) { + set_achievement_internal(t.name.c_str()); + } + } + } + + if (local_storage->store_data(Local_Storage::stats_storage_folder, stat_name, (char* )&nData, sizeof(nData)) == sizeof(nData)) { + stats_cache_int[stat_name] = nData; + result.success = true; + result.notify_server = true; + return result; + } + + return result; +} + +Steam_User_Stats::InternalSetResult> Steam_User_Stats::set_stat_internal( const char *pchName, float fData ) +{ + PRINT_DEBUG("Steam_User_Stats::set_stat_internal '%s' = %f\n", pchName, fData); + std::lock_guard lock(global_mutex); + Steam_User_Stats::InternalSetResult> result{}; + + if (!pchName) return result; + std::string stat_name = common_helpers::ascii_to_lowercase(pchName); + + const auto &stats_config = settings->getStats(); + auto stats_data = stats_config.find(stat_name); + if (stats_config.end() == stats_data) return result; + if (stats_data->second.type == GameServerStats_Messages::StatInfo::STAT_TYPE_INT) return result; + + result.internal_name = stat_name; + result.current_val.first = stats_data->second.type; + result.current_val.second = fData; + + auto cached_stat = stats_cache_float.find(stat_name); + if (cached_stat != stats_cache_float.end()) { + if (cached_stat->second == fData) { + result.success = true; + return result; + } + } + + auto stat_trigger = achievement_stat_trigger.find(stat_name); + if (stat_trigger != achievement_stat_trigger.end()) { + for (auto &t : stat_trigger->second) { + if (t.check_triggered(fData)) { + set_achievement_internal(t.name.c_str()); + } + } + } + + if (local_storage->store_data(Local_Storage::stats_storage_folder, stat_name, (char* )&fData, sizeof(fData)) == sizeof(fData)) { + stats_cache_float[stat_name] = fData; + result.success = true; + result.notify_server = true; + return result; + } + + return result; +} + +Steam_User_Stats::InternalSetResult> Steam_User_Stats::update_avg_rate_stat_internal( const char *pchName, float flCountThisSession, double dSessionLength ) +{ + PRINT_DEBUG("Steam_User_Stats::update_avg_rate_stat_internal %s\n", pchName); + std::lock_guard lock(global_mutex); + Steam_User_Stats::InternalSetResult> result{}; + + if (!pchName) return result; + std::string stat_name = common_helpers::ascii_to_lowercase(pchName); + + const auto &stats_config = settings->getStats(); + auto stats_data = stats_config.find(stat_name); + if (stats_config.end() == stats_data) return result; + if (stats_data->second.type == GameServerStats_Messages::StatInfo::STAT_TYPE_INT) return result; + + result.internal_name = stat_name; + + char data[sizeof(float) + sizeof(float) + sizeof(double)]; + int read_data = local_storage->get_data(Local_Storage::stats_storage_folder, stat_name, (char* )data, sizeof(*data)); + float oldcount = 0; + double oldsessionlength = 0; + if (read_data == sizeof(data)) { + memcpy(&oldcount, data + sizeof(float), sizeof(oldcount)); + memcpy(&oldsessionlength, data + sizeof(float) + sizeof(double), sizeof(oldsessionlength)); + } + + oldcount += flCountThisSession; + oldsessionlength += dSessionLength; + + float average = oldcount / oldsessionlength; + memcpy(data, &average, sizeof(average)); + memcpy(data + sizeof(float), &oldcount, sizeof(oldcount)); + memcpy(data + sizeof(float) * 2, &oldsessionlength, sizeof(oldsessionlength)); + + result.current_val.first = stats_data->second.type; + result.current_val.second = average; + + if (local_storage->store_data(Local_Storage::stats_storage_folder, stat_name, data, sizeof(data)) == sizeof(data)) { + stats_cache_float[stat_name] = average; + result.success = true; + result.notify_server = true; + return result; + } + + return result; +} + +Steam_User_Stats::InternalSetResult Steam_User_Stats::set_achievement_internal( const char *pchName ) +{ + PRINT_DEBUG("Steam_User_Stats::set_achievement_internal '%s'\n", pchName); + std::lock_guard lock(global_mutex); + Steam_User_Stats::InternalSetResult result{}; + + if (!pchName) return result; + + if (settings->achievement_bypass) { + result.success = true; + return result; + } + + nlohmann::detail::iter_impl it = defined_achievements.end(); + try { + it = defined_achievements_find(pchName); + } catch(...) { } + if (defined_achievements.end() == it) return result; + + result.current_val = true; + result.internal_name = pchName; + result.success = true; + + try { + std::string pch_name = it->value("name", std::string()); + + result.internal_name = pch_name; + + auto ach = user_achievements.find(pch_name); + if (user_achievements.end() == ach || ach->value("earned", false) == false) { + user_achievements[pch_name]["earned"] = true; + user_achievements[pch_name]["earned_time"] = + std::chrono::duration_cast>(std::chrono::system_clock::now().time_since_epoch()).count(); + + save_achievements(); + + result.notify_server = true; + + if(!settings->disable_overlay) overlay->AddAchievementNotification(it.value()); + + } + } catch (...) {} + + return result; +} + +Steam_User_Stats::InternalSetResult Steam_User_Stats::clear_achievement_internal( const char *pchName ) +{ + PRINT_DEBUG("Steam_User_Stats::clear_achievement_internal '%s'\n", pchName); + std::lock_guard lock(global_mutex); + Steam_User_Stats::InternalSetResult result{}; + + if (!pchName) return result; + + nlohmann::detail::iter_impl it = defined_achievements.end(); + try { + it = defined_achievements_find(pchName); + } catch(...) { } + if (defined_achievements.end() == it) return result; + + result.current_val = false; + result.internal_name = pchName; + result.success = true; + + try { + std::string pch_name = it->value("name", std::string()); + + result.internal_name = pch_name; + + auto ach = user_achievements.find(pch_name); + // assume "earned" is true in case the json obj exists, but the key is absent + // assume "earned_time" is UINT32_MAX in case the json obj exists, but the key is absent + if (user_achievements.end() == ach || + ach->value("earned", true) == true || + ach->value("earned_time", static_cast(UINT32_MAX)) == UINT32_MAX) { + + user_achievements[pch_name]["earned"] = false; + user_achievements[pch_name]["earned_time"] = static_cast(0); + save_achievements(); + + result.notify_server = true; + + } + } catch (...) {} + + return result; +} + + +void Steam_User_Stats::steam_user_stats_network_callback(void *object, Common_Message *msg) +{ + // PRINT_DEBUG("Steam_GameServerStats::steam_gameserverstats_network_callback\n"); + + auto steam_gameserverstats = (Steam_User_Stats *)object; + steam_gameserverstats->network_callback(msg); +} + +void Steam_User_Stats::steam_user_stats_run_every_runcb(void *object) +{ + // PRINT_DEBUG("Steam_GameServerStats::steam_gameserverstats_run_every_runcb\n"); + + auto steam_gameserverstats = (Steam_User_Stats *)object; + steam_gameserverstats->steam_run_callback(); +} + + +Steam_User_Stats::Steam_User_Stats(Settings *settings, class Networking *network, Local_Storage *local_storage, class SteamCallResults *callback_results, class SteamCallBacks *callbacks, class RunEveryRunCB *run_every_runcb, Steam_Overlay* overlay): + settings(settings), + network(network), + local_storage(local_storage), + callback_results(callback_results), + callbacks(callbacks), + defined_achievements(nlohmann::json::object()), + user_achievements(nlohmann::json::object()), + run_every_runcb(run_every_runcb), + overlay(overlay) +{ + load_achievements_db(); // achievements db + load_achievements(); // achievements per user + + auto x = defined_achievements.begin(); + while (x != defined_achievements.end()) { + + if (!x->contains("name")) { + x = defined_achievements.erase(x); + } else { + ++x; + } + } + + for (auto & it : defined_achievements) { + try { + std::string name = static_cast(it["name"]); + sorted_achievement_names.push_back(name); + if (user_achievements.find(name) == user_achievements.end()) { + user_achievements[name]["earned"] = false; + user_achievements[name]["earned_time"] = static_cast(0); + } + + achievement_trigger trig; + trig.name = name; + trig.value_operation = static_cast(it["progress"]["value"]["operation"]); + std::string stat_name = common_helpers::ascii_to_lowercase(static_cast(it["progress"]["value"]["operand1"])); + trig.min_value = static_cast(it["progress"]["min_val"]); + trig.max_value = static_cast(it["progress"]["max_val"]); + achievement_stat_trigger[stat_name].push_back(trig); + } catch (...) {} + + try { + it["hidden"] = std::to_string(it["hidden"].get()); + } catch (...) {} + + it["displayName"] = get_value_for_language(it, "displayName", settings->get_language()); + it["description"] = get_value_for_language(it, "description", settings->get_language()); + } + + //TODO: not sure if the sort is actually case insensitive, ach names seem to be treated by steam as case insensitive so I assume they are. + //need to find a game with achievements of different case names to confirm + std::sort(sorted_achievement_names.begin(), sorted_achievement_names.end(), [](const std::string lhs, const std::string rhs){ + const auto result = std::mismatch(lhs.cbegin(), lhs.cend(), rhs.cbegin(), rhs.cend(), [](const unsigned char lhs, const unsigned char rhs){return std::tolower(lhs) == std::tolower(rhs);}); + return result.second != rhs.cend() && (result.first == lhs.cend() || std::tolower(*result.first) < std::tolower(*result.second));} + ); + + this->network->setCallback(CALLBACK_ID_GAMESERVER_STATS, settings->get_local_steam_id(), &Steam_User_Stats::steam_user_stats_network_callback, this); + this->run_every_runcb->add(&Steam_User_Stats::steam_user_stats_run_every_runcb, this); +} + +Steam_User_Stats::~Steam_User_Stats() +{ + this->network->rmCallback(CALLBACK_ID_GAMESERVER_STATS, settings->get_local_steam_id(), &Steam_User_Stats::steam_user_stats_network_callback, this); + this->run_every_runcb->remove(&Steam_User_Stats::steam_user_stats_run_every_runcb, this); +} + +// Ask the server to send down this user's data and achievements for this game +STEAM_CALL_BACK( UserStatsReceived_t ) +bool Steam_User_Stats::RequestCurrentStats() +{ + PRINT_DEBUG("Steam_User_Stats::RequestCurrentStats\n"); + std::lock_guard lock(global_mutex); + + UserStatsReceived_t data{}; + data.m_nGameID = settings->get_local_game_id().ToUint64(); + data.m_eResult = k_EResultOK; + data.m_steamIDUser = settings->get_local_steam_id(); + callbacks->addCBResult(data.k_iCallback, &data, sizeof(data), 0.1); + return true; +} + + +// Data accessors +bool Steam_User_Stats::GetStat( const char *pchName, int32 *pData ) +{ + PRINT_DEBUG("Steam_User_Stats::GetStat '%s' %p\n", pchName, pData); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + std::string stat_name = common_helpers::ascii_to_lowercase(pchName); + + const auto &stats_config = settings->getStats(); + auto stats_data = stats_config.find(stat_name); + if (stats_config.end() == stats_data) return false; + if (stats_data->second.type != GameServerStats_Messages::StatInfo::STAT_TYPE_INT) return false; + + auto cached_stat = stats_cache_int.find(stat_name); + if (cached_stat != stats_cache_int.end()) { + if (pData) *pData = cached_stat->second; + return true; + } + + int32 output = 0; + int read_data = local_storage->get_data(Local_Storage::stats_storage_folder, stat_name, (char* )&output, sizeof(output)); + if (read_data == sizeof(int32)) { + stats_cache_int[stat_name] = output; + if (pData) *pData = output; + return true; + } + + stats_cache_int[stat_name] = stats_data->second.default_value_int; + if (pData) *pData = stats_data->second.default_value_int; + return true; +} + +bool Steam_User_Stats::GetStat( const char *pchName, float *pData ) +{ + PRINT_DEBUG("Steam_User_Stats::GetStat '%s' %p\n", pchName, pData); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + std::string stat_name = common_helpers::ascii_to_lowercase(pchName); + + const auto &stats_config = settings->getStats(); + auto stats_data = stats_config.find(stat_name); + if (stats_config.end() == stats_data) return false; + if (stats_data->second.type == GameServerStats_Messages::StatInfo::STAT_TYPE_INT) return false; + + auto cached_stat = stats_cache_float.find(stat_name); + if (cached_stat != stats_cache_float.end()) { + if (pData) *pData = cached_stat->second; + return true; + } + + float output = 0.0; + int read_data = local_storage->get_data(Local_Storage::stats_storage_folder, stat_name, (char* )&output, sizeof(output)); + if (read_data == sizeof(float)) { + stats_cache_float[stat_name] = output; + if (pData) *pData = output; + return true; + } + + stats_cache_float[stat_name] = stats_data->second.default_value_float; + if (pData) *pData = stats_data->second.default_value_float; + return true; +} + + +// Set / update data +bool Steam_User_Stats::SetStat( const char *pchName, int32 nData ) +{ + PRINT_DEBUG("Steam_User_Stats::SetStat '%s' = %i\n", pchName, nData); + std::lock_guard lock(global_mutex); + + auto ret = set_stat_internal(pchName, nData ); + if (ret.success && ret.notify_server ) { + auto &new_stat = (*pending_server_updates.mutable_user_stats())[ret.internal_name]; + new_stat.set_stat_type(GameServerStats_Messages::StatInfo::STAT_TYPE_INT); + new_stat.set_value_int(ret.current_val); + } + + return ret.success; +} + +bool Steam_User_Stats::SetStat( const char *pchName, float fData ) +{ + PRINT_DEBUG("Steam_User_Stats::SetStat '%s' = %f\n", pchName, fData); + std::lock_guard lock(global_mutex); + + auto ret = set_stat_internal(pchName, fData); + if (ret.success && ret.notify_server) { + auto &new_stat = (*pending_server_updates.mutable_user_stats())[ret.internal_name]; + new_stat.set_stat_type(ret.current_val.first); + new_stat.set_value_float(ret.current_val.second); + } + + return ret.success; +} + +bool Steam_User_Stats::UpdateAvgRateStat( const char *pchName, float flCountThisSession, double dSessionLength ) +{ + PRINT_DEBUG("Steam_User_Stats::UpdateAvgRateStat '%s'\n", pchName); + std::lock_guard lock(global_mutex); + + auto ret = update_avg_rate_stat_internal(pchName, flCountThisSession, dSessionLength); + if (ret.success && ret.notify_server) { + auto &new_stat = (*pending_server_updates.mutable_user_stats())[ret.internal_name]; + new_stat.set_stat_type(ret.current_val.first); + new_stat.set_value_float(ret.current_val.second); + } + + return ret.success; +} + + +// Achievement flag accessors +bool Steam_User_Stats::GetAchievement( const char *pchName, bool *pbAchieved ) +{ + PRINT_DEBUG("Steam_User_Stats::GetAchievement '%s'\n", pchName); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + + nlohmann::detail::iter_impl it = defined_achievements.end(); + try { + it = defined_achievements_find(pchName); + } catch(...) { } + if (defined_achievements.end() == it) return false; + + // according to docs, the function returns true if the achievement was found, + // regardless achieved or not + if (!pbAchieved) return true; + + *pbAchieved = false; + try { + std::string pch_name = it->value("name", std::string()); + auto ach = user_achievements.find(pch_name); + if (user_achievements.end() != ach) { + *pbAchieved = ach->value("earned", false); + } + } catch (...) { } + + return true; +} + +bool Steam_User_Stats::SetAchievement( const char *pchName ) +{ + PRINT_DEBUG("Steam_User_Stats::SetAchievement '%s'\n", pchName); + std::lock_guard lock(global_mutex); + + auto ret = set_achievement_internal(pchName); + if (ret.success && ret.notify_server) { + auto &new_ach = (*pending_server_updates.mutable_user_achievements())[ret.internal_name]; + new_ach.set_achieved(ret.current_val); + } + + return ret.success; +} + +bool Steam_User_Stats::ClearAchievement( const char *pchName ) +{ + PRINT_DEBUG("Steam_User_Stats::ClearAchievement '%s'\n", pchName); + std::lock_guard lock(global_mutex); + + auto ret = clear_achievement_internal(pchName); + if (ret.success && ret.notify_server) { + auto &new_ach = (*pending_server_updates.mutable_user_achievements())[ret.internal_name]; + new_ach.set_achieved(ret.current_val); + } + + return ret.success; +} + + +// Get the achievement status, and the time it was unlocked if unlocked. +// If the return value is true, but the unlock time is zero, that means it was unlocked before Steam +// began tracking achievement unlock times (December 2009). Time is seconds since January 1, 1970. +bool Steam_User_Stats::GetAchievementAndUnlockTime( const char *pchName, bool *pbAchieved, uint32 *punUnlockTime ) +{ + PRINT_DEBUG("Steam_User_Stats::GetAchievementAndUnlockTime\n"); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + + nlohmann::detail::iter_impl it = defined_achievements.end(); + try { + it = defined_achievements_find(pchName); + } catch(...) { } + if (defined_achievements.end() == it) return false; + + if (pbAchieved) *pbAchieved = false; + if (punUnlockTime) *punUnlockTime = 0; + + try { + std::string pch_name = it->value("name", std::string()); + auto ach = user_achievements.find(pch_name); + if (user_achievements.end() != ach) { + if (pbAchieved) *pbAchieved = ach->value("earned", false); + if (punUnlockTime) *punUnlockTime = ach->value("earned_time", static_cast(0)); + } + } catch (...) {} + + return true; +} + + +// Store the current data on the server, will get a callback when set +// And one callback for every new achievement +// +// If the callback has a result of k_EResultInvalidParam, one or more stats +// uploaded has been rejected, either because they broke constraints +// or were out of date. In this case the server sends back updated values. +// The stats should be re-iterated to keep in sync. +bool Steam_User_Stats::StoreStats() +{ + PRINT_DEBUG("Steam_User_Stats::StoreStats\n"); + std::lock_guard lock(global_mutex); + + UserStatsStored_t data{}; + data.m_eResult = k_EResultOK; + data.m_nGameID = settings->get_local_game_id().ToUint64(); + callbacks->addCBResult(data.k_iCallback, &data, sizeof(data), 0.01); + + return true; +} + + +// Achievement / GroupAchievement metadata + +// Gets the icon of the achievement, which is a handle to be used in ISteamUtils::GetImageRGBA(), or 0 if none set. +// A return value of 0 may indicate we are still fetching data, and you can wait for the UserAchievementIconFetched_t callback +// which will notify you when the bits are ready. If the callback still returns zero, then there is no image set for the +// specified achievement. +int Steam_User_Stats::GetAchievementIcon( const char *pchName ) +{ + PRINT_DEBUG("TODO Steam_User_Stats::GetAchievementIcon\n"); + std::lock_guard lock(global_mutex); + if (!pchName) return 0; + + return 0; +} + +std::string Steam_User_Stats::get_achievement_icon_name( const char *pchName, bool pbAchieved ) +{ + std::lock_guard lock(global_mutex); + if (!pchName) return ""; + + nlohmann::detail::iter_impl it = defined_achievements.end(); + try { + it = defined_achievements_find(pchName); + } catch(...) { } + if (defined_achievements.end() == it) return ""; + + try { + if (pbAchieved) return it.value()["icon"].get(); + + std::string locked_icon = it.value().value("icon_gray", std::string()); + if (locked_icon.size()) return locked_icon; + else return it.value().value("icongray", std::string()); // old format + } catch (...) {} + + return ""; +} + + +// Get general attributes for an achievement. Accepts the following keys: +// - "name" and "desc" for retrieving the localized achievement name and description (returned in UTF8) +// - "hidden" for retrieving if an achievement is hidden (returns "0" when not hidden, "1" when hidden) +const char * Steam_User_Stats::GetAchievementDisplayAttribute( const char *pchName, const char *pchKey ) +{ + PRINT_DEBUG("Steam_User_Stats::GetAchievementDisplayAttribute [%s] [%s]\n", pchName, pchKey); + std::lock_guard lock(global_mutex); + + if (!pchName || !pchKey || !pchKey[0]) return ""; + + nlohmann::detail::iter_impl it = defined_achievements.end(); + try { + it = defined_achievements_find(pchName); + } catch(...) { } + if (defined_achievements.end() == it) return ""; + + if (strncmp(pchKey, "name", sizeof("name")) == 0) { + try { + return it.value()["displayName"].get_ptr()->c_str(); + } catch (...) {} + } else if (strncmp(pchKey, "desc", sizeof("desc")) == 0) { + try { + return it.value()["description"].get_ptr()->c_str(); + } catch (...) {} + } else if (strncmp(pchKey, "hidden", sizeof("hidden")) == 0) { + try { + return it.value()["hidden"].get_ptr()->c_str(); + } catch (...) {} + } + + return ""; +} + + +// Achievement progress - triggers an AchievementProgress callback, that is all. +// Calling this w/ N out of N progress will NOT set the achievement, the game must still do that. +bool Steam_User_Stats::IndicateAchievementProgress( const char *pchName, uint32 nCurProgress, uint32 nMaxProgress ) +{ + PRINT_DEBUG("Steam_User_Stats::IndicateAchievementProgress %s\n", pchName); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + if (nCurProgress >= nMaxProgress) return false; + auto ach_name = std::string(pchName); + + // find in achievements.json + nlohmann::detail::iter_impl it = defined_achievements.end(); + try { + it = defined_achievements_find(ach_name); + } catch(...) { } + if (defined_achievements.end() == it) return false; + + // get actual name from achievements.json + std::string actual_ach_name{}; + try { + actual_ach_name = it->value("name", std::string()); + } catch (...) { } + if (actual_ach_name.empty()) { // fallback + actual_ach_name = ach_name; + } + + // check if already achieved + bool achieved = false; + try { + auto ach = user_achievements.find(actual_ach_name); + if (ach != user_achievements.end()) { + achieved = ach->value("earned", false); + } + } catch (...) { } + if (achieved) return false; + + // save new progress + try { + user_achievements[actual_ach_name]["progress"] = nCurProgress; + user_achievements[actual_ach_name]["max_progress"] = nMaxProgress; + save_achievements(); + } catch (...) {} + + UserAchievementStored_t data{}; + data.m_nGameID = settings->get_local_game_id().ToUint64(); + data.m_bGroupAchievement = false; + data.m_nCurProgress = nCurProgress; + data.m_nMaxProgress = nMaxProgress; + ach_name.copy(data.m_rgchAchievementName, sizeof(data.m_rgchAchievementName) - 1); + + callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); + return true; +} + + +// Used for iterating achievements. In general games should not need these functions because they should have a +// list of existing achievements compiled into them +uint32 Steam_User_Stats::GetNumAchievements() +{ + PRINT_DEBUG("Steam_User_Stats::GetNumAchievements\n"); + std::lock_guard lock(global_mutex); + return (uint32)defined_achievements.size(); +} + +// Get achievement name iAchievement in [0,GetNumAchievements) +const char * Steam_User_Stats::GetAchievementName( uint32 iAchievement ) +{ + PRINT_DEBUG("Steam_User_Stats::GetAchievementName\n"); + std::lock_guard lock(global_mutex); + if (iAchievement >= sorted_achievement_names.size()) { + return ""; + } + + return sorted_achievement_names[iAchievement].c_str(); +} + + +// Friends stats & achievements + +// downloads stats for the user +// returns a UserStatsReceived_t received when completed +// if the other user has no stats, UserStatsReceived_t.m_eResult will be set to k_EResultFail +// these stats won't be auto-updated; you'll need to call RequestUserStats() again to refresh any data +STEAM_CALL_RESULT( UserStatsReceived_t ) +SteamAPICall_t Steam_User_Stats::RequestUserStats( CSteamID steamIDUser ) +{ + PRINT_DEBUG("Steam_User_Stats::RequestUserStats %llu\n", steamIDUser.ConvertToUint64()); + std::lock_guard lock(global_mutex); + + // Enable this to allow hot reload achievements status + //if (steamIDUser == settings->get_local_steam_id()) { + // load_achievements(); + //} + + + UserStatsReceived_t data; + data.m_nGameID = settings->get_local_game_id().ToUint64(); + data.m_eResult = k_EResultOK; + data.m_steamIDUser = steamIDUser; + return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data), 0.1); +} + + +// requests stat information for a user, usable after a successful call to RequestUserStats() +bool Steam_User_Stats::GetUserStat( CSteamID steamIDUser, const char *pchName, int32 *pData ) +{ + PRINT_DEBUG("Steam_User_Stats::GetUserStat %s %llu\n", pchName, steamIDUser.ConvertToUint64()); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + + if (steamIDUser == settings->get_local_steam_id()) { + GetStat(pchName, pData); + } else { + *pData = 0; + } + + return true; +} + +bool Steam_User_Stats::GetUserStat( CSteamID steamIDUser, const char *pchName, float *pData ) +{ + PRINT_DEBUG("Steam_User_Stats::GetUserStat %s %llu\n", pchName, steamIDUser.ConvertToUint64()); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + + if (steamIDUser == settings->get_local_steam_id()) { + GetStat(pchName, pData); + } else { + *pData = 0; + } + + return true; +} + +bool Steam_User_Stats::GetUserAchievement( CSteamID steamIDUser, const char *pchName, bool *pbAchieved ) +{ + PRINT_DEBUG("Steam_User_Stats::GetUserAchievement %s\n", pchName); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + + if (steamIDUser == settings->get_local_steam_id()) { + return GetAchievement(pchName, pbAchieved); + } + + return false; +} + +// See notes for GetAchievementAndUnlockTime above +bool Steam_User_Stats::GetUserAchievementAndUnlockTime( CSteamID steamIDUser, const char *pchName, bool *pbAchieved, uint32 *punUnlockTime ) +{ + PRINT_DEBUG("Steam_User_Stats::GetUserAchievementAndUnlockTime %s\n", pchName); + std::lock_guard lock(global_mutex); + + if (!pchName) return false; + + if (steamIDUser == settings->get_local_steam_id()) { + return GetAchievementAndUnlockTime(pchName, pbAchieved, punUnlockTime); + } + return false; +} + + +// Reset stats +bool Steam_User_Stats::ResetAllStats( bool bAchievementsToo ) +{ + PRINT_DEBUG("Steam_User_Stats::ResetAllStats\n"); + std::lock_guard lock(global_mutex); + //TODO + if (bAchievementsToo) { + std::for_each(user_achievements.begin(), user_achievements.end(), [](nlohmann::json& v) { + v["earned"] = false; + v["earned_time"] = 0; + }); + } + + return true; +} + + +// Leaderboard functions + +// asks the Steam back-end for a leaderboard by name, and will create it if it's not yet +// This call is asynchronous, with the result returned in LeaderboardFindResult_t +STEAM_CALL_RESULT(LeaderboardFindResult_t) +SteamAPICall_t Steam_User_Stats::FindOrCreateLeaderboard( const char *pchLeaderboardName, ELeaderboardSortMethod eLeaderboardSortMethod, ELeaderboardDisplayType eLeaderboardDisplayType ) +{ + PRINT_DEBUG("Steam_User_Stats::FindOrCreateLeaderboard %s\n", pchLeaderboardName); + std::lock_guard lock(global_mutex); + if (!pchLeaderboardName) { + LeaderboardFindResult_t data; + data.m_hSteamLeaderboard = 0; + data.m_bLeaderboardFound = 0; + return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); + } + + unsigned int leader = find_leaderboard(pchLeaderboardName); + if (!leader) { + struct Steam_Leaderboard leaderboard; + leaderboard.name = std::string(pchLeaderboardName); + leaderboard.sort_method = eLeaderboardSortMethod; + leaderboard.display_type = eLeaderboardDisplayType; + leaderboard.self_score.score = eLeaderboardSortMethod == k_ELeaderboardSortMethodAscending ? INT_MAX : INT_MIN; + + std::vector scores = load_leaderboard_scores(pchLeaderboardName); + for (auto &s : scores) { + if (s.steam_id == settings->get_local_steam_id()) { + leaderboard.self_score = s; + } + } + + leaderboards.push_back(leaderboard); + leader = leaderboards.size(); + } + + LeaderboardFindResult_t data; + data.m_hSteamLeaderboard = leader; + data.m_bLeaderboardFound = 1; + return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); +} + + +// as above, but won't create the leaderboard if it's not found +// This call is asynchronous, with the result returned in LeaderboardFindResult_t +STEAM_CALL_RESULT( LeaderboardFindResult_t ) +SteamAPICall_t Steam_User_Stats::FindLeaderboard( const char *pchLeaderboardName ) +{ + PRINT_DEBUG("Steam_User_Stats::FindLeaderboard %s\n", pchLeaderboardName); + std::lock_guard lock(global_mutex); + if (!pchLeaderboardName) { + LeaderboardFindResult_t data; + data.m_hSteamLeaderboard = 0; + data.m_bLeaderboardFound = 0; + return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); + } + + auto settings_Leaderboards = settings->getLeaderboards(); + if (settings_Leaderboards.count(pchLeaderboardName)) { + auto config = settings_Leaderboards[pchLeaderboardName]; + return FindOrCreateLeaderboard(pchLeaderboardName, config.sort_method, config.display_type); + } else if (settings->createUnknownLeaderboards()) { + return FindOrCreateLeaderboard(pchLeaderboardName, k_ELeaderboardSortMethodDescending, k_ELeaderboardDisplayTypeNumeric); + } else { + LeaderboardFindResult_t data; + data.m_hSteamLeaderboard = find_leaderboard(pchLeaderboardName);; + data.m_bLeaderboardFound = !!data.m_hSteamLeaderboard; + return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); + } +} + + +// returns the name of a leaderboard +const char * Steam_User_Stats::GetLeaderboardName( SteamLeaderboard_t hSteamLeaderboard ) +{ + PRINT_DEBUG("Steam_User_Stats::GetLeaderboardName\n"); + std::lock_guard lock(global_mutex); + + if (hSteamLeaderboard > leaderboards.size() || hSteamLeaderboard <= 0) return ""; + return leaderboards[hSteamLeaderboard - 1].name.c_str(); +} + + +// returns the total number of entries in a leaderboard, as of the last request +int Steam_User_Stats::GetLeaderboardEntryCount( SteamLeaderboard_t hSteamLeaderboard ) +{ + PRINT_DEBUG("Steam_User_Stats::GetLeaderboardEntryCount\n"); + return 0; +} + + +// returns the sort method of the leaderboard +ELeaderboardSortMethod Steam_User_Stats::GetLeaderboardSortMethod( SteamLeaderboard_t hSteamLeaderboard ) +{ + PRINT_DEBUG("Steam_User_Stats::GetLeaderboardSortMethod\n"); + std::lock_guard lock(global_mutex); + if (hSteamLeaderboard > leaderboards.size() || hSteamLeaderboard <= 0) return k_ELeaderboardSortMethodNone; + return leaderboards[hSteamLeaderboard - 1].sort_method; +} + + +// returns the display type of the leaderboard +ELeaderboardDisplayType Steam_User_Stats::GetLeaderboardDisplayType( SteamLeaderboard_t hSteamLeaderboard ) +{ + PRINT_DEBUG("Steam_User_Stats::GetLeaderboardDisplayType\n"); + std::lock_guard lock(global_mutex); + if (hSteamLeaderboard > leaderboards.size() || hSteamLeaderboard <= 0) return k_ELeaderboardDisplayTypeNone; + return leaderboards[hSteamLeaderboard - 1].display_type; +} + + +// Asks the Steam back-end for a set of rows in the leaderboard. +// This call is asynchronous, with the result returned in LeaderboardScoresDownloaded_t +// LeaderboardScoresDownloaded_t will contain a handle to pull the results from GetDownloadedLeaderboardEntries() (below) +// You can ask for more entries than exist, and it will return as many as do exist. +// k_ELeaderboardDataRequestGlobal requests rows in the leaderboard from the full table, with nRangeStart & nRangeEnd in the range [1, TotalEntries] +// k_ELeaderboardDataRequestGlobalAroundUser requests rows around the current user, nRangeStart being negate +// e.g. DownloadLeaderboardEntries( hLeaderboard, k_ELeaderboardDataRequestGlobalAroundUser, -3, 3 ) will return 7 rows, 3 before the user, 3 after +// k_ELeaderboardDataRequestFriends requests all the rows for friends of the current user +STEAM_CALL_RESULT( LeaderboardScoresDownloaded_t ) +SteamAPICall_t Steam_User_Stats::DownloadLeaderboardEntries( SteamLeaderboard_t hSteamLeaderboard, ELeaderboardDataRequest eLeaderboardDataRequest, int nRangeStart, int nRangeEnd ) +{ + PRINT_DEBUG("Steam_User_Stats::DownloadLeaderboardEntries %llu %i %i %i\n", hSteamLeaderboard, eLeaderboardDataRequest, nRangeStart, nRangeEnd); + std::lock_guard lock(global_mutex); + if (hSteamLeaderboard > leaderboards.size() || hSteamLeaderboard <= 0) return k_uAPICallInvalid; //might return callresult even if hSteamLeaderboard is invalid + + LeaderboardScoresDownloaded_t data; + data.m_hSteamLeaderboard = hSteamLeaderboard; + data.m_hSteamLeaderboardEntries = hSteamLeaderboard; + data.m_cEntryCount = leaderboards[hSteamLeaderboard - 1].self_score.steam_id.IsValid(); + return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); +} + +// as above, but downloads leaderboard entries for an arbitrary set of users - ELeaderboardDataRequest is k_ELeaderboardDataRequestUsers +// if a user doesn't have a leaderboard entry, they won't be included in the result +// a max of 100 users can be downloaded at a time, with only one outstanding call at a time +STEAM_METHOD_DESC(Downloads leaderboard entries for an arbitrary set of users - ELeaderboardDataRequest is k_ELeaderboardDataRequestUsers) +STEAM_CALL_RESULT( LeaderboardScoresDownloaded_t ) +SteamAPICall_t Steam_User_Stats::DownloadLeaderboardEntriesForUsers( SteamLeaderboard_t hSteamLeaderboard, + STEAM_ARRAY_COUNT_D(cUsers, Array of users to retrieve) CSteamID *prgUsers, int cUsers ) +{ + PRINT_DEBUG("Steam_User_Stats::DownloadLeaderboardEntriesForUsers %i %llu\n", cUsers, cUsers > 0 ? prgUsers[0].ConvertToUint64() : 0); + std::lock_guard lock(global_mutex); + if (hSteamLeaderboard > leaderboards.size() || hSteamLeaderboard <= 0) return k_uAPICallInvalid; //might return callresult even if hSteamLeaderboard is invalid + + bool get_for_current_id = false; + for (int i = 0; i < cUsers; ++i) { + if (prgUsers[i] == settings->get_local_steam_id()) { + get_for_current_id = true; + } + } + + LeaderboardScoresDownloaded_t data; + data.m_hSteamLeaderboard = hSteamLeaderboard; + data.m_hSteamLeaderboardEntries = hSteamLeaderboard; + data.m_cEntryCount = get_for_current_id && leaderboards[hSteamLeaderboard - 1].self_score.steam_id.IsValid(); + return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); +} + + +// Returns data about a single leaderboard entry +// use a for loop from 0 to LeaderboardScoresDownloaded_t::m_cEntryCount to get all the downloaded entries +// e.g. +// void OnLeaderboardScoresDownloaded( LeaderboardScoresDownloaded_t *pLeaderboardScoresDownloaded ) +// { +// for ( int index = 0; index < pLeaderboardScoresDownloaded->m_cEntryCount; index++ ) +// { +// LeaderboardEntry_t leaderboardEntry; +// int32 details[3]; // we know this is how many we've stored previously +// GetDownloadedLeaderboardEntry( pLeaderboardScoresDownloaded->m_hSteamLeaderboardEntries, index, &leaderboardEntry, details, 3 ); +// assert( leaderboardEntry.m_cDetails == 3 ); +// ... +// } +// once you've accessed all the entries, the data will be free'd, and the SteamLeaderboardEntries_t handle will become invalid +bool Steam_User_Stats::GetDownloadedLeaderboardEntry( SteamLeaderboardEntries_t hSteamLeaderboardEntries, int index, LeaderboardEntry_t *pLeaderboardEntry, int32 *pDetails, int cDetailsMax ) +{ + PRINT_DEBUG("Steam_User_Stats::GetDownloadedLeaderboardEntry\n"); + std::lock_guard lock(global_mutex); + if (hSteamLeaderboardEntries > leaderboards.size() || hSteamLeaderboardEntries <= 0) return false; + if (index > 0) return false; + + LeaderboardEntry_t entry = {}; + entry.m_steamIDUser = leaderboards[hSteamLeaderboardEntries - 1].self_score.steam_id; + entry.m_nGlobalRank = 1; + entry.m_nScore = leaderboards[hSteamLeaderboardEntries - 1].self_score.score; + for (int i = 0; i < leaderboards[hSteamLeaderboardEntries - 1].self_score.score_details.size() && i < cDetailsMax; ++i) { + pDetails[i] = leaderboards[hSteamLeaderboardEntries - 1].self_score.score_details[i]; + } + + *pLeaderboardEntry = entry; + return true; +} + + +// Uploads a user score to the Steam back-end. +// This call is asynchronous, with the result returned in LeaderboardScoreUploaded_t +// Details are extra game-defined information regarding how the user got that score +// pScoreDetails points to an array of int32's, cScoreDetailsCount is the number of int32's in the list +STEAM_CALL_RESULT( LeaderboardScoreUploaded_t ) +SteamAPICall_t Steam_User_Stats::UploadLeaderboardScore( SteamLeaderboard_t hSteamLeaderboard, ELeaderboardUploadScoreMethod eLeaderboardUploadScoreMethod, int32 nScore, const int32 *pScoreDetails, int cScoreDetailsCount ) +{ + PRINT_DEBUG("Steam_User_Stats::UploadLeaderboardScore %i\n", nScore); + std::lock_guard lock(global_mutex); + if (hSteamLeaderboard > leaderboards.size() || hSteamLeaderboard <= 0) return k_uAPICallInvalid; //TODO: might return callresult even if hSteamLeaderboard is invalid + + Steam_Leaderboard_Score score; + score.score = nScore; + score.steam_id = settings->get_local_steam_id(); + for (int i = 0; i < cScoreDetailsCount; ++i) { + score.score_details.push_back(pScoreDetails[i]); + } + + bool changed = false; + if (eLeaderboardUploadScoreMethod == k_ELeaderboardUploadScoreMethodKeepBest) { + if (leaderboards[hSteamLeaderboard - 1].sort_method == k_ELeaderboardSortMethodAscending + ? leaderboards[hSteamLeaderboard - 1].self_score.score >= score.score + : leaderboards[hSteamLeaderboard - 1].self_score.score <= score.score) { + leaderboards[hSteamLeaderboard - 1].self_score = score; + changed = true; + } + } else { + if (leaderboards[hSteamLeaderboard - 1].self_score.score != score.score) changed = true; + leaderboards[hSteamLeaderboard - 1].self_score = score; + } + + if (changed) { + save_leaderboard_score(&(leaderboards[hSteamLeaderboard - 1])); + } + + LeaderboardScoreUploaded_t data; + data.m_bSuccess = 1; //needs to be success or DOA6 freezes when uploading score. + //data.m_bSuccess = 0; + data.m_hSteamLeaderboard = hSteamLeaderboard; + data.m_nScore = nScore; + data.m_bScoreChanged = changed; + data.m_nGlobalRankNew = 1; + data.m_nGlobalRankPrevious = 0; + return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); +} + +SteamAPICall_t Steam_User_Stats::UploadLeaderboardScore( SteamLeaderboard_t hSteamLeaderboard, int32 nScore, int32 *pScoreDetails, int cScoreDetailsCount ) +{ + PRINT_DEBUG("UploadLeaderboardScore old\n"); + return UploadLeaderboardScore(hSteamLeaderboard, k_ELeaderboardUploadScoreMethodKeepBest, nScore, pScoreDetails, cScoreDetailsCount); +} + + +// Attaches a piece of user generated content the user's entry on a leaderboard. +// hContent is a handle to a piece of user generated content that was shared using ISteamUserRemoteStorage::FileShare(). +// This call is asynchronous, with the result returned in LeaderboardUGCSet_t. +STEAM_CALL_RESULT( LeaderboardUGCSet_t ) +SteamAPICall_t Steam_User_Stats::AttachLeaderboardUGC( SteamLeaderboard_t hSteamLeaderboard, UGCHandle_t hUGC ) +{ + PRINT_DEBUG("Steam_User_Stats::AttachLeaderboardUGC\n"); + std::lock_guard lock(global_mutex); + LeaderboardUGCSet_t data = {}; + if (hSteamLeaderboard > leaderboards.size() || hSteamLeaderboard <= 0) { + data.m_eResult = k_EResultFail; + } else { + data.m_eResult = k_EResultOK; + } + + data.m_hSteamLeaderboard = hSteamLeaderboard; + return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); +} + + +// Retrieves the number of players currently playing your game (online + offline) +// This call is asynchronous, with the result returned in NumberOfCurrentPlayers_t +STEAM_CALL_RESULT( NumberOfCurrentPlayers_t ) +SteamAPICall_t Steam_User_Stats::GetNumberOfCurrentPlayers() +{ + PRINT_DEBUG("Steam_User_Stats::GetNumberOfCurrentPlayers\n"); + std::lock_guard lock(global_mutex); + NumberOfCurrentPlayers_t data; + data.m_bSuccess = 1; + data.m_cPlayers = 69; + return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); +} + + +// Requests that Steam fetch data on the percentage of players who have received each achievement +// for the game globally. +// This call is asynchronous, with the result returned in GlobalAchievementPercentagesReady_t. +STEAM_CALL_RESULT( GlobalAchievementPercentagesReady_t ) +SteamAPICall_t Steam_User_Stats::RequestGlobalAchievementPercentages() +{ + PRINT_DEBUG("Steam_User_Stats::RequestGlobalAchievementPercentages\n"); + return 0; +} + + +// Get the info on the most achieved achievement for the game, returns an iterator index you can use to fetch +// the next most achieved afterwards. Will return -1 if there is no data on achievement +// percentages (ie, you haven't called RequestGlobalAchievementPercentages and waited on the callback). +int Steam_User_Stats::GetMostAchievedAchievementInfo( char *pchName, uint32 unNameBufLen, float *pflPercent, bool *pbAchieved ) +{ + PRINT_DEBUG("Steam_User_Stats::GetMostAchievedAchievementInfo\n"); + return -1; +} + + +// Get the info on the next most achieved achievement for the game. Call this after GetMostAchievedAchievementInfo or another +// GetNextMostAchievedAchievementInfo call passing the iterator from the previous call. Returns -1 after the last +// achievement has been iterated. +int Steam_User_Stats::GetNextMostAchievedAchievementInfo( int iIteratorPrevious, char *pchName, uint32 unNameBufLen, float *pflPercent, bool *pbAchieved ) +{ + PRINT_DEBUG("Steam_User_Stats::GetNextMostAchievedAchievementInfo\n"); + return -1; +} + + +// Returns the percentage of users who have achieved the specified achievement. +bool Steam_User_Stats::GetAchievementAchievedPercent( const char *pchName, float *pflPercent ) +{ + PRINT_DEBUG("Steam_User_Stats::GetAchievementAchievedPercent\n"); + return false; +} + + +// Requests global stats data, which is available for stats marked as "aggregated". +// This call is asynchronous, with the results returned in GlobalStatsReceived_t. +// nHistoryDays specifies how many days of day-by-day history to retrieve in addition +// to the overall totals. The limit is 60. +STEAM_CALL_RESULT( GlobalStatsReceived_t ) +SteamAPICall_t Steam_User_Stats::RequestGlobalStats( int nHistoryDays ) +{ + PRINT_DEBUG("Steam_User_Stats::RequestGlobalStats %i\n", nHistoryDays); + std::lock_guard lock(global_mutex); + GlobalStatsReceived_t data{}; + data.m_nGameID = settings->get_local_game_id().ToUint64(); + data.m_eResult = k_EResultOK; + return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); +} + + +// Gets the lifetime totals for an aggregated stat +bool Steam_User_Stats::GetGlobalStat( const char *pchStatName, int64 *pData ) +{ + PRINT_DEBUG("Steam_User_Stats::GetGlobalStat %s\n", pchStatName); + return false; +} + +bool Steam_User_Stats::GetGlobalStat( const char *pchStatName, double *pData ) +{ + PRINT_DEBUG("Steam_User_Stats::GetGlobalStat %s\n", pchStatName); + return false; +} + + +// Gets history for an aggregated stat. pData will be filled with daily values, starting with today. +// So when called, pData[0] will be today, pData[1] will be yesterday, and pData[2] will be two days ago, +// etc. cubData is the size in bytes of the pubData buffer. Returns the number of +// elements actually set. +int32 Steam_User_Stats::GetGlobalStatHistory( const char *pchStatName, STEAM_ARRAY_COUNT(cubData) int64 *pData, uint32 cubData ) +{ + PRINT_DEBUG("Steam_User_Stats::GetGlobalStatHistory int64 %s\n", pchStatName); + return 0; +} + +int32 Steam_User_Stats::GetGlobalStatHistory( const char *pchStatName, STEAM_ARRAY_COUNT(cubData) double *pData, uint32 cubData ) +{ + PRINT_DEBUG("Steam_User_Stats::GetGlobalStatHistory double %s\n", pchStatName); + return 0; +} + +// For achievements that have related Progress stats, use this to query what the bounds of that progress are. +// You may want this info to selectively call IndicateAchievementProgress when appropriate milestones of progress +// have been made, to show a progress notification to the user. +bool Steam_User_Stats::GetAchievementProgressLimits( const char *pchName, int32 *pnMinProgress, int32 *pnMaxProgress ) +{ + PRINT_DEBUG("Steam_User_Stats::GetAchievementProgressLimits int\n"); + return false; +} + +bool Steam_User_Stats::GetAchievementProgressLimits( const char *pchName, float *pfMinProgress, float *pfMaxProgress ) +{ + PRINT_DEBUG("Steam_User_Stats::GetAchievementProgressLimits float\n"); + return false; +} + + + +// --- steam callbacks + +void Steam_User_Stats::send_updated_stats() +{ + if (pending_server_updates.user_stats().empty() && pending_server_updates.user_achievements().empty()) return; + + auto new_updates_msg = new GameServerStats_Messages::AllStats(pending_server_updates); + pending_server_updates.clear_user_stats(); + pending_server_updates.clear_user_achievements(); + + auto gameserverstats_msg = new GameServerStats_Messages(); + gameserverstats_msg->set_type(GameServerStats_Messages::UpdateUserStats); + gameserverstats_msg->set_allocated_update_user_stats(new_updates_msg); + + Common_Message msg{}; + // https://protobuf.dev/reference/cpp/cpp-generated/#string + // set_allocated_xxx() takes ownership of the allocated object, no need to delete + msg.set_allocated_gameserver_stats_messages(gameserverstats_msg); + msg.set_source_id(settings->get_local_steam_id().ConvertToUint64()); + // here we send to all users on the network because we don't know the server steamid + network->sendToAll(&msg, true); + + PRINT_DEBUG( + "Steam_User_Stats::send_updated_stats sent updated stats: %zu stats, %zu achievements\n", + new_updates_msg->user_stats().size(), new_updates_msg->user_achievements().size() + ); +} + +void Steam_User_Stats::steam_run_callback() +{ + send_updated_stats(); +} + + + +// --- networking callbacks + +void Steam_User_Stats::network_callback_initial_stats(Common_Message *msg) +{ + if (!msg->gameserver_stats_messages().has_initial_user_stats()) { + PRINT_DEBUG("Steam_User_Stats::network_callback_initial_stats error empty msg\n"); + return; + } + + uint64 server_steamid = msg->source_id(); + + auto all_stats_msg = new GameServerStats_Messages::AllStats(); + + // get all stats + auto &stats_map = *all_stats_msg->mutable_user_stats(); + const auto ¤t_stats = settings->getStats(); + for (const auto &stat : current_stats) { + auto &this_stat = stats_map[stat.first]; + this_stat.set_stat_type(stat.second.type); + switch (stat.second.type) + { + case GameServerStats_Messages::StatInfo::STAT_TYPE_INT: { + int32 val = 0; + GetStat(stat.first.c_str(), &val); + this_stat.set_value_int(val); + } + break; + + case GameServerStats_Messages::StatInfo::STAT_TYPE_AVGRATE: // we set the float value also for avg + case GameServerStats_Messages::StatInfo::STAT_TYPE_FLOAT: { + float val = 0; + GetStat(stat.first.c_str(), &val); + this_stat.set_value_float(val); + } + break; + + default: + PRINT_DEBUG("Steam_User_Stats::network_callback_initial_stats Request_AllUserStats unhandled stat type %i\n", (int)stat.second.type); + break; + } + } + + // get all achievements + auto &achievements_map = *all_stats_msg->mutable_user_achievements(); + for (const auto &ach : defined_achievements) { + const std::string &name = static_cast( ach.value("name", std::string()) ); + auto &this_ach = achievements_map[name]; + bool achieved = false; + GetAchievement(name.c_str(), &achieved); + this_ach.set_achieved(achieved); + } + + auto initial_stats_msg = new GameServerStats_Messages::InitialAllStats(); + // send back same api call id + initial_stats_msg->set_steam_api_call(msg->gameserver_stats_messages().initial_user_stats().steam_api_call()); + initial_stats_msg->set_allocated_all_data(all_stats_msg); + + auto gameserverstats_msg = new GameServerStats_Messages(); + gameserverstats_msg->set_type(GameServerStats_Messages::Response_AllUserStats); + gameserverstats_msg->set_allocated_initial_user_stats(initial_stats_msg); + + Common_Message new_msg{}; + // https://protobuf.dev/reference/cpp/cpp-generated/#string + // set_allocated_xxx() takes ownership of the allocated object, no need to delete + new_msg.set_allocated_gameserver_stats_messages(gameserverstats_msg); + new_msg.set_source_id(settings->get_local_steam_id().ConvertToUint64()); + new_msg.set_dest_id(server_steamid); + network->sendTo(&new_msg, true); + + PRINT_DEBUG( + "Steam_User_Stats::network_callback_initial_stats server requested all stats, sent %zu stats, %zu achievements\n", + initial_stats_msg->all_data().user_stats().size(), initial_stats_msg->all_data().user_achievements().size() + ); + + +} + +void Steam_User_Stats::network_callback_updated_stats(Common_Message *msg) +{ + if (!msg->gameserver_stats_messages().has_update_user_stats()) { + PRINT_DEBUG("Steam_User_Stats::network_callback_updated_stats error empty msg\n"); + return; + } + + uint64 server_steamid = msg->source_id(); + + auto &new_user_data = msg->gameserver_stats_messages().update_user_stats(); + + // update our stats + for (auto &new_stat : new_user_data.user_stats()) { + switch (new_stat.second.stat_type()) + { + case GameServerStats_Messages::StatInfo::STAT_TYPE_INT: { + set_stat_internal(new_stat.first.c_str(), new_stat.second.value_int()); + } + break; + + case GameServerStats_Messages::StatInfo::STAT_TYPE_AVGRATE: + case GameServerStats_Messages::StatInfo::STAT_TYPE_FLOAT: { + set_stat_internal(new_stat.first.c_str(), new_stat.second.value_float()); + // non-INT values could have avg values + if (new_stat.second.has_value_avg()) { + auto &avg_val = new_stat.second.value_avg(); + update_avg_rate_stat_internal(new_stat.first.c_str(), avg_val.count_this_session(), avg_val.session_length()); + } + } + break; + + default: + PRINT_DEBUG("Steam_User_Stats::network_callback_updated_stats UpdateUserStats unhandled stat type %i\n", (int)new_stat.second.stat_type()); + break; + } + } + + // update achievements + for (auto &new_ach : new_user_data.user_achievements()) { + if (new_ach.second.achieved()) { + set_achievement_internal(new_ach.first.c_str()); + } else { + clear_achievement_internal(new_ach.first.c_str()); + } + } + + PRINT_DEBUG( + "Steam_User_Stats::network_callback_updated_stats server sent updated user stats, %zu stats, %zu achievements\n", + new_user_data.user_stats().size(), new_user_data.user_achievements().size() + ); +} + +// only triggered when we have a message +void Steam_User_Stats::network_callback(Common_Message *msg) +{ + uint64 server_steamid = msg->source_id(); + + switch (msg->gameserver_stats_messages().type()) + { + // server wants all stats + case GameServerStats_Messages::Request_AllUserStats: + network_callback_initial_stats(msg); + break; + + // server has updated/new stats + case GameServerStats_Messages::UpdateUserStats: + network_callback_updated_stats(msg); + break; + + default: + PRINT_DEBUG("Steam_GameServerStats::network_callback unhandled type %i\n", (int)msg->gameserver_stats_messages().type()); + break; + } +}