initial commit of plugin just filling the tables with data for the playtime display

This commit is contained in:
jenz
2026-08-20 17:12:46 +02:00
parent e731a1f3fc
commit e1a93bd0dd
2 changed files with 383 additions and 0 deletions
@@ -0,0 +1,300 @@
#pragma semicolon 1
#define PLUGIN_AUTHOR "jenz"
#define PLUGIN_VERSION "1.0"
#include <sourcemod>
#include <geoip>
#include <mapchooser_extended>
#include <cstrike>
Database g_hDatabase;
bool g_bInsertedPlayerSession[MAXPLAYERS + 1];
bool g_bCorrectServer = false;
int g_iClientHours[MAXPLAYERS + 1];
int g_iClientMinutes[MAXPLAYERS + 1];
ArrayList g_hPendingVoteChoices;
public Plugin myinfo =
{
name = "UNLOZE_playtime_display",
author = PLUGIN_AUTHOR,
description = "collects all info for playtime display",
version = PLUGIN_VERSION,
url = "www.unloze.com"
};
enum struct VoteChoiceRecord
{
char SteamId[64];
char VoteChoice[64];
int VoteWeight;
}
public void OnPluginStart()
{
if (!g_hDatabase)
{
Database.Connect(SQL_OnDatabaseConnect, "unloze_playtimestats");
}
g_hPendingVoteChoices = new ArrayList(sizeof(VoteChoiceRecord));
g_bCorrectServer = GetConVarInt(FindConVar("hostport")) == 27015 ? true : false;
for (int i = 1; i <= MaxClients; i++)
{
if (IsValidClient(i) && !IsFakeClient(i))
{
g_bInsertedPlayerSession[i] = false;
}
}
}
public void SQL_OnDatabaseConnect(Database db, const char[] error, any data)
{
if(!db || strlen(error))
{
LogError("Database error: %s", error);
return;
}
g_hDatabase = db;
OnMapStart();
}
public void OnMapStart()
{
if (!g_bCorrectServer)
{
return;
}
if (!g_hDatabase)
{
Database.Connect(SQL_OnDatabaseConnect, "unloze_playtimestats");
return;
}
char sMapname[64];
GetCurrentMap(sMapname, sizeof(sMapname));
insert_map(sMapname);
}
public void insert_map(char sMapname[64])
{
char sQuery[512];
Format(sQuery, sizeof(sQuery), "INSERT INTO `playtime_display_map_history` (map_name) VALUES('%s')", sMapname);
g_hDatabase.Query(SQL_FinishedQuery, sQuery, _, DBPrio_Normal);
}
public void OnMapEnd()
{
if (!g_bCorrectServer)
{
return;
}
char sQuery[512];
Format(sQuery, sizeof(sQuery), "UPDATE `playtime_display_map_history` set map_end_dt = NOW() order by map_id desc limit 1");
g_hDatabase.Query(SQL_FinishedQuery, sQuery, _, DBPrio_Normal);
}
public void OnClientAuthorized(int client, const char[] sSteamID32)
{
g_bInsertedPlayerSession[client] = false; //the query will take a little to finish for playtime so should be save to set here
}
//forward is called from OnClientAuthorized but also from timer every 1 minute.
public void GetPlayerHoursServer(int client, int hours, int minutes)
{
if (!g_bCorrectServer)
{
return;
}
if (g_bInsertedPlayerSession[client])
{
//update the last session entry every time the forward is called, in case of server crashes this still means we have it up to date.
update_play_session(client, hours, minutes);
return;
}
g_iClientHours[client] = hours;
g_iClientMinutes[client] = minutes;
//chaining the calls
upsert_player(client);
}
public void update_play_session(int client, int client_hours, int client_minutes)
{
char sSID[64];
GetClientAuthId(client, AuthId_Steam2, sSID, sizeof(sSID));
char sQuery[512];
Format(sQuery, sizeof(sQuery), "UPDATE `playtime_display_sessions` SET session_end_playtime_minutes='%i', session_end_dt=NOW() WHERE steamid = '%s' order by session_id desc limit 1", (client_hours * 60) + client_minutes, sSID);
g_hDatabase.Query(SQL_FinishedQuery, sQuery, _, DBPrio_Normal);
}
public void upsert_player(int client)
{
char sName[MAX_NAME_LENGTH];
GetClientName(client, sName, sizeof(sName));
int size2 = 2 * strlen(sName) + 1;
char[] sEscapedName = new char[size2 + 1];
g_hDatabase.Escape(sName, sEscapedName, size2 + 1);
char sIP[32];
GetClientIP(client, sIP, sizeof(sIP));
char countryCode[3];
GeoipCode2(sIP, countryCode);
char sSID[64];
GetClientAuthId(client, AuthId_Steam2, sSID, sizeof(sSID));
char sQuery[512];
Format(sQuery, sizeof(sQuery), "INSERT INTO `playtime_display_players` (`steamid`, `current_name`, `current_country`) VALUES('%s', '%s', '%s') ON DUPLICATE KEY UPDATE `current_name` = '%s', `current_country` = '%s', `last_seen` = CURRENT_TIMESTAMP", sSID, sEscapedName, countryCode, sEscapedName, countryCode);
g_hDatabase.Query(SQL_FinishedQueryUpsertPlayerCallback, sQuery, GetClientSerial(client), DBPrio_Normal);
}
public void SQL_FinishedQueryUpsertPlayerCallback(Database db, DBResultSet results, const char[] error, int Serial)
{
if (!db)
{
delete results;
return;
}
int client;
if ((client = GetClientFromSerial(Serial)) == 0)
{
delete results;
return;
}
if (!IsValidClient(client))
{
delete results;
return;
}
upsert_name_history(client);
}
public void upsert_name_history(int client)
{
char sName[MAX_NAME_LENGTH];
GetClientName(client, sName, sizeof(sName));
int size2 = 2 * strlen(sName) + 1;
char[] sEscapedName = new char[size2 + 1];
g_hDatabase.Escape(sName, sEscapedName, size2 + 1);
char sSID[64];
GetClientAuthId(client, AuthId_Steam2, sSID, sizeof(sSID));
char sQuery[512];
Format(sQuery, sizeof(sQuery), "INSERT INTO `playtime_display_name_history` (`steamid`, `player_name`) VALUES('%s', '%s') ON DUPLICATE KEY UPDATE `player_name` = '%s', `last_seen` = CURRENT_TIMESTAMP", sSID, sEscapedName, sEscapedName);
g_hDatabase.Query(SQL_FinishedQueryUpsertNameHistoryCallback, sQuery, GetClientSerial(client), DBPrio_Normal);
}
public void SQL_FinishedQueryUpsertNameHistoryCallback(Database db, DBResultSet results, const char[] error, int Serial)
{
if (!db)
{
delete results;
return;
}
int client;
if ((client = GetClientFromSerial(Serial)) == 0)
{
delete results;
return;
}
if (!IsValidClient(client))
{
delete results;
return;
}
insert_play_session(client, g_iClientHours[client], g_iClientMinutes[client]);
}
public void insert_play_session(int client, int hours, int minutes)
{
g_bInsertedPlayerSession[client] = true;
char sName[MAX_NAME_LENGTH];
GetClientName(client, sName, sizeof(sName));
int size2 = 2 * strlen(sName) + 1;
char[] sEscapedName = new char[size2 + 1];
g_hDatabase.Escape(sName, sEscapedName, size2 + 1);
char sIP[32];
GetClientIP(client, sIP, sizeof(sIP));
char countryCode[3];
GeoipCode2(sIP, countryCode);
char sSID[64];
GetClientAuthId(client, AuthId_Steam2, sSID, sizeof(sSID));
char sQuery[512];
Format(sQuery, sizeof(sQuery), "INSERT INTO `playtime_display_sessions` (`steamid`, `country_code`, `player_name`, `session_start_playtime_minutes`) VALUES('%s', '%s','%s','%i')", sSID, countryCode, sEscapedName, (hours * 60) + minutes);
g_hDatabase.Query(SQL_FinishedQuery, sQuery, _, DBPrio_Normal);
}
public void SQL_FinishedQuery(Database db, DBResultSet results, const char[] error, any data)
{
if (!db || strlen(error))
{
LogError("Query error 3: %s", error);
}
delete results;
}
public void OnClientDisconnect(int client)
{
g_bInsertedPlayerSession[client] = false;
g_iClientHours[client] = 0;
g_iClientMinutes[client] = 0;
}
public void OnMapVoteEnd(const char[] map)
{
if (!g_bCorrectServer)
{
return;
}
char sMapname[64];
GetCurrentMap(sMapname, sizeof(sMapname));
char sQuery[512]; //picking max() is usually considered haram
Format(sQuery, sizeof(sQuery), "INSERT INTO `playtime_display_map_vote_events` (`map_id`, `current_map`, `result`) VALUES((select max(map_id) from `playtime_display_map_history`), '%s','%s')", sMapname, map);
g_hDatabase.Query(SQL_FinishedQueryMapVoteEndCallback, sQuery, _, DBPrio_Normal);
}
public void SQL_FinishedQueryMapVoteEndCallback(Database db, DBResultSet results, const char[] error, any data)
{
if (!db || strlen(error))
{
LogError("Query error 3: %s", error);
}
delete results;
for (int j = 0; j < GetArraySize(g_hPendingVoteChoices); j++)
{
VoteChoiceRecord entry;
GetArrayArray(g_hPendingVoteChoices, j, entry);
char sQuery[512]; //again, selecting with max() is generally not very good but should be reliable given the context here.
Format(sQuery, sizeof(sQuery), "INSERT INTO `playtime_display_map_vote_choices` (`vote_id`, `steamid`, `vote_choice`, `vote_weight`) VALUES ((select max(vote_id) from `playtime_display_map_vote_events`), '%s', '%s','%i')", entry.SteamId, entry.VoteChoice, entry.VoteWeight);
g_hDatabase.Query(SQL_FinishedQuery, sQuery, _, DBPrio_Normal);
}
g_hPendingVoteChoices.Clear();
}
public void OnMapVoteClientResult(char[] steamID, char[] vote_choice, int voteweight)
{
VoteChoiceRecord entry;
strcopy(entry.SteamId, sizeof(entry.SteamId), steamID);
strcopy(entry.VoteChoice, sizeof(entry.VoteChoice), vote_choice);
entry.VoteWeight = voteweight;
g_hPendingVoteChoices.PushArray(entry);
}
stock bool IsValidClient(int client)
{
if (client > 0 && client <= MaxClients && IsClientConnected(client) && IsClientInGame(client))
return true;
return false;
}
@@ -0,0 +1,83 @@
CREATE TABLE `playtime_display_map_history` (
`map_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`map_name` varchar(64) NOT NULL,
`map_start_dt` datetime NOT NULL DEFAULT current_timestamp(),
`map_end_dt` datetime DEFAULT NULL,
PRIMARY KEY (`map_id`),
KEY `idx_map_name` (`map_name`,`map_start_dt`),
KEY `idx_start` (`map_start_dt`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `playtime_display_map_vote_events` (
`vote_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`map_id` bigint(20) unsigned DEFAULT NULL,
`vote_time` datetime NOT NULL DEFAULT current_timestamp(),
`current_map` varchar(64) DEFAULT NULL,
`result` varchar(64) DEFAULT NULL,
PRIMARY KEY (`vote_id`),
KEY `idx_map_id` (`map_id`),
CONSTRAINT `fk_voteevent_map` FOREIGN KEY (`map_id`) REFERENCES `playtime_display_map_history` (`map_id`) ON DELETE SET NULL
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `playtime_display_players` (
`steamid` varchar(64) NOT NULL,
`current_name` varchar(128) NOT NULL,
`current_country` char(2) DEFAULT NULL,
`first_seen` datetime NOT NULL DEFAULT current_timestamp(),
`last_seen` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`steamid`),
KEY `idx_country` (`current_country`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `playtime_display_name_history` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`steamid` varchar(64) NOT NULL,
`player_name` varchar(128) NOT NULL,
`first_seen` datetime NOT NULL DEFAULT current_timestamp(),
`last_seen` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `uq_steamid_name` (`steamid`,`player_name`),
KEY `idx_player_name` (`player_name`),
CONSTRAINT `fk_namehist_steamid` FOREIGN KEY (`steamid`) REFERENCES `playtime_display_players` (`steamid`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=197 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `playtime_display_map_vote_choices` (
`vote_id` int(10) unsigned NOT NULL,
`steamid` varchar(64) NOT NULL,
`vote_choice` varchar(64) NOT NULL,
`vote_weight` int(11) NOT NULL DEFAULT 1,
PRIMARY KEY (`vote_id`,`steamid`),
KEY `idx_vote_choice` (`vote_choice`),
KEY `fk_votechoice_steamid` (`steamid`),
CONSTRAINT `fk_votechoice_event` FOREIGN KEY (`vote_id`) REFERENCES `playtime_display_map_vote_events` (`vote_id`) ON DELETE CASCADE,
CONSTRAINT `fk_votechoice_steamid` FOREIGN KEY (`steamid`) REFERENCES `playtime_display_players` (`steamid`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- unloze_playtimestats.playtime_display_sessions definition
CREATE TABLE `playtime_display_sessions` (
`session_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`steamid` varchar(64) NOT NULL,
`country_code` char(2) DEFAULT NULL,
`player_name` varchar(128) NOT NULL,
`session_start_playtime_minutes` bigint(20) NOT NULL,
`session_end_playtime_minutes` bigint(20) DEFAULT NULL,
`session_start_dt` datetime NOT NULL DEFAULT current_timestamp(),
`session_end_dt` datetime DEFAULT NULL,
`session_active_minutes` bigint(20) GENERATED ALWAYS AS (`session_end_playtime_minutes` - `session_start_playtime_minutes`) STORED,
`session_start_dow` tinyint(4) GENERATED ALWAYS AS (dayofweek(`session_start_dt`)) STORED,
`session_start_tod` time GENERATED ALWAYS AS (cast(`session_start_dt` as time)) STORED,
PRIMARY KEY (`session_id`),
KEY `idx_steamid_start` (`steamid`,`session_start_dt`),
KEY `idx_interval` (`session_start_dt`,`session_end_dt`),
KEY `idx_dow_tod` (`session_start_dow`,`session_start_tod`),
KEY `idx_country` (`country_code`),
CONSTRAINT `fk_session_steamid` FOREIGN KEY (`steamid`) REFERENCES `playtime_display_players` (`steamid`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=215 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;