From e1a93bd0dd30c543b6a2f65542e85630cc779aa2 Mon Sep 17 00:00:00 2001 From: jenz Date: Thu, 20 Aug 2026 17:12:46 +0200 Subject: [PATCH] initial commit of plugin just filling the tables with data for the playtime display --- .../scripting/unloze_playtime_display.sp | 300 ++++++++++++++++++ .../sql/create_tables_playtime_service.sql | 83 +++++ 2 files changed, 383 insertions(+) create mode 100644 discord_verificiation/scripting/unloze_playtime_display.sp create mode 100644 discord_verificiation/sql/create_tables_playtime_service.sql diff --git a/discord_verificiation/scripting/unloze_playtime_display.sp b/discord_verificiation/scripting/unloze_playtime_display.sp new file mode 100644 index 0000000..65cca70 --- /dev/null +++ b/discord_verificiation/scripting/unloze_playtime_display.sp @@ -0,0 +1,300 @@ +#pragma semicolon 1 +#define PLUGIN_AUTHOR "jenz" +#define PLUGIN_VERSION "1.0" +#include +#include +#include +#include + +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; +} diff --git a/discord_verificiation/sql/create_tables_playtime_service.sql b/discord_verificiation/sql/create_tables_playtime_service.sql new file mode 100644 index 0000000..7cdcfb5 --- /dev/null +++ b/discord_verificiation/sql/create_tables_playtime_service.sql @@ -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;