* 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
This commit is contained in:
otavepto 2024-03-23 07:22:47 +02:00 committed by otavepto
parent 75bb4ff03d
commit b6c7df40b6
12 changed files with 2412 additions and 1209 deletions

View File

@ -4,14 +4,16 @@
- the above command introduced the ability to run without root - the above command introduced the ability to run without root
- if the script was ran without root, and `-packages_skip` wasn't specified, - 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 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, * for windows: updated stub drm patterns and added a workaround for older variants,
this increases the compatibility, but makes it easier to be detected this increases the compatibility, but makes it easier to be detected
* new stub dll `GameOverlayRenderer` for the experiemntal steamclient setup, * 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. some apps verify the existence of this dll, either on disk, or inside their memory space.
not recommended to ignore it **not recommended** to ignore it
* allow overlay invitations to obscure game input to be able to accept/reject the request * 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` * 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-` * 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
--- ---

View File

@ -62,12 +62,13 @@ enum Callback_Ids {
CALLBACK_ID_NETWORKING_SOCKETS, CALLBACK_ID_NETWORKING_SOCKETS,
CALLBACK_ID_STEAM_MESSAGES, CALLBACK_ID_STEAM_MESSAGES,
CALLBACK_ID_NETWORKING_MESSAGES, CALLBACK_ID_NETWORKING_MESSAGES,
CALLBACK_ID_GAMESERVER_STATS,
CALLBACK_IDS_MAX CALLBACK_IDS_MAX
}; };
struct Network_Callback_Container { struct Network_Callback_Container {
std::vector<struct Network_Callback> callbacks; std::vector<struct Network_Callback> callbacks{};
}; };
struct TCP_Socket { struct TCP_Socket {
@ -131,12 +132,23 @@ public:
void addListenId(CSteamID id); void addListenId(CSteamID id);
void setAppID(uint32 appid); void setAppID(uint32 appid);
void Run(); 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); 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); 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); 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 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); 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 getIP(CSteamID id);
uint32 getOwnIP(); uint32 getOwnIP();

View File

@ -78,14 +78,8 @@ struct Leaderboard_config {
enum ELeaderboardDisplayType display_type; enum ELeaderboardDisplayType display_type;
}; };
enum Stat_Type {
STAT_TYPE_INT,
STAT_TYPE_FLOAT,
STAT_TYPE_AVGRATE
};
struct Stat_config { struct Stat_config {
enum Stat_Type type; GameServerStats_Messages::StatInfo::Stat_Type type;
union { union {
float default_value_float; float default_value_float;
int32 default_value_int; int32 default_value_int;

View File

@ -26,8 +26,52 @@ class Steam_GameServerStats : public ISteamGameServerStats
class Networking *network; class Networking *network;
class SteamCallResults *callback_results; class SteamCallResults *callback_results;
class SteamCallBacks *callbacks; 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<std::string, CachedStat> stats{};
std::map<std::string, CachedAchievement> achievements{};
};
std::vector<RequestAllStats> pending_RequestUserStats{};
std::map<uint64, UserData> 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: 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 // downloads stats for the user
// returns a GSStatsReceived_t callback when completed // returns a GSStatsReceived_t callback when completed
// if the user has no stats, GSStatsReceived_t.m_eResult will be set to k_EResultFail // if the user has no stats, GSStatsReceived_t.m_eResult will be set to k_EResultFail

File diff suppressed because it is too large Load Diff

View File

@ -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<string, StatInfo> user_stats = 1;
map<string, AchievementInfo> 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 { message Common_Message {
uint64 source_id = 1; // SteamID64 of the sender uint64 source_id = 1; // SteamID64 of the sender
uint64 dest_id = 2; // SteamID64 of the target receiver uint64 dest_id = 2; // SteamID64 of the target receiver
@ -232,6 +285,7 @@ message Common_Message {
Networking_Sockets networking_sockets = 13; Networking_Sockets networking_sockets = 13;
Steam_Messages steam_messages = 14; Steam_Messages steam_messages = 14;
Networking_Messages networking_messages = 15; Networking_Messages networking_messages = 15;
GameServerStats_Messages gameserver_stats_messages = 16;
} }
uint32 source_ip = 128; uint32 source_ip = 128;

View File

@ -584,6 +584,12 @@ void Networking::do_callbacks_message(Common_Message *msg)
PRINT_DEBUG("Networking has_networking_messages\n"); PRINT_DEBUG("Networking has_networking_messages\n");
run_callbacks(CALLBACK_ID_NETWORKING_MESSAGES, msg); 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) 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); bool is_local_ip = ((ip >> 24) == 0x7F);
uint32_t local_ip = getIP(ids.front()); 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 //TODO: actually send to ip/port
for (auto &conn: connections) { for (auto &conn: connections) {
if (ntohl(conn.tcp_ip_port.ip) == ip || (is_local_ip && ntohl(conn.tcp_ip_port.ip) == local_ip)) { 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; bool ret = false;
CSteamID dest_id((uint64)msg->dest_id()); CSteamID dest_id((uint64)msg->dest_id());
if (std::find(ids.begin(), ids.end(), dest_id) != ids.end()) { 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) { if (!conn) {
PRINT_DEBUG("local send\n"); PRINT_DEBUG("Networking local send\n");
local_send.push_back(*msg); local_send.push_back(*msg);
ret = true; ret = true;
} }
@ -1278,7 +1284,11 @@ bool Networking::sendToAll(Common_Message *msg, bool reliable)
void Networking::run_callbacks(Callback_Ids id, Common_Message *msg) void Networking::run_callbacks(Callback_Ids id, Common_Message *msg)
{ {
for (auto &cb : callbacks[id].callbacks) { 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); 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; if (id >= CALLBACK_IDS_MAX) return false;
struct Network_Callback nc; struct Network_Callback nc{};
nc.message_callback = message_callback; nc.message_callback = message_callback;
nc.object = object; nc.object = object;
nc.steam_id = steam_id; nc.steam_id = steam_id;
@ -1314,6 +1324,24 @@ bool Networking::setCallback(Callback_Ids id, CSteamID steam_id, void (*message_
return true; 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() uint32 Networking::getOwnIP()
{ {
return own_ip; return own_ip;

View File

@ -666,13 +666,13 @@ static void parse_stats(class Settings *settings_client, Settings *settings_serv
try { try {
if (stat_type == "float") { 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); config.default_value_float = std::stof(stat_default_value);
} else if (stat_type == "int") { } 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); config.default_value_int = std::stol(stat_default_value);
} else if (stat_type == "avgrate") { } 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); config.default_value_float = std::stof(stat_default_value);
} else { } else {
PRINT_DEBUG("Error adding stat %s, type %s isn't valid\n", stat_name.c_str(), stat_type.c_str()); PRINT_DEBUG("Error adding stat %s, type %s isn't valid\n", stat_name.c_str(), stat_type.c_str());

View File

@ -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 = 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_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_apps = new Steam_Apps(settings_client, callback_results_client);
steam_networking = new Steam_Networking(settings_client, network, callbacks_client, run_every_runcb); 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); 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"); PRINT_DEBUG("client init gameserver\n");
steam_gameserver = new Steam_GameServer(settings_server, network, callbacks_server); steam_gameserver = new Steam_GameServer(settings_server, network, callbacks_server);
steam_gameserver_utils = new Steam_Utils(settings_server, callback_results_server, steam_overlay); 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_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_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); 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; gameserver_has_ipv6_functions = false;
last_cb_run = 0; last_cb_run = 0;
PRINT_DEBUG("Steam_Client init end ----------\n"); PRINT_DEBUG("Steam_Client init end *********\n");
} }
Steam_Client::~Steam_Client() Steam_Client::~Steam_Client()
@ -1914,7 +1914,7 @@ void Steam_Client::RunCallbacks(bool runClientCB, bool runGameserverCB, bool run
callbacks_client->runCallBacks(); callbacks_client->runCallBacks();
last_cb_run = std::chrono::duration_cast<std::chrono::duration<unsigned long long>>(std::chrono::system_clock::now().time_since_epoch()).count(); last_cb_run = std::chrono::duration_cast<std::chrono::duration<unsigned long long>>(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() void Steam_Client::DestroyAllInterfaces()

View File

@ -56,8 +56,12 @@ bool Steam_GameServer::InitGameServer( uint32 unIP, uint16 usGamePort, uint16 us
std::string version(pchVersionString); std::string version(pchVersionString);
version.erase(std::remove(version.begin(), version.end(), ' '), version.end()); version.erase(std::remove(version.begin(), version.end(), ' '), version.end());
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 { 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 (...) { } catch (...) {
PRINT_DEBUG("Steam_GameServer::InitGameServer: not a number: %s\n", pchVersionString); PRINT_DEBUG("Steam_GameServer::InitGameServer: not a number: %s\n", pchVersionString);
server_data.set_version(0); server_data.set_version(0);

View File

@ -17,14 +17,90 @@
#include "dll/steam_gameserverstats.h" #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<const std::string, CachedStat> &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<const std::string, CachedAchievement> &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->settings = settings;
this->network = network; this->network = network;
this->callback_results = callback_results; this->callback_results = callback_results;
this->callbacks = callbacks; 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 // downloads stats for the user
// returns a GSStatsReceived_t callback when completed // returns a GSStatsReceived_t callback when completed
// if the user has no stats, GSStatsReceived_t.m_eResult will be set to k_EResultFail // 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 ) STEAM_CALL_RESULT( GSStatsReceived_t )
SteamAPICall_t Steam_GameServerStats::RequestUserStats( CSteamID steamIDUser ) 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<std::recursive_mutex> lock(global_mutex); std::lock_guard<std::recursive_mutex> lock(global_mutex);
GSStatsReceived_t data{}; struct RequestAllStats new_request{};
data.m_eResult = k_EResultFail;//k_EResultOK; new_request.created = std::chrono::high_resolution_clock::now();
data.m_steamIDUser = steamIDUser; new_request.steamAPICall = callback_results->reserveCallResult();
return callback_results->addCallResult(data.k_iCallback, &data, sizeof(data)); 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() // requests stat information for a user, usable after a successful call to RequestUserStats()
bool Steam_GameServerStats::GetUserStat( CSteamID steamIDUser, const char *pchName, int32 *pData ) bool Steam_GameServerStats::GetUserStat( CSteamID steamIDUser, const char *pchName, int32 *pData )
{ {
PRINT_DEBUG("Steam_GameServerStats::GetUserStat\n"); PRINT_DEBUG("Steam_GameServerStats::GetUserStat <int32> %llu '%s' %p\n", (uint64)steamIDUser.ConvertToUint64(), pchName, pData);
return false; std::lock_guard<std::recursive_mutex> 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 ) bool Steam_GameServerStats::GetUserStat( CSteamID steamIDUser, const char *pchName, float *pData )
{ {
PRINT_DEBUG("Steam_GameServerStats::GetUserStat\n"); PRINT_DEBUG("Steam_GameServerStats::GetUserStat <float> %llu '%s' %p\n", (uint64)steamIDUser.ConvertToUint64(), pchName, pData);
return false; std::lock_guard<std::recursive_mutex> 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 ) bool Steam_GameServerStats::GetUserAchievement( CSteamID steamIDUser, const char *pchName, bool *pbAchieved )
{ {
PRINT_DEBUG("Steam_GameServerStats::GetUserAchievement\n"); PRINT_DEBUG("Steam_GameServerStats::GetUserAchievement %llu '%s' %p\n", (uint64)steamIDUser.ConvertToUint64(), pchName, pbAchieved);
return false; std::lock_guard<std::recursive_mutex> 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 // Set the IP range of your official servers on the Steamworks page
bool Steam_GameServerStats::SetUserStat( CSteamID steamIDUser, const char *pchName, int32 nData ) bool Steam_GameServerStats::SetUserStat( CSteamID steamIDUser, const char *pchName, int32 nData )
{ {
PRINT_DEBUG("Steam_GameServerStats::SetUserStat\n"); PRINT_DEBUG("Steam_GameServerStats::SetUserStat <int32> %llu '%s' %i\n", (uint64)steamIDUser.ConvertToUint64(), pchName, nData);
return false; std::lock_guard<std::recursive_mutex> 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 ) bool Steam_GameServerStats::SetUserStat( CSteamID steamIDUser, const char *pchName, float fData )
{ {
PRINT_DEBUG("Steam_GameServerStats::SetUserStat\n"); PRINT_DEBUG("Steam_GameServerStats::SetUserStat <float> %llu '%s' %f\n", (uint64)steamIDUser.ConvertToUint64(), pchName, fData);
return false; std::lock_guard<std::recursive_mutex> 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 ) bool Steam_GameServerStats::UpdateUserAvgRateStat( CSteamID steamIDUser, const char *pchName, float flCountThisSession, double dSessionLength )
{ {
PRINT_DEBUG("Steam_GameServerStats::UpdateUserAvgRateStat\n"); PRINT_DEBUG("Steam_GameServerStats::UpdateUserAvgRateStat %llu '%s'\n", (uint64)steamIDUser.ConvertToUint64(), pchName);
return false; std::lock_guard<std::recursive_mutex> 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 ) bool Steam_GameServerStats::SetUserAchievement( CSteamID steamIDUser, const char *pchName )
{ {
PRINT_DEBUG("Steam_GameServerStats::SetUserAchievement\n"); PRINT_DEBUG("Steam_GameServerStats::SetUserAchievement %llu '%s'\n", (uint64)steamIDUser.ConvertToUint64(), pchName);
return false; std::lock_guard<std::recursive_mutex> 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 ) bool Steam_GameServerStats::ClearUserAchievement( CSteamID steamIDUser, const char *pchName )
{ {
PRINT_DEBUG("Steam_GameServerStats::ClearUserAchievement\n"); PRINT_DEBUG("Steam_GameServerStats::ClearUserAchievement %llu '%s'\n", (uint64)steamIDUser.ConvertToUint64(), pchName);
return false; std::lock_guard<std::recursive_mutex> 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 ) STEAM_CALL_RESULT( GSStatsStored_t )
SteamAPICall_t Steam_GameServerStats::StoreUserStats( CSteamID steamIDUser ) SteamAPICall_t Steam_GameServerStats::StoreUserStats( CSteamID steamIDUser )
{ {
// it's not necessary to send all data here
PRINT_DEBUG("Steam_GameServerStats::StoreUserStats\n"); PRINT_DEBUG("Steam_GameServerStats::StoreUserStats\n");
std::lock_guard<std::recursive_mutex> lock(global_mutex); std::lock_guard<std::recursive_mutex> lock(global_mutex);
GSStatsStored_t data; GSStatsStored_t data{};
data.m_eResult = k_EResultOK;
if (all_users_data.count(steamIDUser.ConvertToUint64())) {
data.m_eResult = EResult::k_EResultOK;
} else {
data.m_eResult = EResult::k_EResultFail;
}
data.m_steamIDUser = steamIDUser; 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<uint64, UserData> 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 &current_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 &current_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 &current_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 &current_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 &current_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;
}
} }

1527
dll/steam_user_stats.cpp Normal file

File diff suppressed because it is too large Load Diff