mirror of
https://github.com/Detanup01/gbe_fork.git
synced 2024-11-23 11:15:34 +08:00
Merge pull request #7 from otavepto/patch-game-stats-csv
Allow saving stats from `ISteamGameStats` to .csv files
This commit is contained in:
commit
a8bebdde0a
@ -300,6 +300,9 @@ public:
|
|||||||
// synchronize user stats/achievements with game servers as soon as possible instead of caching them.
|
// synchronize user stats/achievements with game servers as soon as possible instead of caching them.
|
||||||
bool immediate_gameserver_stats = false;
|
bool immediate_gameserver_stats = false;
|
||||||
|
|
||||||
|
// steam_game_stats
|
||||||
|
std::string steam_game_stats_reports_dir{};
|
||||||
|
|
||||||
//overlay
|
//overlay
|
||||||
bool disable_overlay = true;
|
bool disable_overlay = true;
|
||||||
int overlay_hook_delay_sec = 0; // "Saints Row (2022)" needs a lot of time to initialize, otherwise detection will fail
|
int overlay_hook_delay_sec = 0; // "Saints Row (2022)" needs a lot of time to initialize, otherwise detection will fail
|
||||||
|
@ -64,11 +64,14 @@ private:
|
|||||||
|
|
||||||
struct Session_t
|
struct Session_t
|
||||||
{
|
{
|
||||||
|
bool ended = false;
|
||||||
|
bool saved_to_disk = false;
|
||||||
|
uint64 account_id{};
|
||||||
|
|
||||||
EGameStatsAccountType nAccountType{};
|
EGameStatsAccountType nAccountType{};
|
||||||
RTime32 rtTimeStarted{};
|
RTime32 rtTimeStarted{};
|
||||||
RTime32 rtTimeEnded{};
|
RTime32 rtTimeEnded{};
|
||||||
int nReasonCode{};
|
int nReasonCode{};
|
||||||
bool ended = false;
|
|
||||||
std::map<std::string, Attribute_t> attributes{};
|
std::map<std::string, Attribute_t> attributes{};
|
||||||
|
|
||||||
std::vector<std::pair<std::string, Table_t>> tables{};
|
std::vector<std::pair<std::string, Table_t>> 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);
|
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();
|
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();
|
void steam_run_callback();
|
||||||
|
|
||||||
// user connect/disconnect
|
// user connect/disconnect
|
||||||
|
@ -1345,6 +1345,20 @@ static void parse_overlay_general_config(class Settings *settings_client, class
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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::misc", "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
|
// mainly enable/disable features
|
||||||
static void parse_simple_features(class Settings *settings_client, class Settings *settings_server)
|
static void parse_simple_features(class Settings *settings_client, class Settings *settings_server)
|
||||||
{
|
{
|
||||||
@ -1705,6 +1719,7 @@ uint32 create_localstorage_settings(Settings **settings_client_out, Settings **s
|
|||||||
|
|
||||||
parse_overlay_general_config(settings_client, settings_server);
|
parse_overlay_general_config(settings_client, settings_server);
|
||||||
load_overlay_appearance(settings_client, settings_server, local_storage);
|
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_client_out = settings_client;
|
||||||
*settings_server_out = settings_server;
|
*settings_server_out = settings_server;
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "dll/steam_gamestats.h"
|
#include "dll/steam_gamestats.h"
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
|
||||||
Steam_GameStats::Attribute_t::Attribute_t(AttributeType_t type)
|
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();
|
auto session_id = create_session_id();
|
||||||
Session_t new_session{};
|
Session_t new_session{};
|
||||||
|
new_session.account_id = ulAccountID;
|
||||||
new_session.nAccountType = (EGameStatsAccountType)nAccountType;
|
new_session.nAccountType = (EGameStatsAccountType)nAccountType;
|
||||||
new_session.rtTimeStarted = rtTimeStarted;
|
new_session.rtTimeStarted = rtTimeStarted;
|
||||||
sessions.insert_or_assign(session_id, new_session);
|
sessions.insert_or_assign(session_id, new_session);
|
||||||
@ -484,6 +486,113 @@ EResult Steam_GameStats::AddRowAttributeInt64( uint64 ulRowID, const char *pstrN
|
|||||||
|
|
||||||
// --- steam callbacks
|
// --- 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(), (unsigned int)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<std::string, std::vector<const Attribute_t*>> 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<const Attribute_t*>{});
|
||||||
|
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(), (unsigned int)ss_str.size());
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Steam_GameStats::steam_run_callback()
|
void Steam_GameStats::steam_run_callback()
|
||||||
{
|
{
|
||||||
// remove ended sessions that are inactive
|
// remove ended sessions that are inactive
|
||||||
@ -496,6 +605,13 @@ void Steam_GameStats::steam_run_callback()
|
|||||||
|
|
||||||
auto &session = session_it->second;
|
auto &session = session_it->second;
|
||||||
if (session.ended) {
|
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 ) {
|
if ( (now_epoch_sec.count() - (long long)session.rtTimeEnded) >= MAX_DEAD_SESSION_SECONDS ) {
|
||||||
should_remove = true;
|
should_remove = true;
|
||||||
PRINT_DEBUG("removing outdated session id=%llu", session_it->first);
|
PRINT_DEBUG("removing outdated session id=%llu", session_it->first);
|
||||||
|
@ -362,7 +362,7 @@ int Steam_Matchmaking::AddFavoriteGame( AppId_t nAppID, uint32 nIP, uint16 nConn
|
|||||||
directory_path = file_path.substr(0, file_directory);
|
directory_path = file_path.substr(0, file_directory);
|
||||||
file_name = file_path.substr(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;
|
++list_lines;
|
||||||
return static_cast<int>(list_lines);
|
return static_cast<int>(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);
|
directory_path = file_path.substr(0, file_directory);
|
||||||
file_name = file_path.substr(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;
|
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);
|
directory_path = file_path.substr(0, file_directory);
|
||||||
file_name = file_path.substr(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;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -477,3 +477,31 @@ std::string common_helpers::to_str(std::wstring_view wstr)
|
|||||||
|
|
||||||
return {};
|
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;
|
||||||
|
}
|
||||||
|
@ -110,4 +110,6 @@ std::string get_utc_time();
|
|||||||
std::wstring to_wstr(std::string_view str);
|
std::wstring to_wstr(std::string_view str);
|
||||||
std::string to_str(std::wstring_view wstr);
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -124,3 +124,9 @@ disable_steamoverlaygameid_env_var=0
|
|||||||
# https://developer.valvesoftware.com/wiki/Dedicated_Servers_List
|
# https://developer.valvesoftware.com/wiki/Dedicated_Servers_List
|
||||||
# default=0
|
# default=0
|
||||||
enable_steam_preowned_ids=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/
|
||||||
|
Loading…
Reference in New Issue
Block a user