diff --git a/misc/import_stats_pgsql.sql b/misc/import_stats_pgsql.sql new file mode 100644 index 000000000..a9df73036 --- /dev/null +++ b/misc/import_stats_pgsql.sql @@ -0,0 +1,147 @@ +DROP TABLE IF EXISTS get5_stats_players; +DROP TABLE IF EXISTS get5_stats_maps; +DROP TABLE IF EXISTS get5_stats_matches; + +CREATE TABLE IF NOT EXISTS get5_stats_matches +( + matchid BIGINT UNIQUE NOT NULL, + start_time TIMESTAMP NOT NULL, + end_time TIMESTAMP, + winner VARCHAR(16) NOT NULL DEFAULT '', + series_type VARCHAR(64) NOT NULL DEFAULT '', + team1_name VARCHAR(64) NOT NULL DEFAULT '', + team1_score INTEGER NOT NULL DEFAULT 0, + team2_name VARCHAR(64) NOT NULL DEFAULT '', + team2_score INTEGER NOT NULL DEFAULT 0, + server_id BIGINT NOT NULL DEFAULT 0, + CONSTRAINT get5_stats_matches_pk PRIMARY KEY (matchid) +); + +CREATE SEQUENCE get5_stats_matches_matchid_seq; + +ALTER TABLE get5_stats_matches + ALTER COLUMN matchid SET DEFAULT nextval('get5_stats_matches_matchid_seq'); + +ALTER SEQUENCE get5_stats_matches_matchid_seq OWNED BY get5_stats_matches.matchid; + +CREATE TABLE IF NOT EXISTS get5_stats_maps +( + matchid BIGINT NOT NULL, + mapnumber INTEGER NOT NULL, + start_time TIMESTAMP NOT NULL, + end_time TIMESTAMP, + winner VARCHAR(16) NOT NULL DEFAULT '', + mapname VARCHAR(64) NOT NULL DEFAULT '', + team1_score INTEGER NOT NULL DEFAULT 0, + team2_score INTEGER NOT NULL DEFAULT 0, + CONSTRAINT get5_stats_maps_pk PRIMARY KEY (matchid, mapnumber), + CONSTRAINT get5_stats_maps_matchid FOREIGN KEY (matchid) REFERENCES get5_stats_matches (matchid) +); + +CREATE TABLE IF NOT EXISTS get5_stats_players +( + matchid BIGINT NOT NULL, + mapnumber INTEGER NOT NULL, + steamid64 VARCHAR(64) NOT NULL DEFAULT '', + team VARCHAR(16) NOT NULL DEFAULT '', + rounds_played INTEGER NOT NULL DEFAULT 0, + name VARCHAR(64) NOT NULL DEFAULT '', + kills INTEGER NOT NULL DEFAULT 0, + deaths INTEGER NOT NULL DEFAULT 0, + assists INTEGER NOT NULL DEFAULT 0, + flashbang_assists INTEGER NOT NULL DEFAULT 0, + teamkills INTEGER NOT NULL DEFAULT 0, + headshot_kills INTEGER NOT NULL DEFAULT 0, + damage INTEGER NOT NULL DEFAULT 0, + bomb_plants INTEGER NOT NULL DEFAULT 0, + bomb_defuses INTEGER NOT NULL DEFAULT 0, + v1 INTEGER NOT NULL DEFAULT 0, + v2 INTEGER NOT NULL DEFAULT 0, + v3 INTEGER NOT NULL DEFAULT 0, + v4 INTEGER NOT NULL DEFAULT 0, + v5 INTEGER NOT NULL DEFAULT 0, + k2 INTEGER NOT NULL DEFAULT 0, + k3 INTEGER NOT NULL DEFAULT 0, + k4 INTEGER NOT NULL DEFAULT 0, + k5 INTEGER NOT NULL DEFAULT 0, + firstkill_t INTEGER NOT NULL DEFAULT 0, + firstkill_ct INTEGER NOT NULL DEFAULT 0, + firstdeath_t INTEGER NOT NULL DEFAULT 0, + firstdeath_ct INTEGER NOT NULL DEFAULT 0, + tradekill INTEGER NOT NULL DEFAULT 0, + kast INTEGER NOT NULL DEFAULT 0, + contribution_score INTEGER NOT NULL DEFAULT 0, + mvp INTEGER NOT NULL DEFAULT 0, + CONSTRAINT get5_stats_players_pk PRIMARY KEY (matchid, mapnumber, steamid64), + CONSTRAINT get5_stats_players_matchid FOREIGN KEY (matchid) REFERENCES get5_stats_matches, + CONSTRAINT get5_stats_players_mapnumber FOREIGN KEY (matchid, mapnumber) REFERENCES get5_stats_maps (matchid, mapnumber) +); + +CREATE FUNCTION save_stats_player(matchId BIGINT, mapnumber INTEGER, steamid64 VARCHAR, team VARCHAR, + rounds_played INTEGER, name VARCHAR, kills INTEGER, deaths INTEGER, assists INTEGER, + flashbang_assists INTEGER, teamkills INTEGER, headshot_kills INTEGER, damage INTEGER, + bomb_plants INTEGER, bomb_defuses INTEGER, v1 INTEGER, v2 INTEGER, v3 INTEGER, + v4 INTEGER, v5 INTEGER, k2 INTEGER, k3 INTEGER, k4 INTEGER, k5 INTEGER, + firstkill_t INTEGER, firstkill_ct INTEGER, firstdeath_t INTEGER, + firstdeath_ct INTEGER, tradekill INTEGER, kast integer, contribution_score INTEGER, + mvp INTEGER) RETURNS VOID AS +$$ +BEGIN + LOOP + UPDATE get5_stats_players statsPlayers + SET rounds_played = save_stats_player.rounds_played, + kills = save_stats_player.kills, + deaths = save_stats_player.deaths, + assists = save_stats_player.assists, + flashbang_assists = save_stats_player.flashbang_assists, + teamkills = save_stats_player.teamkills, + headshot_kills = save_stats_player.headshot_kills, + damage = save_stats_player.damage, + bomb_plants = save_stats_player.bomb_plants, + bomb_defuses = save_stats_player.bomb_defuses, + v1 = save_stats_player.v1, + v2 = save_stats_player.v2, + v3 = save_stats_player.v3, + v4 = save_stats_player.v4, + v5 = save_stats_player.v5, + k2 = save_stats_player.k2, + k3 = save_stats_player.k3, + k4 = save_stats_player.k4, + k5 = save_stats_player.k5, + firstkill_t = save_stats_player.firstkill_t, + firstkill_ct = save_stats_player.firstkill_ct, + firstdeath_t = save_stats_player.firstdeath_t, + firstdeath_ct = save_stats_player.firstdeath_ct, + tradekill = save_stats_player.tradekill, + kast = save_stats_player.kast, + contribution_score = save_stats_player.contribution_score, + mvp = save_stats_player.mvp + WHERE statsPlayers.matchid = save_stats_player.matchid + AND statsPlayers.mapnumber = save_stats_player.mapnumber + AND statsPlayers.steamid64 = save_stats_player.steamid64; + IF found THEN + RETURN; + END IF; + BEGIN + INSERT INTO get5_stats_players(matchid, mapnumber, steamid64, team, rounds_played, name, kills, deaths, + assists, flashbang_assists, teamkills, headshot_kills, damage, bomb_plants, + bomb_defuses, v1, v2, v3, v4, v5, k2, k3, k4, k5, firstkill_t, firstkill_ct, + firstdeath_t, firstdeath_ct, tradekill, kast, contribution_score, mvp) + VALUES (save_stats_player.matchid, save_stats_player.mapnumber, save_stats_player.steamid64, + save_stats_player.team, save_stats_player.rounds_played, save_stats_player.name, + save_stats_player.kills, save_stats_player.deaths, save_stats_player.assists, + save_stats_player.flashbang_assists, save_stats_player.teamkills, save_stats_player.headshot_kills, + save_stats_player.damage, save_stats_player.bomb_plants, save_stats_player.bomb_defuses, + save_stats_player.v1, save_stats_player.v2, save_stats_player.v3, save_stats_player.v4, + save_stats_player.v5, save_stats_player.k2, save_stats_player.k3, save_stats_player.k4, + save_stats_player.k5, save_stats_player.firstkill_t, save_stats_player.firstkill_ct, + save_stats_player.firstdeath_t, save_stats_player.firstdeath_ct, save_stats_player.tradekill, + save_stats_player.kast, save_stats_player.contribution_score, save_stats_player.mvp); + RETURN; + EXCEPTION + WHEN unique_violation THEN + END; + END LOOP; +END; +$$ + LANGUAGE plpgsql; diff --git a/scripting/get5_pgsqlstats.sp b/scripting/get5_pgsqlstats.sp new file mode 100644 index 000000000..913fed836 --- /dev/null +++ b/scripting/get5_pgsqlstats.sp @@ -0,0 +1,345 @@ +/** + * ============================================================================= + * Get5 PostgreSQL stats + * Copyright (C) 2016. Sean Lewis. All rights reserved. + * ============================================================================= + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "get5/version.sp" +#include "include/get5.inc" +#include "include/logdebug.inc" + +#include "get5/util.sp" + +#pragma semicolon 1 +#pragma newdecls required + +Database db = null; +char queryBuffer[2048]; + +int g_MatchID = -1; + +ConVar g_ForceMatchIDCvar; +bool g_DisableStats = false; + +// clang-format off +public Plugin myinfo = { + name = "Get5 PostgreSQL stats", + author = "splewis", + description = "Records match stats collected by get5 to PostgreSQL", + version = PLUGIN_VERSION, + url = "https://github.com/splewis/get5" +}; +// clang-format on + +public void OnPluginStart() { + InitDebugLog("get5_debug", "get5_pgsql"); + + g_ForceMatchIDCvar = CreateConVar( + "get5_pgsql_force_matchid", "0", + "If set to a positive integer, this will force get5 to use the matchid in this convar"); + + char error[255]; + db = SQL_Connect("get5", true, error, sizeof(error)); + if (db == null) { + SetFailState("Could not connect to get5 database: %s", error); + } else { + g_DisableStats = false; + db.SetCharset("utf8mb4"); + } +} + +public void Get5_OnBackupRestore() { + char matchid[64]; + Get5_GetMatchID(matchid, sizeof(matchid)); + g_MatchID = StringToInt(matchid); +} + +public void Get5_OnSeriesInit() { + g_MatchID = -1; + + char seriesType[64]; + char team1Name[64]; + char team2Name[64]; + + int serverId = Get5_GetServerID(); + + char seriesTypeSz[sizeof(seriesType) * 2 + 1]; + char team1NameSz[sizeof(team1Name) * 2 + 1]; + char team2NameSz[sizeof(team2Name) * 2 + 1]; + + KeyValues tmpStats = new KeyValues("Stats"); + + Get5_GetMatchStats(tmpStats); + tmpStats.GetString(STAT_SERIESTYPE, seriesType, sizeof(seriesType)); + db.Escape(seriesType, seriesTypeSz, sizeof(seriesTypeSz)); + + tmpStats.GetString(STAT_SERIES_TEAM1NAME, team1Name, sizeof(team1Name)); + db.Escape(team1Name, team1NameSz, sizeof(team1NameSz)); + + tmpStats.GetString(STAT_SERIES_TEAM2NAME, team2Name, sizeof(team2Name)); + db.Escape(team2Name, team2NameSz, sizeof(team2NameSz)); + + delete tmpStats; + + g_DisableStats = false; + LogDebug("Setting up series stats, get5_pgsql_force_matchid = %d", g_ForceMatchIDCvar.IntValue); + + if (g_ForceMatchIDCvar.IntValue > 0) { + SetMatchID(g_ForceMatchIDCvar.IntValue); + g_ForceMatchIDCvar.IntValue = 0; + Format(queryBuffer, sizeof(queryBuffer), "INSERT INTO get5_stats_matches \ + (matchid, series_type, team1_name, team2_name, start_time, server_id) VALUES \ + (%d, '%s', '%s', '%s', NOW(), %d)", + g_MatchID, seriesTypeSz, team1NameSz, team2NameSz, serverId); + LogDebug(queryBuffer); + db.Query(SQLErrorCheckCallback, queryBuffer); + + LogMessage("Starting match id %d", g_MatchID); + + } else { + Transaction transaction = SQL_CreateTransaction(); + Format(queryBuffer, sizeof(queryBuffer), "INSERT INTO get5_stats_matches \ + (series_type, team1_name, team2_name, start_time, server_id) VALUES \ + ('%s', '%s', '%s', NOW(), %d)", + seriesTypeSz, team1NameSz, team2NameSz, serverId); + LogDebug(queryBuffer); + transaction.AddQuery(queryBuffer); + Format(queryBuffer, sizeof(queryBuffer), "SELECT currval(pg_get_serial_sequence('get5_stats_matches','matchid'))"); + LogDebug(queryBuffer); + transaction.AddQuery(queryBuffer); + db.Execute(transaction, MatchInitSuccess, MatchInitFailure); + } +} + +public void MatchInitCallback(Database dbObj, DBResultSet results, const char[] error, any data) { + if (results == null) { + LogError("Failed to get matchid from match init query"); + g_DisableStats = true; + } else { + if (results.InsertId > 0) { + SetMatchID(results.InsertId); + } + LogMessage("Starting match id %d", g_MatchID); + } +} + +static void SetMatchID(int matchid) { + g_MatchID = matchid; + char idStr[32]; + IntToString(g_MatchID, idStr, sizeof(idStr)); + Get5_SetMatchID(idStr); +} + +public void Get5_OnGoingLive(int mapNumber) { + if (g_DisableStats) + return; + + char mapName[255]; + GetCurrentMap(mapName, sizeof(mapName)); + + char mapNameSz[sizeof(mapName) * 2 + 1]; + db.Escape(mapName, mapNameSz, sizeof(mapNameSz)); + + Format(queryBuffer, sizeof(queryBuffer), "INSERT INTO get5_stats_maps \ + (matchid, mapnumber, mapname, start_time) VALUES \ + (%d, %d, '%s', NOW())", + g_MatchID, mapNumber, mapNameSz); + LogDebug(queryBuffer); + + db.Query(SQLErrorCheckCallback, queryBuffer); +} + +public void UpdateRoundStats(int mapNumber) { + // Update team scores + int t1score = CS_GetTeamScore(Get5_MatchTeamToCSTeam(MatchTeam_Team1)); + int t2score = CS_GetTeamScore(Get5_MatchTeamToCSTeam(MatchTeam_Team2)); + + Format(queryBuffer, sizeof(queryBuffer), "UPDATE get5_stats_maps \ + SET team1_score = %d, team2_score = %d WHERE matchid = %d and mapnumber = %d", + t1score, t2score, g_MatchID, mapNumber); + LogDebug(queryBuffer); + db.Query(SQLErrorCheckCallback, queryBuffer); + + // Update player stats + KeyValues kv = new KeyValues("Stats"); + Get5_GetMatchStats(kv); + char mapKey[32]; + Format(mapKey, sizeof(mapKey), "map%d", mapNumber); + if (kv.JumpToKey(mapKey)) { + if (kv.JumpToKey("team1")) { + AddPlayerStats(kv, MatchTeam_Team1); + kv.GoBack(); + } + if (kv.JumpToKey("team2")) { + AddPlayerStats(kv, MatchTeam_Team2); + kv.GoBack(); + } + kv.GoBack(); + } + delete kv; +} + +public void Get5_OnMapResult(const char[] map, MatchTeam mapWinner, int team1Score, int team2Score, + int mapNumber) { + if (g_DisableStats) + return; + + // Update the map winner + char winnerString[64]; + GetTeamString(mapWinner, winnerString, sizeof(winnerString)); + Format(queryBuffer, sizeof(queryBuffer), + "UPDATE get5_stats_maps SET winner = '%s', end_time = NOW() \ + WHERE matchid = %d and mapnumber = %d", + winnerString, g_MatchID, mapNumber); + LogDebug(queryBuffer); + db.Query(SQLErrorCheckCallback, queryBuffer); + + // Update the series scores + int t1_seriesscore, t2_seriesscore, tmp; + Get5_GetTeamScores(MatchTeam_Team1, t1_seriesscore, tmp); + Get5_GetTeamScores(MatchTeam_Team2, t2_seriesscore, tmp); + + Format(queryBuffer, sizeof(queryBuffer), "UPDATE get5_stats_matches \ + SET team1_score = %d, team2_score = %d WHERE matchid = %d", + t1_seriesscore, t2_seriesscore, g_MatchID); + LogDebug(queryBuffer); + db.Query(SQLErrorCheckCallback, queryBuffer); +} + +public void AddPlayerStats(KeyValues kv, MatchTeam team) { + char name[MAX_NAME_LENGTH]; + char auth[AUTH_LENGTH]; + char nameSz[MAX_NAME_LENGTH * 2 + 1]; + char authSz[AUTH_LENGTH * 2 + 1]; + int mapNumber = MapNumber(); + + if (kv.GotoFirstSubKey()) { + do { + kv.GetSectionName(auth, sizeof(auth)); + kv.GetString("name", name, sizeof(name)); + db.Escape(auth, authSz, sizeof(authSz)); + db.Escape(name, nameSz, sizeof(nameSz)); + + int kills = kv.GetNum(STAT_KILLS); + int deaths = kv.GetNum(STAT_DEATHS); + int flashbang_assists = kv.GetNum(STAT_FLASHBANG_ASSISTS); + int assists = kv.GetNum(STAT_ASSISTS); + int teamkills = kv.GetNum(STAT_TEAMKILLS); + int damage = kv.GetNum(STAT_DAMAGE); + int headshot_kills = kv.GetNum(STAT_HEADSHOT_KILLS); + int roundsplayed = kv.GetNum(STAT_ROUNDSPLAYED); + int plants = kv.GetNum(STAT_BOMBPLANTS); + int defuses = kv.GetNum(STAT_BOMBDEFUSES); + int v1 = kv.GetNum(STAT_V1); + int v2 = kv.GetNum(STAT_V2); + int v3 = kv.GetNum(STAT_V3); + int v4 = kv.GetNum(STAT_V4); + int v5 = kv.GetNum(STAT_V5); + int k2 = kv.GetNum(STAT_2K); + int k3 = kv.GetNum(STAT_3K); + int k4 = kv.GetNum(STAT_4K); + int k5 = kv.GetNum(STAT_5K); + int firstkill_t = kv.GetNum(STAT_FIRSTKILL_T); + int firstkill_ct = kv.GetNum(STAT_FIRSTKILL_CT); + int firstdeath_t = kv.GetNum(STAT_FIRSTDEATH_T); + int firstdeath_ct = kv.GetNum(STAT_FIRSTDEATH_CT); + int tradekill = kv.GetNum(STAT_TRADEKILL); + int kast = kv.GetNum(STAT_KAST); + int contribution_score = kv.GetNum(STAT_CONTRIBUTION_SCORE); + int mvp = kv.GetNum(STAT_MVP); + + char teamString[16]; + GetTeamString(team, teamString, sizeof(teamString)); + + // TODO: this should really get split up somehow. Once it hits 32-arguments + // (aka just a few more) it will cause runtime errors and the Format will fail. + // clang-format off + Format(queryBuffer, sizeof(queryBuffer), + "SELECT save_stats_player(%d, %d, '%s', '%s', %d, '%s', %d, %d, %d, %d, %d, %d, %d, \ + %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d)", + g_MatchID, mapNumber, authSz, teamString, + roundsplayed, nameSz, kills, deaths, assists, + flashbang_assists, teamkills, headshot_kills, damage, + plants, defuses, v1, v2, v3, v4, v5, k2, k3, k4, k5, + firstkill_t, firstkill_ct, firstdeath_t, firstdeath_ct, + tradekill, kast, contribution_score, mvp); + // clang-format on + + LogDebug(queryBuffer); + db.Query(SQLErrorCheckCallback, queryBuffer); + + } while (kv.GotoNextKey()); + kv.GoBack(); + } +} + +public void MatchInitSuccess(Database database, any data, int numQueries, Handle[] results, + any[] queryData) { + Handle matchidResult = results[1]; + if (SQL_FetchRow(matchidResult)) { + SetMatchID(SQL_FetchInt(matchidResult, 0)); + + LogMessage("Starting match id %d", g_MatchID); + } else { + LogError("Failed to get matchid from match init query"); + g_DisableStats = true; + } +} + +public void MatchInitFailure(Database database, any data, int numQueries, const char[] error, + int failIndex, any[] queryData) { + LogError("Failed match creation query, error = %s", error); + g_DisableStats = true; +} + +public void Get5_OnSeriesResult(MatchTeam seriesWinner, int team1MapScore, int team2MapScore) { + if (g_DisableStats) + return; + + char winnerString[64]; + GetTeamString(seriesWinner, winnerString, sizeof(winnerString)); + + Format(queryBuffer, sizeof(queryBuffer), "UPDATE get5_stats_matches \ + SET winner = '%s', team1_score = %d, team2_score = %d, end_time = NOW() \ + WHERE matchid = %d", + winnerString, team1MapScore, team2MapScore, g_MatchID); + LogDebug(queryBuffer); + db.Query(SQLErrorCheckCallback, queryBuffer); +} + +public int SQLErrorCheckCallback(Handle owner, Handle hndl, const char[] error, int data) { + if (!StrEqual("", error)) { + LogError("Last Connect SQL Error: %s", error); + } +} + +public void Get5_OnRoundStatsUpdated() { + if (Get5_GetGameState() == Get5State_Live && !g_DisableStats) { + UpdateRoundStats(MapNumber()); + } +} + +static int MapNumber() { + int t1, t2; + int buf; + Get5_GetTeamScores(MatchTeam_Team1, t1, buf); + Get5_GetTeamScores(MatchTeam_Team2, t2, buf); + return t1 + t2; +}