/* 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" #include // change stats without sending back to server bool Steam_User_Stats::clear_stats_internal() { PRINT_DEBUG_ENTRY(); std::lock_guard lock(global_mutex); bool notify_server = false; for (const auto &stat : settings->getStats()) { std::string stat_name(common_helpers::ascii_to_lowercase(stat.first)); switch (stat.second.type) { case GameServerStats_Messages::StatInfo::STAT_TYPE_INT: { auto data = stat.second.default_value_int; bool needs_disk_write = false; auto it_res = stats_cache_int.find(stat_name); if (stats_cache_int.end() == it_res || it_res->second != data) { needs_disk_write = true; notify_server = true; } stats_cache_int[stat_name] = data; if (needs_disk_write) local_storage->store_data(Local_Storage::stats_storage_folder, stat_name, (char *)&data, sizeof(data)); } break; case GameServerStats_Messages::StatInfo::STAT_TYPE_FLOAT: case GameServerStats_Messages::StatInfo::STAT_TYPE_AVGRATE: { auto data = stat.second.default_value_float; bool needs_disk_write = false; auto it_res = stats_cache_float.find(stat_name); if (stats_cache_float.end() == it_res || it_res->second != data) { needs_disk_write = true; notify_server = true; } stats_cache_float[stat_name] = data; if (needs_disk_write) local_storage->store_data(Local_Storage::stats_storage_folder, stat_name, (char *)&data, sizeof(data)); } break; default: PRINT_DEBUG("unhandled type %i", (int)stat.second.type); break; } } return notify_server; } Steam_User_Stats::InternalSetResult Steam_User_Stats::set_stat_internal( const char *pchName, int32 nData ) { PRINT_DEBUG(" '%s' = %i", 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 && settings->allow_unknown_stats) { Stat_config cfg{}; cfg.type = GameServerStats_Messages::StatInfo::STAT_TYPE_INT; cfg.default_value_int = 0; stats_data = settings->setStatDefiniton(stat_name, cfg); } 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 (stats_cache_int.end() != cached_stat) { 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.should_unlock_ach(nData)) { set_achievement_internal(t.name.c_str()); } if (settings->stat_achievement_progress_functionality && t.should_indicate_progress(nData)) { bool indicate_progress = true; // appid 1482380 needs that otherwise it will spam progress indications while driving if (settings->save_only_higher_stat_achievement_progress) { try { auto user_ach_it = user_achievements.find(t.name); if (user_achievements.end() != user_ach_it) { auto user_progress_it = user_ach_it->find("progress"); if (user_ach_it->end() != user_progress_it) { int32 user_progress = *user_progress_it; if (nData <= user_progress) { indicate_progress = false; } } } } catch(...){} } if (indicate_progress && (stats_data->second + settings->start_int_min_diff_progress) >= nData) { IndicateAchievementProgress(t.name.c_str(), nData, std::stoi(t.max_value)); } } } } 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 = !settings->disable_sharing_stats_with_gameserver; return result; } return result; } Steam_User_Stats::InternalSetResult> Steam_User_Stats::set_stat_internal( const char *pchName, float fData ) { PRINT_DEBUG(" '%s' = %f", 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 && settings->allow_unknown_stats) { Stat_config cfg{}; cfg.type = GameServerStats_Messages::StatInfo::STAT_TYPE_FLOAT; cfg.default_value_float = 0; stats_data = settings->setStatDefiniton(stat_name, cfg); } 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 (stats_cache_float.end() != cached_stat) { 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.should_unlock_ach(fData)) { set_achievement_internal(t.name.c_str()); } if (settings->stat_achievement_progress_functionality && t.should_indicate_progress(fData)) { bool indicate_progress = true; // appid 1482380 needs that otherwise it will spam progress indications while driving if (settings->save_only_higher_stat_achievement_progress) { try { auto user_ach_it = user_achievements.find(t.name); if (user_achievements.end() != user_ach_it) { auto user_progress_it = user_ach_it->find("progress"); if (user_ach_it->end() != user_progress_it) { float user_progress = *user_progress_it; if (fData <= user_progress) { indicate_progress = false; } } } } catch(...){} } if (indicate_progress && (stats_data->second + settings->start_float_min_diff_progress) >= fData) { IndicateAchievementProgress(t.name.c_str(), (uint32)fData, (uint32)std::stof(t.max_value)); } } } } 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 = !settings->disable_sharing_stats_with_gameserver; 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("%s", 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 && settings->allow_unknown_stats) { Stat_config cfg{}; cfg.type = GameServerStats_Messages::StatInfo::STAT_TYPE_AVGRATE; cfg.default_value_float = 0; stats_data = settings->setStatDefiniton(stat_name, cfg); } 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(float), sizeof(oldsessionlength)); } oldcount += flCountThisSession; oldsessionlength += dSessionLength; float average = static_cast(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 = !settings->disable_sharing_stats_with_gameserver; return result; } return result; } void Steam_User_Stats::steam_user_stats_network_stats(void *object, Common_Message *msg) { // PRINT_DEBUG_ENTRY(); auto inst = (Steam_User_Stats *)object; inst->network_callback_stats(msg); } // 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_ENTRY(); 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(" '%s' %p", 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 && settings->allow_unknown_stats) { Stat_config cfg{}; cfg.type = GameServerStats_Messages::StatInfo::STAT_TYPE_INT; cfg.default_value_int = 0; stats_data = settings->setStatDefiniton(stat_name, cfg); } 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(" '%s' %p", 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 && settings->allow_unknown_stats) { Stat_config cfg{}; cfg.type = GameServerStats_Messages::StatInfo::STAT_TYPE_FLOAT; cfg.default_value_float = 0; stats_data = settings->setStatDefiniton(stat_name, cfg); } 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(" '%s' = %i", 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); if (settings->immediate_gameserver_stats) send_updated_stats(); } return ret.success; } bool Steam_User_Stats::SetStat( const char *pchName, float fData ) { PRINT_DEBUG(" '%s' = %f", 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); if (settings->immediate_gameserver_stats) send_updated_stats(); } return ret.success; } bool Steam_User_Stats::UpdateAvgRateStat( const char *pchName, float flCountThisSession, double dSessionLength ) { PRINT_DEBUG("'%s'", 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); if (settings->immediate_gameserver_stats) send_updated_stats(); } return ret.success; } // 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() { // no need to exchange data with gameserver, we already do that in run_callback() and on each stat/ach update (immediate mode) PRINT_DEBUG_ENTRY(); 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); for (auto &kv : store_stats_trigger) { callbacks->addCBResult(kv.second.k_iCallback, &kv.second, sizeof(kv.second)); } store_stats_trigger.clear(); return true; } // Friends stats // 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("%llu", 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; // appid 756800 expects both: a callback (global event occurring in the Steam environment), // and a callresult (the specific result of this function call) // otherwise it will lock-up and hang at startup auto ret = callback_results->addCallResult(data.k_iCallback, &data, sizeof(data), 0.1); callbacks->addCBResult(data.k_iCallback, &data, sizeof(data), 0.1); return ret; } // 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("'%s' %llu", 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("%s %llu", 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; } // Reset stats bool Steam_User_Stats::ResetAllStats( bool bAchievementsToo ) { PRINT_DEBUG("bAchievementsToo = %i", (int)bAchievementsToo); std::lock_guard lock(global_mutex); clear_stats_internal(); // this will save stats to disk if necessary if (!settings->disable_sharing_stats_with_gameserver) { for (const auto &stat : settings->getStats()) { std::string stat_name(common_helpers::ascii_to_lowercase(stat.first)); auto &new_stat = (*pending_server_updates.mutable_user_stats())[stat_name]; new_stat.set_stat_type(stat.second.type); switch (stat.second.type) { case GameServerStats_Messages::StatInfo::STAT_TYPE_INT: new_stat.set_value_int(stat.second.default_value_int); break; case GameServerStats_Messages::StatInfo::STAT_TYPE_AVGRATE: case GameServerStats_Messages::StatInfo::STAT_TYPE_FLOAT: new_stat.set_value_float(stat.second.default_value_float); break; default: PRINT_DEBUG("unhandled type %i", (int)stat.second.type); break; } } } if (bAchievementsToo) { bool needs_disk_write = false; for (auto &kv : user_achievements.items()) { try { auto &name = kv.key(); auto &item = kv.value(); // assume "earned" is true in case the json obj exists, but the key is absent if (item.value("earned", true) == true) needs_disk_write = true; item["earned"] = false; item["earned_time"] = static_cast(0); try { auto defined_ach_it = defined_achievements_find(name); if (defined_achievements.end() != defined_ach_it) { auto defined_progress_it = defined_ach_it->find("progress"); if (defined_ach_it->end() != defined_progress_it) { // if the schema had "progress" uint32 val = 0; try { auto defined_min_val = defined_progress_it->value("min_val", std::string("0")); val = std::stoul(defined_min_val); } catch(...){} item["progress"] = val; } } }catch(...){} // this won't actually trigger a notification, just updates the data overlay->AddAchievementNotification(name, item, false); } catch(const std::exception& e) { const char *errorMessage = e.what(); PRINT_DEBUG("ERROR: %s", errorMessage); } } if (needs_disk_write) save_achievements(); if (!settings->disable_sharing_stats_with_gameserver) { for (const auto &item : user_achievements.items()) { auto &new_ach = (*pending_server_updates.mutable_user_achievements())[item.key()]; new_ach.set_achieved(false); } } } if (!settings->disable_sharing_stats_with_gameserver && settings->immediate_gameserver_stats) send_updated_stats(); return true; } // 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("%i", nHistoryDays); std::lock_guard lock(global_mutex); GlobalStatsReceived_t data{}; data.m_nGameID = settings->get_local_game_id().ToUint64(); data.m_eResult = k_EResultOK; auto ret = callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); callbacks->addCBResult(data.k_iCallback, &data, sizeof(data)); return ret; } // Gets the lifetime totals for an aggregated stat bool Steam_User_Stats::GetGlobalStat( const char *pchStatName, int64 *pData ) { PRINT_DEBUG_TODO(); std::lock_guard lock(global_mutex); return false; } bool Steam_User_Stats::GetGlobalStat( const char *pchStatName, double *pData ) { PRINT_DEBUG_TODO(); std::lock_guard lock(global_mutex); 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_TODO(); std::lock_guard lock(global_mutex); return 0; } int32 Steam_User_Stats::GetGlobalStatHistory( const char *pchStatName, STEAM_ARRAY_COUNT(cubData) double *pData, uint32 cubData ) { PRINT_DEBUG_TODO(); std::lock_guard lock(global_mutex); return 0; } // --- steam callbacks void Steam_User_Stats::send_updated_stats() { if (pending_server_updates.user_stats().empty() && pending_server_updates.user_achievements().empty()) return; if (settings->disable_sharing_stats_with_gameserver) 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::UpdateUserStatsFromUser); 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 gameservers on the network because we don't know the server steamid network->sendToAllGameservers(&msg, true); PRINT_DEBUG("sent updated stats: %zu stats, %zu achievements", new_updates_msg->user_stats().size(), new_updates_msg->user_achievements().size() ); } // --- networking callbacks // only triggered when we have a message // server wants all stats void Steam_User_Stats::network_stats_initial(Common_Message *msg) { if (!msg->gameserver_stats_messages().has_initial_user_stats()) { PRINT_DEBUG("error empty msg"); 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("Request_AllUserStats unhandled stat type %i", (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]; // achieved or not 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("server requested all stats, sent %zu stats, %zu achievements", initial_stats_msg->all_data().user_stats().size(), initial_stats_msg->all_data().user_achievements().size() ); } // server has updated/new stats void Steam_User_Stats::network_stats_updated(Common_Message *msg) { if (!msg->gameserver_stats_messages().has_update_user_stats()) { PRINT_DEBUG("error empty msg"); return; } 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("UpdateUserStats unhandled stat type %i", (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("server sent updated user stats, %zu stats, %zu achievements", new_user_data.user_stats().size(), new_user_data.user_achievements().size() ); } void Steam_User_Stats::network_callback_stats(Common_Message *msg) { // network->sendToAll() sends to current user also if (msg->source_id() == settings->get_local_steam_id().ConvertToUint64()) return; uint64 server_steamid = msg->source_id(); switch (msg->gameserver_stats_messages().type()) { // server wants all stats case GameServerStats_Messages::Request_AllUserStats: network_stats_initial(msg); break; // server has updated/new stats case GameServerStats_Messages::UpdateUserStatsFromServer: network_stats_updated(msg); break; // a user has updated/new stats case GameServerStats_Messages::UpdateUserStatsFromUser: // nothing break; default: PRINT_DEBUG("unhandled type %i", (int)msg->gameserver_stats_messages().type()); break; } }