From 08f2bc36e06986450922a07b466de252ef4d0999 Mon Sep 17 00:00:00 2001 From: a Date: Sat, 3 Aug 2024 07:02:39 +0300 Subject: [PATCH 1/5] helper function to str replace substr --- helpers/common_helpers.cpp | 28 +++++++++++++++++++++++ helpers/common_helpers/common_helpers.hpp | 2 ++ 2 files changed, 30 insertions(+) diff --git a/helpers/common_helpers.cpp b/helpers/common_helpers.cpp index 198dc763..58ac1e15 100644 --- a/helpers/common_helpers.cpp +++ b/helpers/common_helpers.cpp @@ -499,3 +499,31 @@ std::string common_helpers::to_str(std::wstring_view wstr) return {}; } + +std::string common_helpers::str_replace_all(std::string_view source, std::string_view substr, std::string_view replace) +{ + if (source.empty() || substr.empty()) return std::string(source); + + std::string out{}; + out.reserve(source.size() / 4); // out could be bigger or smaller than source, start small + + size_t start_offset = 0; + auto f_idx = source.find(substr); + while (std::string::npos != f_idx) { + // copy the chars before the match + auto chars_count_until_match = f_idx - start_offset; + out.append(source, start_offset, chars_count_until_match); + // copy the replace str + out.append(replace); + + // adjust the start offset to point at the char after this match + start_offset = f_idx + substr.size(); + // search for next match + f_idx = source.find(substr, start_offset); + } + + // copy last remaining part + out.append(source, start_offset, std::string::npos); + + return out; +} diff --git a/helpers/common_helpers/common_helpers.hpp b/helpers/common_helpers/common_helpers.hpp index ea30c4ed..7d6707a0 100644 --- a/helpers/common_helpers/common_helpers.hpp +++ b/helpers/common_helpers/common_helpers.hpp @@ -110,4 +110,6 @@ std::string get_utc_time(); std::wstring to_wstr(std::string_view str); std::string to_str(std::wstring_view wstr); +std::string str_replace_all(std::string_view source, std::string_view substr, std::string_view replace); + } From a647c6cfddd58a0afc6d3b9812f99882235d4835 Mon Sep 17 00:00:00 2001 From: a Date: Sat, 3 Aug 2024 07:05:51 +0300 Subject: [PATCH 2/5] new ini option `steam_game_stats_reports_dir` to define a folder where statistics from ISteamGameStats will be saved --- dll/dll/settings.h | 3 +++ dll/settings_parser.cpp | 15 +++++++++++++++ .../configs.main.EXAMPLE.ini | 4 ++++ 3 files changed, 22 insertions(+) diff --git a/dll/dll/settings.h b/dll/dll/settings.h index d0450bd2..584bafd1 100644 --- a/dll/dll/settings.h +++ b/dll/dll/settings.h @@ -295,6 +295,9 @@ public: // synchronize user stats/achievements with game servers as soon as possible instead of caching them. bool immediate_gameserver_stats = false; + // steam_game_stats + std::string steam_game_stats_reports_dir{}; + //overlay bool disable_overlay = true; int overlay_hook_delay_sec = 0; // "Saints Row (2022)" needs a lot of time to initialize, otherwise detection will fail diff --git a/dll/settings_parser.cpp b/dll/settings_parser.cpp index 772b7c13..c778dd9d 100644 --- a/dll/settings_parser.cpp +++ b/dll/settings_parser.cpp @@ -1345,6 +1345,20 @@ static void parse_overlay_general_config(class Settings *settings_client, class } +// main::general::steam_game_stats_reports_dir +static void parse_steam_game_stats_reports_dir(class Settings *settings_client, class Settings *settings_server) +{ + std::string line(common_helpers::string_strip(ini.GetValue("main::general", "steam_game_stats_reports_dir", ""))); + if (line.size()) { + auto folder = common_helpers::to_absolute(line, get_full_program_path()); + if (folder.size()) { + PRINT_DEBUG("ISteamGameStats reports will be saved to '%s'", folder.c_str()); + settings_client->steam_game_stats_reports_dir = folder; + settings_server->steam_game_stats_reports_dir = folder; + } + } +} + // mainly enable/disable features static void parse_simple_features(class Settings *settings_client, class Settings *settings_server) { @@ -1697,6 +1711,7 @@ uint32 create_localstorage_settings(Settings **settings_client_out, Settings **s parse_overlay_general_config(settings_client, settings_server); load_overlay_appearance(settings_client, settings_server, local_storage); + parse_steam_game_stats_reports_dir(settings_client, settings_server); *settings_client_out = settings_client; *settings_server_out = settings_server; diff --git a/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini b/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini index c3a3290b..a053b983 100644 --- a/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini +++ b/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini @@ -57,6 +57,10 @@ matchmaking_server_details_via_source_query=0 # this is intended to debug some annoying scenarios, and best used with the debug build of the emu # default= crash_printer_location=./path/relative/to/dll/crashes.txt +# some Source-based games use the interface ISteamGameStats to report some stats +# you can make the emu save this data to a folder +# empty value = don't save anything (default), otherwise the path specified must be writable +steam_game_stats_reports_dir=./path/relative/to/dll/ [main::connectivity] # 1=prevent hooking OS networking APIs and allow any external requests From a372a2ddb4dc27f7262f86f406c6c0a4312ca361 Mon Sep 17 00:00:00 2001 From: a Date: Sat, 3 Aug 2024 07:06:49 +0300 Subject: [PATCH 3/5] save statistics from ISteamGameStats to disk in a csv file --- dll/dll/steam_gamestats.h | 7 ++- dll/steam_gamestats.cpp | 116 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/dll/dll/steam_gamestats.h b/dll/dll/steam_gamestats.h index 54b9da67..a0c12b8f 100644 --- a/dll/dll/steam_gamestats.h +++ b/dll/dll/steam_gamestats.h @@ -64,11 +64,14 @@ private: struct Session_t { + bool ended = false; + bool saved_to_disk = false; + uint64 account_id{}; + EGameStatsAccountType nAccountType{}; RTime32 rtTimeStarted{}; RTime32 rtTimeEnded{}; int nReasonCode{}; - bool ended = false; std::map attributes{}; std::vector> tables{}; @@ -90,6 +93,8 @@ private: Attribute_t *get_or_create_row_att(uint64 ulRowID, const char *att_name, Table_t &table, AttributeType_t type_if_create); Session_t* get_last_active_session(); + std::string sanitize_csv_value(std::string_view value); + void save_session_to_disk(Steam_GameStats::Session_t &session, uint64 session_id); void steam_run_callback(); // user connect/disconnect diff --git a/dll/steam_gamestats.cpp b/dll/steam_gamestats.cpp index 9039acb3..efe5cd3e 100644 --- a/dll/steam_gamestats.cpp +++ b/dll/steam_gamestats.cpp @@ -22,6 +22,7 @@ */ #include "dll/steam_gamestats.h" +#include Steam_GameStats::Attribute_t::Attribute_t(AttributeType_t type) @@ -194,6 +195,7 @@ SteamAPICall_t Steam_GameStats::GetNewSession( int8 nAccountType, uint64 ulAccou auto session_id = create_session_id(); Session_t new_session{}; + new_session.account_id = ulAccountID; new_session.nAccountType = (EGameStatsAccountType)nAccountType; new_session.rtTimeStarted = rtTimeStarted; sessions.insert_or_assign(session_id, new_session); @@ -484,6 +486,113 @@ EResult Steam_GameStats::AddRowAttributeInt64( uint64 ulRowID, const char *pstrN // --- steam callbacks +std::string Steam_GameStats::sanitize_csv_value(std::string_view value) +{ + // ref: https://en.wikipedia.org/wiki/Comma-separated_values + // double quotes must be represented by a pair of double quotes + auto val_str = common_helpers::str_replace_all(value, "\"", "\"\""); + // multiline values aren't supported by all parsers + val_str = common_helpers::str_replace_all(val_str, "\r\n", "\n"); + val_str = common_helpers::str_replace_all(val_str, "\n", " "); + return val_str; +} + +void Steam_GameStats::save_session_to_disk(Steam_GameStats::Session_t &session, uint64 session_id) +{ + auto folder = std::to_string(session.account_id) + "_" + std::to_string(session.rtTimeStarted) + "_" + std::to_string(session_id); + auto folder_p = std::filesystem::u8path(settings->steam_game_stats_reports_dir) / std::filesystem::u8path(folder); + auto folder_u8_str = folder_p.u8string(); + + // save session attributes + if (session.attributes.size()) { + std::stringstream ss{}; + ss << "Session attribute,Value\n"; + for (const auto& [name, val] : session.attributes) { + std::string val_str{}; + switch (val.type) { + case AttributeType_t::Int: val_str = std::to_string(val.n_data); break; + case AttributeType_t::Str: val_str = val.s_data; break; + case AttributeType_t::Float: val_str = std::to_string(val.f_data); break; + case AttributeType_t::Int64: val_str = std::to_string(val.ll_data); break; + } + + val_str = sanitize_csv_value(val_str); + auto name_str = sanitize_csv_value(name); + ss << "\"" << name_str << "\",\"" << val_str << "\"\n"; + } + auto ss_str = ss.str(); + Local_Storage::store_file_data(folder_u8_str, "session_attributes.csv", ss_str.c_str(), ss_str.size()); + } + + // save each table + for (const auto& [table_name, table_data] : session.tables) { + bool rows_has_attributes = std::any_of(table_data.rows.begin(), table_data.rows.end(), [](const Steam_GameStats::Row_t &item) { + return item.attributes.size() > 0; + }); + + if (!rows_has_attributes) continue; + + // convert the data representation to be column oriented + // key=column header/title + // value = list of column values + std::unordered_map> columns{}; + for (size_t row_idx = 0; row_idx < table_data.rows.size(); ++row_idx) { + const auto &row = table_data.rows[row_idx]; + for (const auto& [att_name, att_val] : row.attributes) { + auto [column_it, new_column] = columns.emplace(att_name, std::vector{}); + auto &column_values = column_it->second; + // when adding new column make sure we have correct rows count + if (new_column) { + column_values.assign(table_data.rows.size(), nullptr); + } + // add the row value in its correct place + column_values[row_idx] = &att_val; + } + } + + std::stringstream ss_table{}; + // write header + bool first_header_atom = true; + for (const auto& [col_name, _] : columns) { + auto csv_col_name = sanitize_csv_value(col_name); + if (first_header_atom) { + first_header_atom = false; + ss_table << "\"" << csv_col_name << "\""; + } else { + ss_table << ",\"" << csv_col_name << "\""; + } + } + ss_table << "\n"; + // write values + for (size_t row_idx = 0; row_idx < table_data.rows.size(); ++row_idx) { + bool first_col_cell = true; + for (const auto& [_, col_values] : columns) { + auto &cell_val = col_values[row_idx]; + + std::string val_str{}; + switch (cell_val->type) { + case AttributeType_t::Int: val_str = std::to_string(cell_val->n_data); break; + case AttributeType_t::Str: val_str = cell_val->s_data; break; + case AttributeType_t::Float: val_str = std::to_string(cell_val->f_data); break; + case AttributeType_t::Int64: val_str = std::to_string(cell_val->ll_data); break; + } + + val_str = sanitize_csv_value(val_str); + if (first_col_cell) { + first_col_cell = false; + ss_table << "\"" << val_str << "\""; + } else { + ss_table << ",\"" << val_str << "\""; + } + } + } + ss_table << "\n"; + auto ss_str = ss_table.str(); + Local_Storage::store_file_data(folder_u8_str, table_name.c_str(), ss_str.c_str(), ss_str.size()); + + } +} + void Steam_GameStats::steam_run_callback() { // remove ended sessions that are inactive @@ -496,6 +605,13 @@ void Steam_GameStats::steam_run_callback() auto &session = session_it->second; if (session.ended) { + if (!session.saved_to_disk) { + session.saved_to_disk = true; + if (settings->steam_game_stats_reports_dir.size()) { + save_session_to_disk(session, session_it->first); + } + } + if ( (now_epoch_sec.count() - (long long)session.rtTimeEnded) >= MAX_DEAD_SESSION_SECONDS ) { should_remove = true; PRINT_DEBUG("removing outdated session id=%llu", session_it->first); From 6066d0cbe884e41a0a24ec418a5189898e9a8896 Mon Sep 17 00:00:00 2001 From: a Date: Sat, 3 Aug 2024 19:12:58 +0300 Subject: [PATCH 4/5] move the ini option `steam_game_stats_reports_dir` to the section [main::misc] --- dll/settings_parser.cpp | 4 ++-- .../steam_settings.EXAMPLE/configs.main.EXAMPLE.ini | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/dll/settings_parser.cpp b/dll/settings_parser.cpp index c778dd9d..a7552703 100644 --- a/dll/settings_parser.cpp +++ b/dll/settings_parser.cpp @@ -1345,10 +1345,10 @@ static void parse_overlay_general_config(class Settings *settings_client, class } -// main::general::steam_game_stats_reports_dir +// main::misc::steam_game_stats_reports_dir static void parse_steam_game_stats_reports_dir(class Settings *settings_client, class Settings *settings_server) { - std::string line(common_helpers::string_strip(ini.GetValue("main::general", "steam_game_stats_reports_dir", ""))); + std::string line(common_helpers::string_strip(ini.GetValue("main::misc", "steam_game_stats_reports_dir", ""))); if (line.size()) { auto folder = common_helpers::to_absolute(line, get_full_program_path()); if (folder.size()) { diff --git a/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini b/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini index a053b983..31754f42 100644 --- a/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini +++ b/post_build/steam_settings.EXAMPLE/configs.main.EXAMPLE.ini @@ -57,10 +57,6 @@ matchmaking_server_details_via_source_query=0 # this is intended to debug some annoying scenarios, and best used with the debug build of the emu # default= crash_printer_location=./path/relative/to/dll/crashes.txt -# some Source-based games use the interface ISteamGameStats to report some stats -# you can make the emu save this data to a folder -# empty value = don't save anything (default), otherwise the path specified must be writable -steam_game_stats_reports_dir=./path/relative/to/dll/ [main::connectivity] # 1=prevent hooking OS networking APIs and allow any external requests @@ -119,3 +115,9 @@ disable_steamoverlaygameid_env_var=0 # https://developer.valvesoftware.com/wiki/Dedicated_Servers_List # default=0 enable_steam_preowned_ids=0 +# some Source-based games use the interface `ISteamGameStats` to report some stats +# you can make the emu save this data to a folder +# empty value = don't save anything (default) +# the emu will create the folders if they are missing but the path specified must be writable +# default= +steam_game_stats_reports_dir=./path/relative/to/dll/ From 63280612120097174910be0779a322e1afb6238f Mon Sep 17 00:00:00 2001 From: a Date: Sat, 10 Aug 2024 11:43:45 +0300 Subject: [PATCH 5/5] fix vs warnings --- dll/steam_gamestats.cpp | 4 ++-- dll/steam_matchmaking.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dll/steam_gamestats.cpp b/dll/steam_gamestats.cpp index efe5cd3e..297d0ccc 100644 --- a/dll/steam_gamestats.cpp +++ b/dll/steam_gamestats.cpp @@ -521,7 +521,7 @@ void Steam_GameStats::save_session_to_disk(Steam_GameStats::Session_t &session, ss << "\"" << name_str << "\",\"" << val_str << "\"\n"; } auto ss_str = ss.str(); - Local_Storage::store_file_data(folder_u8_str, "session_attributes.csv", ss_str.c_str(), ss_str.size()); + Local_Storage::store_file_data(folder_u8_str, "session_attributes.csv", ss_str.c_str(), (unsigned int)ss_str.size()); } // save each table @@ -588,7 +588,7 @@ void Steam_GameStats::save_session_to_disk(Steam_GameStats::Session_t &session, } ss_table << "\n"; auto ss_str = ss_table.str(); - Local_Storage::store_file_data(folder_u8_str, table_name.c_str(), ss_str.c_str(), ss_str.size()); + Local_Storage::store_file_data(folder_u8_str, table_name.c_str(), ss_str.c_str(), (unsigned int)ss_str.size()); } } diff --git a/dll/steam_matchmaking.cpp b/dll/steam_matchmaking.cpp index c359473b..b3e3a50f 100644 --- a/dll/steam_matchmaking.cpp +++ b/dll/steam_matchmaking.cpp @@ -362,7 +362,7 @@ int Steam_Matchmaking::AddFavoriteGame( AppId_t nAppID, uint32 nIP, uint16 nConn directory_path = file_path.substr(0, file_directory); file_name = file_path.substr(file_directory); } - Local_Storage::store_file_data(directory_path, file_name, (char *)list.data(), list.size()); + Local_Storage::store_file_data(directory_path, file_name, (char *)list.data(), (unsigned int)list.size()); ++list_lines; return static_cast(list_lines); @@ -379,7 +379,7 @@ int Steam_Matchmaking::AddFavoriteGame( AppId_t nAppID, uint32 nIP, uint16 nConn directory_path = file_path.substr(0, file_directory); file_name = file_path.substr(file_directory); } - Local_Storage::store_file_data(directory_path, file_name, (char *)newip_string.data(), newip_string.size()); + Local_Storage::store_file_data(directory_path, file_name, (char *)newip_string.data(), (unsigned int)newip_string.size()); return 1; } @@ -429,7 +429,7 @@ bool Steam_Matchmaking::RemoveFavoriteGame( AppId_t nAppID, uint32 nIP, uint16 n directory_path = file_path.substr(0, file_directory); file_name = file_path.substr(file_directory); } - Local_Storage::store_file_data(directory_path, file_name, (char *)list.data(), list.size()); + Local_Storage::store_file_data(directory_path, file_name, (char *)list.data(), (unsigned int)list.size()); return true; }