diff --git a/sourcemod/plugins/hlstatsx.smx b/sourcemod/plugins/hlstatsx.smx index 68c693e..7e9a165 100644 Binary files a/sourcemod/plugins/hlstatsx.smx and b/sourcemod/plugins/hlstatsx.smx differ diff --git a/sourcemod/plugins/superlogs-aoc.smx b/sourcemod/plugins/superlogs-aoc.smx new file mode 100644 index 0000000..300d5bc Binary files /dev/null and b/sourcemod/plugins/superlogs-aoc.smx differ diff --git a/sourcemod/plugins/superlogs-css.smx b/sourcemod/plugins/superlogs-css.smx new file mode 100644 index 0000000..be027d7 Binary files /dev/null and b/sourcemod/plugins/superlogs-css.smx differ diff --git a/sourcemod/plugins/superlogs-ddd.smx b/sourcemod/plugins/superlogs-ddd.smx new file mode 100644 index 0000000..5d5d6d2 Binary files /dev/null and b/sourcemod/plugins/superlogs-ddd.smx differ diff --git a/sourcemod/plugins/superlogs-dods.smx b/sourcemod/plugins/superlogs-dods.smx new file mode 100644 index 0000000..73dfcc8 Binary files /dev/null and b/sourcemod/plugins/superlogs-dods.smx differ diff --git a/sourcemod/plugins/superlogs-generic.smx b/sourcemod/plugins/superlogs-generic.smx new file mode 100644 index 0000000..e4ac976 Binary files /dev/null and b/sourcemod/plugins/superlogs-generic.smx differ diff --git a/sourcemod/plugins/superlogs-ges.smx b/sourcemod/plugins/superlogs-ges.smx new file mode 100644 index 0000000..44050bb Binary files /dev/null and b/sourcemod/plugins/superlogs-ges.smx differ diff --git a/sourcemod/plugins/superlogs-hl2mp.smx b/sourcemod/plugins/superlogs-hl2mp.smx new file mode 100644 index 0000000..325e58d Binary files /dev/null and b/sourcemod/plugins/superlogs-hl2mp.smx differ diff --git a/sourcemod/plugins/superlogs-ins.smx b/sourcemod/plugins/superlogs-ins.smx new file mode 100644 index 0000000..ac2230f Binary files /dev/null and b/sourcemod/plugins/superlogs-ins.smx differ diff --git a/sourcemod/plugins/superlogs-l4d.smx b/sourcemod/plugins/superlogs-l4d.smx new file mode 100644 index 0000000..aa0a93e Binary files /dev/null and b/sourcemod/plugins/superlogs-l4d.smx differ diff --git a/sourcemod/plugins/superlogs-neotokyo.smx b/sourcemod/plugins/superlogs-neotokyo.smx new file mode 100644 index 0000000..cff1262 Binary files /dev/null and b/sourcemod/plugins/superlogs-neotokyo.smx differ diff --git a/sourcemod/plugins/superlogs-nucleardawn.smx b/sourcemod/plugins/superlogs-nucleardawn.smx new file mode 100644 index 0000000..2f5b7af Binary files /dev/null and b/sourcemod/plugins/superlogs-nucleardawn.smx differ diff --git a/sourcemod/plugins/superlogs-pvkii.smx b/sourcemod/plugins/superlogs-pvkii.smx new file mode 100644 index 0000000..8f4b23e Binary files /dev/null and b/sourcemod/plugins/superlogs-pvkii.smx differ diff --git a/sourcemod/plugins/superlogs-tf2.smx b/sourcemod/plugins/superlogs-tf2.smx new file mode 100644 index 0000000..5984a7e Binary files /dev/null and b/sourcemod/plugins/superlogs-tf2.smx differ diff --git a/sourcemod/plugins/superlogs-zps.smx b/sourcemod/plugins/superlogs-zps.smx new file mode 100644 index 0000000..f3080d6 Binary files /dev/null and b/sourcemod/plugins/superlogs-zps.smx differ diff --git a/sourcemod/scripting/hlstatsx.sp b/sourcemod/scripting/hlstatsx.sp index 8289506..b346d09 100644 --- a/sourcemod/scripting/hlstatsx.sp +++ b/sourcemod/scripting/hlstatsx.sp @@ -626,6 +626,7 @@ public Action:ProtectForwardingChange(args) } + public Action:ProtectForwardingDelallChange(args) { if (hlx_protect_address != INVALID_HANDLE) @@ -777,6 +778,7 @@ color_player(color_type, player_index, String: client_message[192]) } + color_all_players(String: message[192]) { new color_index = -1; @@ -1160,11 +1162,11 @@ public Action:hlx_sm_psay(args) } if (strcmp(message_prefix, "") == 0) { - Format(display_message, sizeof(display_message), "\x01%s", client_message); + Format(display_message, sizeof(display_message), "\x01\x0B\x01%s", client_message); } else { - Format(display_message, sizeof(display_message), "%c%s\x01 %s", ((gamemod == Game_ZPS || gamemod == Game_GES)?5:4), message_prefix, client_message); + Format(display_message, sizeof(display_message), "\x01\x0B%c%s\x01 %s", ((gamemod == Game_ZPS || gamemod == Game_GES)?5:4), message_prefix, client_message); } new bool: setupColorForRecipients = false; @@ -1188,23 +1190,28 @@ public Action:hlx_sm_psay(args) color_index = player_index; } new Handle:hBf; - hBf = StartMessageOne("SayText2", player_index); + hBf = StartMessageOne("SayText2", player_index, USERMSG_RELIABLE|USERMSG_BLOCKHOOKS); + if (hBf != INVALID_HANDLE) { - BfWriteByte(hBf, color_index); - BfWriteByte(hBf, 0); - - if (gamemod == Game_CSGO) + if(GetUserMessageType() == UM_Protobuf) { - // hackhackhack... - // CS:GO won't print any colors unless you not only start with standard color (1) - // like in other games, but also have a 'printable' character following it. We will just - // use an unused control code - BfWriteByte(hBf, 1); - BfWriteByte(hBf, 11); + PbSetInt(hBf, "ent_idx", 0); + PbSetBool(hBf, "chat", false); + PbSetString(hBf, "msg_name", display_message); + PbAddString(hBf, "params", ""); + PbAddString(hBf, "params", ""); + PbAddString(hBf, "params", ""); + PbAddString(hBf, "params", ""); + } + else + { + BfWriteByte(hBf, color_index); + BfWriteByte(hBf, 0); + + BfWriteString(hBf, display_message); } - BfWriteString(hBf, display_message); EndMessage(); } } @@ -1324,21 +1331,39 @@ public Action:hlx_sm_psay2(args) { // thanks to Fyren and IceMatrix for help with this new Handle:hBf; - hBf = StartMessageOne("SayText", player_index); + hBf = StartMessageOne("SayText", player_index, USERMSG_RELIABLE|USERMSG_BLOCKHOOKS); if (hBf != INVALID_HANDLE) { - BfWriteByte(hBf, 1); - BfWriteBool(hBf, true); - BfWriteByte(hBf, player_index); - - if (prefix == 0) + if(GetUserMessageType() == UM_Protobuf) { - BfWriteString(hBf, buffer_message); + PbSetInt(hBf, "ent_idx", player_index); + PbSetBool(hBf, "chat", true); + + if (prefix == 0) + { + PbSetString(hBf, "text", buffer_message); + } + else + { + PbSetString(hBf, "text", client_message); + } } else { - BfWriteString(hBf, client_message); + BfWriteByte(hBf, 1); + BfWriteBool(hBf, true); + BfWriteByte(hBf, player_index); + + if (prefix == 0) + { + BfWriteString(hBf, buffer_message); + } + else + { + BfWriteString(hBf, client_message); + } } + EndMessage(); } } @@ -1564,6 +1589,36 @@ public Action:hlx_sm_browse(args) { if (g_bPlyrCanDoMotd[player_index]) { + if (GetUserMessageType() == UM_Protobuf) + { + decl String:typeStr[5]; + IntToString(MOTDPANEL_TYPE_URL, typeStr, 4); + + new Handle:pb = StartMessageOne("VGUIMenu", player_index); + + PbSetString(pb, "name", "info"); + PbSetBool(pb, "show", true); + + new Handle:modkey = PbAddMessage(pb, "subkeys"); + + PbSetString(modkey, "name", "type"); + PbSetString(modkey, "str", typeStr); + + modkey = PbAddMessage(pb, "subkeys"); + PbSetString(modkey, "name", "title"); + PbSetString(modkey, "str", "HLstatsX:CE"); + + modkey = PbAddMessage(pb, "subkeys"); + PbSetString(modkey, "name", "msg"); + PbSetString(modkey, "str", client_url); + + EndMessage(); + } + else + { + ShowMOTDPanel(player_index, "HLstatsX:CE", client_url, MOTDPANEL_TYPE_URL); + } + ShowMOTDPanel(player_index, "HLstatsX:CE", client_url, MOTDPANEL_TYPE_URL); } else @@ -1662,6 +1717,7 @@ public Action:hlx_sm_redirect(args) } + public Action:hlx_sm_player_action(args) { if (args < 2) @@ -2134,12 +2190,21 @@ stock PrintToChatRecipientsFF(const String:message[]) if (client > 0 && !IsFakeClient(client) && IsClientInGame(client)) { new Handle:hBf; - hBf = StartMessageOne("SayText", client); + hBf = StartMessageOne("SayText", client, USERMSG_RELIABLE|USERMSG_BLOCKHOOKS); if (hBf != INVALID_HANDLE) { - BfWriteByte(hBf, 0); // send as console - BfWriteString(hBf, message); - BfWriteByte(hBf, 1); // 1 to enable color parsing, 0 to not + if(GetUserMessageType() == UM_Protobuf) + { + PbSetInt(hBf, "ent_idx", 0); + PbSetBool(hBf, "chat", true); + PbSetString(hBf, "text", message); + } + else + { + BfWriteByte(hBf, 0); // send as console + BfWriteString(hBf, message); + BfWriteByte(hBf, 1); // 1 to enable color parsing, 0 to not + } EndMessage(); } } diff --git a/sourcemod/scripting/include/wstatshelper.inc b/sourcemod/scripting/include/wstatshelper.inc new file mode 100644 index 0000000..2648d83 --- /dev/null +++ b/sourcemod/scripting/include/wstatshelper.inc @@ -0,0 +1,138 @@ +#define HITGROUP_GENERIC 0 +#define HITGROUP_HEAD 1 +#define HITGROUP_CHEST 2 +#define HITGROUP_STOMACH 3 +#define HITGROUP_LEFTARM 4 +#define HITGROUP_RIGHTARM 5 +#define HITGROUP_LEFTLEG 6 +#define HITGROUP_RIGHTLEG 7 + +#define LOG_HIT_OFFSET 7 + +#define LOG_HIT_SHOTS 0 +#define LOG_HIT_HITS 1 +#define LOG_HIT_KILLS 2 +#define LOG_HIT_HEADSHOTS 3 +#define LOG_HIT_TEAMKILLS 4 +#define LOG_HIT_DAMAGE 5 +#define LOG_HIT_DEATHS 6 +#define LOG_HIT_GENERIC 7 +#define LOG_HIT_HEAD 8 +#define LOG_HIT_CHEST 9 +#define LOG_HIT_STOMACH 10 +#define LOG_HIT_LEFTARM 11 +#define LOG_HIT_RIGHTARM 12 +#define LOG_HIT_LEFTLEG 13 +#define LOG_HIT_RIGHTLEG 14 + +new Handle:g_weapon_trie = INVALID_HANDLE; + +CreatePopulateWeaponTrie() +{ + // Create a Trie + g_weapon_trie = CreateTrie(); + + // Initial populate + for (new i = 0; i < MAX_LOG_WEAPONS; i++) + { + if (g_weapon_list[i][0] == 0) + { + // some games have a couple blanks as place holders (so array indexes match with weapon ids) + decl String:randomKey[6]; + Format(randomKey, sizeof(randomKey), "%c%c%c%c%c%c", GetURandomInt(), GetURandomInt(), GetURandomInt(), GetURandomInt(), GetURandomInt(), GetURandomInt()); + SetTrieValue(g_weapon_trie, randomKey, i); + continue; + } + + SetTrieValue(g_weapon_trie, g_weapon_list[i], i); + } +} + +dump_player_stats(client) +{ + if (IsClientInGame(client) && IsClientConnected(client)) + { + decl String: player_authid[64]; + if (!GetClientAuthString(client, player_authid, sizeof(player_authid))) + { + strcopy(player_authid, sizeof(player_authid), "UNKNOWN"); + } + new player_team_index = GetClientTeam(client); + + new player_userid = GetClientUserId(client); + + new is_logged; + for (new i = 0; (i < MAX_LOG_WEAPONS); i++) + { + #if defined INS + if (g_weapon_stats[client][i][LOG_HIT_HITS] > 0) + { + LogToGame("\"%N<%d><%s><%s>\" triggered \"weaponstats\" (weapon \"weapon_%s\") (shots \"%d\") (hits \"%d\") (kills \"%d\") (headshots \"%d\") (tks \"%d\") (damage \"%d\") (deaths \"%d\")", client, player_userid, player_authid, g_team_list[player_team_index], g_weapon_list[i], g_weapon_stats[client][i][LOG_HIT_SHOTS], g_weapon_stats[client][i][LOG_HIT_HITS], g_weapon_stats[client][i][LOG_HIT_KILLS], g_weapon_stats[client][i][LOG_HIT_HEADSHOTS], g_weapon_stats[client][i][LOG_HIT_TEAMKILLS], g_weapon_stats[client][i][LOG_HIT_DAMAGE], g_weapon_stats[client][i][LOG_HIT_DEATHS]); + LogToGame("\"%N<%d><%s><%s>\" triggered \"weaponstats2\" (weapon \"weapon_%s\") (head \"%d\") (chest \"%d\") (stomach \"%d\") (leftarm \"%d\") (rightarm \"%d\") (leftleg \"%d\") (rightleg \"%d\")", client, player_userid, player_authid, g_team_list[player_team_index], g_weapon_list[i], g_weapon_stats[client][i][LOG_HIT_HEAD], g_weapon_stats[client][i][LOG_HIT_CHEST], g_weapon_stats[client][i][LOG_HIT_STOMACH], g_weapon_stats[client][i][LOG_HIT_LEFTARM], g_weapon_stats[client][i][LOG_HIT_RIGHTARM], g_weapon_stats[client][i][LOG_HIT_LEFTLEG], g_weapon_stats[client][i][LOG_HIT_RIGHTLEG]); + #else + if (g_weapon_stats[client][i][LOG_HIT_SHOTS] > 0) + { + #if defined GES + LogToGame("\"%N<%d><%s><%s>\" triggered \"weaponstats\" (weapon \"weapon_%s\") (shots \"%d\") (hits \"%d\") (kills \"%d\") (headshots \"%d\") (tks \"%d\") (damage \"%d\") (deaths \"%d\")", client, player_userid, player_authid, g_team_list[player_team_index], g_weapon_loglist[i], g_weapon_stats[client][i][LOG_HIT_SHOTS], g_weapon_stats[client][i][LOG_HIT_HITS], g_weapon_stats[client][i][LOG_HIT_KILLS], g_weapon_stats[client][i][LOG_HIT_HEADSHOTS], g_weapon_stats[client][i][LOG_HIT_TEAMKILLS], g_weapon_stats[client][i][LOG_HIT_DAMAGE], g_weapon_stats[client][i][LOG_HIT_DEATHS]); + LogToGame("\"%N<%d><%s><%s>\" triggered \"weaponstats2\" (weapon \"weapon_%s\") (head \"%d\") (chest \"%d\") (stomach \"%d\") (leftarm \"%d\") (rightarm \"%d\") (leftleg \"%d\") (rightleg \"%d\")", client, player_userid, player_authid, g_team_list[player_team_index], g_weapon_loglist[i], g_weapon_stats[client][i][LOG_HIT_HEAD], g_weapon_stats[client][i][LOG_HIT_CHEST], g_weapon_stats[client][i][LOG_HIT_STOMACH], g_weapon_stats[client][i][LOG_HIT_LEFTARM], g_weapon_stats[client][i][LOG_HIT_RIGHTARM], g_weapon_stats[client][i][LOG_HIT_LEFTLEG], g_weapon_stats[client][i][LOG_HIT_RIGHTLEG]); + #else + LogToGame("\"%N<%d><%s><%s>\" triggered \"weaponstats\" (weapon \"%s\") (shots \"%d\") (hits \"%d\") (kills \"%d\") (headshots \"%d\") (tks \"%d\") (damage \"%d\") (deaths \"%d\")", client, player_userid, player_authid, g_team_list[player_team_index], g_weapon_list[i], g_weapon_stats[client][i][LOG_HIT_SHOTS], g_weapon_stats[client][i][LOG_HIT_HITS], g_weapon_stats[client][i][LOG_HIT_KILLS], g_weapon_stats[client][i][LOG_HIT_HEADSHOTS], g_weapon_stats[client][i][LOG_HIT_TEAMKILLS], g_weapon_stats[client][i][LOG_HIT_DAMAGE], g_weapon_stats[client][i][LOG_HIT_DEATHS]); + LogToGame("\"%N<%d><%s><%s>\" triggered \"weaponstats2\" (weapon \"%s\") (head \"%d\") (chest \"%d\") (stomach \"%d\") (leftarm \"%d\") (rightarm \"%d\") (leftleg \"%d\") (rightleg \"%d\")", client, player_userid, player_authid, g_team_list[player_team_index], g_weapon_list[i], g_weapon_stats[client][i][LOG_HIT_HEAD], g_weapon_stats[client][i][LOG_HIT_CHEST], g_weapon_stats[client][i][LOG_HIT_STOMACH], g_weapon_stats[client][i][LOG_HIT_LEFTARM], g_weapon_stats[client][i][LOG_HIT_RIGHTARM], g_weapon_stats[client][i][LOG_HIT_LEFTLEG], g_weapon_stats[client][i][LOG_HIT_RIGHTLEG]); + #endif + #endif + is_logged++; + } + } + if (is_logged > 0) + { + reset_player_stats(client); + } + } +} + +reset_player_stats(client) +{ + for (new i = 0; (i < MAX_LOG_WEAPONS); i++) + { + g_weapon_stats[client][i][LOG_HIT_SHOTS] = 0; + g_weapon_stats[client][i][LOG_HIT_HITS] = 0; + g_weapon_stats[client][i][LOG_HIT_KILLS] = 0; + g_weapon_stats[client][i][LOG_HIT_HEADSHOTS] = 0; + g_weapon_stats[client][i][LOG_HIT_TEAMKILLS] = 0; + g_weapon_stats[client][i][LOG_HIT_DAMAGE] = 0; + g_weapon_stats[client][i][LOG_HIT_DEATHS] = 0; + g_weapon_stats[client][i][LOG_HIT_GENERIC] = 0; + g_weapon_stats[client][i][LOG_HIT_HEAD] = 0; + g_weapon_stats[client][i][LOG_HIT_CHEST] = 0; + g_weapon_stats[client][i][LOG_HIT_STOMACH] = 0; + g_weapon_stats[client][i][LOG_HIT_LEFTARM] = 0; + g_weapon_stats[client][i][LOG_HIT_RIGHTARM] = 0; + g_weapon_stats[client][i][LOG_HIT_LEFTLEG] = 0; + g_weapon_stats[client][i][LOG_HIT_RIGHTLEG] = 0; + } +} + +stock get_weapon_index(const String:weapon_name[]) +{ + new index = -1; + GetTrieValue(g_weapon_trie, weapon_name, index); + return index; +} + + +WstatsDumpAll() +{ + for (new i = 1; i <= MaxClients; i++) + { + dump_player_stats(i); + } +} + +OnPlayerDisconnect(client) +{ + if(client > 0 && IsClientInGame(client)) + { + dump_player_stats(client); + reset_player_stats(client); + } +} \ No newline at end of file diff --git a/sourcemod/scripting/superlogs-aoc.sp b/sourcemod/scripting/superlogs-aoc.sp new file mode 100644 index 0000000..655ee75 --- /dev/null +++ b/sourcemod/scripting/superlogs-aoc.sp @@ -0,0 +1,270 @@ +/** + * HLstatsX Community Edition - SourceMod plugin to generate advanced weapon logging + * http://www.hlxcommunity.com + * Copyright (C) 2009 Nicholas Hastings (psychonic) + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma semicolon 1 + +#include +#include + +#define NAME "SuperLogs: Age of Chivalry" +#define VERSION "1.0.2" + +new Handle:g_cvar_headshots = INVALID_HANDLE; +new Handle:g_cvar_locations = INVALID_HANDLE; +new Handle:g_cvar_classchanges = INVALID_HANDLE; +new Handle:g_cvar_actions = INVALID_HANDLE; + +new bool:g_logheadshots = true; +new bool:g_loglocations = true; +new bool:g_logclasschanges = true; +new bool:g_logactions = true; + +new bool:g_bLogClassNextSpawn[MAXPLAYERS+1]; + +#include + +public Plugin:myinfo = { + name = NAME, + author = "psychonic", + description = "Advanced logging for Age of Chivalry. Generates auxilary logging for use with log parsers such as HLstatsX and Psychostats", + version = VERSION, + url = "http://www.hlxcommunity.com" +}; + + +public OnPluginStart() +{ + g_cvar_locations = CreateConVar("superlogs_locations", "1", "Enable logging of kill coordinates (default on)", 0, true, 0.0, true, 1.0); + g_cvar_headshots = CreateConVar("superlogs_headshots", "1", "Enable logging of headshot and decapitation actions (default on)", 0, true, 0.0, true, 1.0); + g_cvar_classchanges = CreateConVar("superlogs_classchanges", "1", "Enable logging of character changes (default on)", 0, true, 0.0, true, 1.0); + g_cvar_actions = CreateConVar("superlogs_actions", "1", "Enable logging of actions, such as \"Round_Win\" (default on)", 0, true, 0.0, true, 1.0); + + HookConVarChange(g_cvar_locations, OnCvarLocationsChange); + HookConVarChange(g_cvar_headshots, OnCvarHeadshotsChange); + HookConVarChange(g_cvar_classchanges, OnCvarClasschangesChange); + + CreateConVar("superlogs_aoc_version", VERSION, NAME, FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY); + + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + HookEvent("round_end", Event_RoundEnd); + hook_classchanges(); + + CreateTimer(1.0, LogMap); + + GetTeams(); +} + +hook_classchanges() +{ + HookUserMessage(GetUserMessageId("ClassChanged"), Event_ClassChanged); + HookEvent("player_spawn", Event_Spawn, EventHookMode_Pre); + + for (new i = 1; i <= MAXPLAYERS; i++) + { + g_bLogClassNextSpawn[i] = false; + } +} + +unhook_classchanges() +{ + UnhookUserMessage(GetUserMessageId("ClassChanged"), Event_ClassChanged); + UnhookEvent("player_spawn", Event_Spawn, EventHookMode_Pre); +} + + +public OnMapStart() +{ + GetTeams(); +} + +public Action:Event_ClassChanged(UserMsg:msg_id, Handle:bf, const players[], playersNum, bool:reliable, bool:init) +{ + g_bLogClassNextSpawn[players[0]] = true; + return Plugin_Continue; +} + +public Action:Event_Spawn(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0 && g_bLogClassNextSpawn[client]) + { + switch (GetEntProp(client, Prop_Send, "m_iClass")) + { + case 0: + LogRoleChange(client, "Longbowman"); + case 1: + LogRoleChange(client, "Crossbowman"); + case 2: + LogRoleChange(client, "Javelineer"); + case 3: + LogRoleChange(client, "Man at Arms"); + case 4: + LogRoleChange(client, "Sergeant"); + case 5: + LogRoleChange(client, "Guardsman"); + case 6: + LogRoleChange(client, "Crusader"); + case 7: + LogRoleChange(client, "Knight"); + case 8: + LogRoleChange(client, "Heavy Knight"); + } + g_bLogClassNextSpawn[client] = false; + } + + return Plugin_Continue; +} + + +public Action:Event_PlayerDeathPre(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID who died + // "attacker" "short" // user ID who killed + // "weapon" "string" // weapon name killed used + // "printweapon" "string" // full print weapon name killed used + // "hitgroup" "long" // Part of body that was hit + // "damagetype" "long" // CDamageInfo.GetAOCDamageType() + // "weaponid" "short" // Weapon ID + // "team" "short" // victim's team + + new attacker = GetEventInt(event, "attacker"); + new victim = GetEventInt(event, "userid"); + + if (attacker > 0 && victim > 0) + { + if (g_loglocations) + { + LogKillLoc(GetClientOfUserId(attacker), GetClientOfUserId(victim)); + } + + if (g_logheadshots) + { + new bool:headshot = GetEventBool(event, "headshot"); + new bool:decapped = GetEventBool(event, "decapped"); + new bool:headexplodie = GetEventBool(event, "headexplodie"); + if (decapped) + { + //decapitation + LogPlayerEvent(GetClientOfUserId(attacker), "triggered", "headshot", true, " (hstype \"decap\")"); + } + else if (headshot) + { + //headshot + LogPlayerEvent(GetClientOfUserId(attacker), "triggered", "headshot", true, " (hstype \"headshot\")"); + } + else if (headexplodie) + { + // head "explodie" + LogPlayerEvent(GetClientOfUserId(attacker), "triggered", "headshot", true, " (hstype \"headexplodie\")"); + } + } + } + + return Plugin_Continue; +} + +public OnClientPutInGame(client) +{ + g_bLogClassNextSpawn[client] = false; +} + +public Event_RoundEnd(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogTeamEvent(GetEventInt(event, "winner"), "triggered", "Round_Win"); +} + +public OnCvarLocationsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_loglocations; + g_loglocations = GetConVarBool(g_cvar_locations); + + if (old_value != g_loglocations) + { + if (g_loglocations & !g_logheadshots) + { + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + else if (!g_logheadshots) + { + UnhookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + } +} + +public OnCvarActionsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logactions; + g_logactions = GetConVarBool(g_cvar_actions); + + if (old_value != g_logactions) + { + if (g_logactions) + { + HookEvent("round_end", Event_RoundEnd); + } + else + { + UnhookEvent("round_end", Event_RoundEnd); + } + } +} + +public OnCvarHeadshotsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logheadshots; + g_logheadshots = GetConVarBool(g_cvar_headshots); + + if (old_value != g_logheadshots) + { + if (g_logheadshots & !g_loglocations) + { + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + else if (!g_loglocations) + { + UnhookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + } +} + +public OnCvarClasschangesChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logclasschanges; + g_logclasschanges = GetConVarBool(g_cvar_classchanges); + + if (old_value != g_logclasschanges) + { + if (g_logclasschanges) + { + hook_classchanges(); + } + else + { + unhook_classchanges(); + } + } +} + +public Action:LogMap(Handle:timer) +{ + // Called 1 second after OnPluginStart since srcds does not log the first map loaded. Idea from Stormtrooper's "mapfix.sp" for psychostats + LogMapLoad(); +} \ No newline at end of file diff --git a/sourcemod/scripting/superlogs-cspromod.sp b/sourcemod/scripting/superlogs-cspromod.sp new file mode 100644 index 0000000..04b4306 --- /dev/null +++ b/sourcemod/scripting/superlogs-cspromod.sp @@ -0,0 +1,116 @@ +#include +#include + +#define VERSION "2.2" + +public Plugin:myinfo = +{ + name = "SuperLogs: CSpromod", + author = "NeoCortex, psychonic", + description = "Rewrites the logs from CSProMod so HLStatsX:CE will understand them", + version = VERSION, + url = "http://www.sourcemod.net/" +}; + +public OnPluginStart() +{ + CreateConVar("superlogs_cspromod_version", VERSION, "SuperLogs: CSpromod", FCVAR_NOTIFY); + + HookEvent("player_death", Event_PlayerDeath) + HookEvent("player_connect", Event_PlayerConnect) + HookEvent("player_disconnect", Event_PlayerDisconnect) + HookEvent("player_team", Event_PlayerTeam) + + HookEvent("round_start", Event_RoundStart) + HookEvent("round_end", Event_RoundEnd) + + CreateTimer(1.0, LogMap); +} + +public Action:LogMap(Handle:timer) +{ + // Taken from superlogs-generic.sp by psychonic + // Called 1 second after OnPluginStart since srcds does not log the first map loaded. Idea from Stormtrooper's "mapfix.sp" for psychostats + LogMapLoad(); +} + +public OnMapStart() +{ + // For loghelper + GetTeams(); +} + +public Event_PlayerConnect(Handle:event, const String:name[], bool:dontBroadcast) +{ + decl String:cname[MAX_NAME_LENGTH]; + GetEventString(event, "name", cname, sizeof(cname)); + decl String:steamid[24]; + GetEventString(event, "networkid", steamid, sizeof(steamid)); + decl String:ip[32]; + GetEventString(event, "address", ip, sizeof(ip)); + + LogToGame("\"%s<%d><%s><>\" connected, address \"%s\"", cname, GetEventInt(event, "userid"), steamid, ip); +} + +public Event_PlayerDisconnect(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + decl String:cname[MAX_NAME_LENGTH]; + GetClientName(client, cname, sizeof(cname)); + decl String:cauth[32]; + GetClientAuthString(client, cauth, 32); + decl String:creason[128]; + GetEventString(event, "reason", creason, sizeof(creason)); + + LogToGame("\"%s<%d><%s><>\" disconnected (reason \"%s\")", cname, client, cauth, creason); +} + +public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast) +{ + // Version 2 of this subroutine (using loghelper) is written by psychonic + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + decl String:AWeapon[64]; + GetEventString(event, "weapon", AWeapon, sizeof(AWeapon)); + + decl String:properties[12] = ""; + if (GetEventBool(event, "headshot")) + { + strcopy(properties, sizeof(properties), " (headshot)"); + } + + // Player_Suicide + if (attacker == victim) + { + LogSuicide(victim, AWeapon, true, properties); + return; + } + + LogKill(attacker, victim, AWeapon, true, properties); +} + +public Event_RoundStart(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogToGame("World triggered \"Round_Start\""); +} + +public Event_RoundEnd(Handle:event, const String:name[], bool:dontBroadcast) +{ + new winner = GetEventInt(event, "winner"); + if (winner == 2 || winner == 3) + { + LogTeamEvent(winner, "triggered", "Round_Win"); + } + + LogToGame("World triggered \"Round_End\""); +} + +public Event_PlayerTeam(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + new NTeam = GetEventInt(event, "team"); + decl String:STeam[32]; + GetTeamName(NTeam, STeam, sizeof(STeam)); + + LogPlayerEvent(client, "joined team", STeam, true) +} diff --git a/sourcemod/scripting/superlogs-css.sp b/sourcemod/scripting/superlogs-css.sp new file mode 100644 index 0000000..2754f2c --- /dev/null +++ b/sourcemod/scripting/superlogs-css.sp @@ -0,0 +1,471 @@ +/** + * HLstatsX Community Edition - SourceMod plugin to generate advanced weapon logging + * http://www.hlxcommunity.com + * Copyright (C) 2009 Nicholas Hastings (psychonic) + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma semicolon 1 + +#include +#include + +#define NAME "SuperLogs: CSS" +#define VERSION "1.2.4" + +#define MAX_LOG_WEAPONS 28 +#define IGNORE_SHOTS_START 25 +#define MAX_WEAPON_LEN 13 + + +new g_weapon_stats[MAXPLAYERS+1][MAX_LOG_WEAPONS][15]; +new const String:g_weapon_list[MAX_LOG_WEAPONS][MAX_WEAPON_LEN] = { + "ak47", + "m4a1", + "awp", + "deagle", + "mp5navy", + "aug", + "p90", + "famas", + "galil", + "scout", + "g3sg1", + "usp", + "glock", + "m249", + "m3", + "elite", + "fiveseven", + "mac10", + "p228", + "sg550", + "sg552", + "tmp", + "ump45", + "xm1014", + "knife", + "hegrenade", + "smokegrenade", + "flashbang" + }; + +new Handle:g_cvar_wstats = INVALID_HANDLE; +new Handle:g_cvar_headshots = INVALID_HANDLE; +new Handle:g_cvar_actions = INVALID_HANDLE; +new Handle:g_cvar_locations = INVALID_HANDLE; +new Handle:g_cvar_ktraj = INVALID_HANDLE; +new Handle:g_cvar_version = INVALID_HANDLE; + +new bool:g_logwstats = true; +new bool:g_logheadshots = true; +new bool:g_logactions = true; +new bool:g_loglocations = true; +new bool:g_logktraj = false; + +#include +#include + + +public Plugin:myinfo = { + name = NAME, + author = "psychonic", + description = "Advanced logging for CSS. Generates auxilary logging for use with log parsers such as HLstatsX and Psychostats", + version = VERSION, + url = "http://www.hlxcommunity.com" +}; + +#if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 +public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) +#else +public bool:AskPluginLoad(Handle:myself, bool:late, String:error[], err_max) +#endif +{ + decl String:game_description[64]; + GetGameDescription(game_description, sizeof(game_description), true); + if (StrContains(game_description, "Counter-Strike", false) == -1) + { + decl String:game_folder[64]; + GetGameFolderName(game_folder, sizeof(game_folder)); + if (StrContains(game_folder, "cstrike", false) == -1) + { + strcopy(error, err_max, "This plugin is only supported on CS:S"); + #if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 + return APLRes_Failure; + #else + return false; + #endif + } + } +#if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 + return APLRes_Success; +#else + return true; +#endif +} + + +public OnPluginStart() +{ + CreatePopulateWeaponTrie(); + + g_cvar_wstats = CreateConVar("superlogs_wstats", "1", "Enable logging of weapon stats (default on)", 0, true, 0.0, true, 1.0); + g_cvar_headshots = CreateConVar("superlogs_headshots", "1", "Enable logging of headshot player action (default on)", 0, true, 0.0, true, 1.0); + g_cvar_actions = CreateConVar("superlogs_actions", "1", "Enable logging of player actions (default on)", 0, true, 0.0, true, 1.0); + g_cvar_locations = CreateConVar("superlogs_locations", "1", "Enable logging of location on player death (default on)", 0, true, 0.0, true, 1.0); + g_cvar_ktraj = CreateConVar("superlogs_ktraj", "0", "Enable Psychostats \"KTRAJ\" logging (default off)", 0, true, 0.0, true, 1.0); + + // cvars will have already existed if plugin was reloaded and might be set to non-default values + g_logwstats = GetConVarBool(g_cvar_wstats); + g_logheadshots = GetConVarBool(g_cvar_headshots); + g_logactions = GetConVarBool(g_cvar_actions); + g_loglocations = GetConVarBool(g_cvar_locations); + g_logktraj = GetConVarBool(g_cvar_ktraj); + + HookConVarChange(g_cvar_wstats, OnCvarWstatsChange); + HookConVarChange(g_cvar_headshots, OnCvarHeadshotsChange); + HookConVarChange(g_cvar_actions, OnCvarActionsChange); + HookConVarChange(g_cvar_locations, OnCvarLocationsChange); + HookConVarChange(g_cvar_ktraj, OnCvarKtrajChange); + + g_cvar_version = CreateConVar("superlogs_css_version", VERSION, NAME, FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY); + + hook_wstats(); + hook_actions(); + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + HookEvent("player_death", Event_PlayerDeath); + + CreateTimer(1.0, LogMap); + + GetTeams(); +} + + +public OnMapStart() +{ + GetTeams(); +} + +public OnConfigsExecuted() +{ + decl String:version[255]; + GetConVarString(g_cvar_version, version, sizeof(version)); + SetConVarString(g_cvar_version, version); +} + +hook_wstats() +{ + HookEvent("weapon_fire", Event_PlayerShoot); + HookEvent("player_hurt", Event_PlayerHurt); + HookEvent("player_spawn", Event_PlayerSpawn); + HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy); + HookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre); +} + +unhook_wstats() +{ + UnhookEvent("weapon_fire", Event_PlayerShoot); + UnhookEvent("player_hurt", Event_PlayerHurt); + UnhookEvent("player_spawn", Event_PlayerSpawn); + UnhookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy); + UnhookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre); +} + +hook_actions() +{ + HookEvent("round_mvp", Event_RoundMVP); +} + +unhook_actions() +{ + UnhookEvent("round_mvp", Event_RoundMVP); +} + +public OnClientPutInServer(client) +{ + reset_player_stats(client); +} + + +public Event_PlayerShoot(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" + // "weapon" "string" // weapon name used + + new attacker = GetClientOfUserId(GetEventInt(event, "userid")); + if (attacker > 0) + { + decl String: weapon[MAX_WEAPON_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + new weapon_index = get_weapon_index(weapon); + if (weapon_index > -1 && weapon_index < IGNORE_SHOTS_START) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_SHOTS]++; + } + } +} + + +public Event_PlayerHurt(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // player index who was hurt + // "attacker" "short" // player index who attacked + // "health" "byte" // remaining health points + // "armor" "byte" // remaining armor points + // "weapon" "string" // weapon name attacker used, if not the world + // "dmg_health" "byte" // damage done to health + // "dmg_armor" "byte" // damage done to armor + // "hitgroup" "byte" // hitgroup that was damaged + + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + + if (attacker > 0) { + decl String: weapon[MAX_WEAPON_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + new weapon_index = get_weapon_index(weapon); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HITS]++; + g_weapon_stats[attacker][weapon_index][LOG_HIT_DAMAGE] += GetEventInt(event, "dmg_health"); + new hitgroup = GetEventInt(event, "hitgroup"); + if (hitgroup < 8) + { + g_weapon_stats[attacker][weapon_index][hitgroup + LOG_HIT_OFFSET]++; + } + } + } +} + + +public Action:Event_PlayerDeathPre(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID who died + // "attacker" "short" // user ID who killed + // "weapon" "string" // weapon name killer used + // "headshot" "bool" // signals a headshot + + new attacker = GetEventInt(event, "attacker"); + if (g_loglocations) + { + LogKillLoc(GetClientOfUserId(attacker), GetClientOfUserId(GetEventInt(event, "userid"))); + } + + if (g_logheadshots && GetEventBool(event, "headshot")) + { + LogPlayerEvent(GetClientOfUserId(attacker), "triggered", "headshot"); + } + + return Plugin_Continue; +} + +public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast) +{ + // this extents the original player_death by a new fields + // "userid" "short" // user ID who died + // "attacker" "short" // user ID who killed + // "weapon" "string" // weapon name killer used + // "headshot" "bool" // signals a headshot + // "dominated" "short" // did killer dominate victim with this kill + // "revenge" "short" // did killer get revenge on victim with this kill + + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + decl String: weapon[MAX_WEAPON_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + + if (attacker <= 0 || victim <= 0) + { + return; + } + + if (g_logwstats) + { + new weapon_index = get_weapon_index(weapon); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_KILLS]++; + if (GetEventBool(event, "headshot")) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HEADSHOTS]++; + } + g_weapon_stats[victim][weapon_index][LOG_HIT_DEATHS]++; + if (GetClientTeam(attacker) == GetClientTeam(victim)) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_TEAMKILLS]++; + } + dump_player_stats(victim); + } + } + if (g_logktraj) + { + LogPSKillTraj(attacker, victim, weapon); + } + if (g_logactions) + { + // these are only in Orangebox CS:S. These properties won't exist on ep1 css and should eval to 0/false. + if (GetEventInt(event, "dominated")) + { + LogPlyrPlyrEvent(attacker, victim, "triggered", "domination"); + } + else if (GetEventInt(event, "revenge")) + { + LogPlyrPlyrEvent(attacker, victim, "triggered", "revenge"); + } + } +} + +public Event_PlayerSpawn(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID on server + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + reset_player_stats(client); + } +} + +public Event_RoundEnd(Handle:event, const String:name[], bool:dontBroadcast) +{ + WstatsDumpAll(); +} + +public Event_RoundMVP(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogPlayerEvent(GetClientOfUserId(GetEventInt(event, "userid")), "triggered", "round_mvp"); +} + +public Action:Event_PlayerDisconnect(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + OnPlayerDisconnect(client); + return Plugin_Continue; +} + + +public Action:LogMap(Handle:timer) +{ + // Called 1 second after OnPluginStart since srcds does not log the first map loaded. Idea from Stormtrooper's "mapfix.sp" for psychostats + LogMapLoad(); +} + + +public OnCvarWstatsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logwstats; + g_logwstats = GetConVarBool(g_cvar_wstats); + + if (old_value != g_logwstats) + { + if (g_logwstats) + { + hook_wstats(); + if (!g_logktraj && !g_logactions) + { + HookEvent("player_death", Event_PlayerDeath); + } + } + else + { + unhook_wstats(); + if (!g_logktraj && !g_logactions) + { + UnhookEvent("player_death", Event_PlayerDeath); + } + } + } +} + +public OnCvarActionsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logactions; + g_logactions = GetConVarBool(g_cvar_actions); + + if (old_value != g_logactions) + { + if (g_logactions) + { + hook_actions(); + if (!g_logktraj && !g_logwstats) + { + HookEvent("player_death", Event_PlayerDeath); + } + } + else + { + unhook_actions(); + if (!g_logktraj && !g_logwstats) + { + UnhookEvent("player_death", Event_PlayerDeath); + } + } + } +} + +public OnCvarHeadshotsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logheadshots; + g_logheadshots = GetConVarBool(g_cvar_headshots); + + if (old_value != g_logheadshots) + { + if (g_logheadshots && !g_loglocations) + { + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + else if (!g_loglocations) + { + UnhookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + } +} + +public OnCvarLocationsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_loglocations; + g_loglocations = GetConVarBool(g_cvar_locations); + + if (old_value != g_loglocations) + { + if (g_loglocations && !g_logheadshots) + { + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + else if (!g_logheadshots) + { + UnhookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + } +} + +public OnCvarKtrajChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logktraj; + g_logktraj = GetConVarBool(g_cvar_ktraj); + + if (old_value != g_logktraj) + { + if (g_logktraj && !g_logwstats && !g_logactions) + { + HookEvent("player_death", Event_PlayerDeath); + } + else if (!g_logwstats && !g_logactions) + { + UnhookEvent("player_death", Event_PlayerDeath); + } + } +} \ No newline at end of file diff --git a/sourcemod/scripting/superlogs-ddd.sp b/sourcemod/scripting/superlogs-ddd.sp new file mode 100644 index 0000000..a18fc44 --- /dev/null +++ b/sourcemod/scripting/superlogs-ddd.sp @@ -0,0 +1,444 @@ +/** + * HLstatsX Community Edition - SourceMod plugin to generate advanced weapon logging + * http://www.hlxcommunity.com + * Copyright (C) 2009-2012 Nicholas Hastings (psychonic) + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma semicolon 1 + +#include +#include +#include + +#define NAME "SuperLogs: Dino D-Day" +#define VERSION "1.1.1" + +#define MAX_LOG_WEAPONS 47 +#define MAX_WEAPON_LEN 12 +#define PREFIX_LEN 7 + +// Some DDD Things +#define WEAPON_MG42 18 +#define WEAPON_TREX 37 +#define PLAYERCLASS_TREX 8 +#define PLAYERCLASS_DILOPHOSAURUS 5 +#define CUSTOMKILL_GOAT 5 +#define GOAT_BITE 19 +#define DDD_TEAM_ALLIES 2 +#define DDD_TEAM_AXIS 3 + +new g_weapon_stats[MAXPLAYERS+1][MAX_LOG_WEAPONS][15]; +new const String:g_weapon_list[MAX_LOG_WEAPONS][MAX_WEAPON_LEN] = { + "", + "mp40", + "thompson", + "shotgun", + "", + "pistol", + "", + "", + "garand", + "bar", + "luger", + "", + "piat", + "", + "mosin", + "k98", + "", + "", + "mg42", + "", + "k98sniper", + "", + "", + "", + "flechette", + "", + "", + "", + "", + "mp44", + "", + "", + "sten", + "p38", + "nagant", + "", + "", + "trex", + "", + "", + "trigger", + "stygimoloch", + "", + "", + "", + "carbine", + "greasegun" + }; + +new g_iNextHitgroup[MAXPLAYERS+1]; +new bool:g_DiloGoatBite[MAXPLAYERS+1]; + +new g_LastClass[MAXPLAYERS+1] = { -1, ... }; +new g_LastTeam[MAXPLAYERS+1] = { -1, ... }; + +new bool:g_bLate; +new bool:g_bIgnoreLog; + +PauseLogging() { g_bIgnoreLog = true; } +ResumeLogging() { g_bIgnoreLog = false; } + +#include +#include + + +public Plugin:myinfo = { + name = NAME, + author = "psychonic and FeuerSturm", + description = "Advanced logging for Dino D-Day. Generates auxilary logging for use with log parsers such as HLstatsX and Psychostats", + version = VERSION, + url = "http://www.hlxce.com" +}; + +public APLRes:AskPluginLoad2( Handle:myself, bool:late, String:error[], err_max ) +{ + decl String:szGameDir[64]; + GetGameFolderName( szGameDir, sizeof(szGameDir) ); + if ( !!strcmp( szGameDir, "dinodday" ) ) + { + strcopy( error, err_max, "This plugin is only supported on Dino D-Day" ); + return APLRes_Failure; + } + + g_bLate = late; + + return APLRes_Success; +} + +public OnPluginStart() +{ + CreatePopulateWeaponTrie(); + new Handle:hVersion = CreateConVar( "superlogs_dinodday_version", VERSION, NAME, FCVAR_NOTIFY|FCVAR_DONTRECORD ); + SetConVarString(hVersion, VERSION, false, true); + + HookEvent( "player_death", Event_PlayerDeathPre, EventHookMode_Pre ); + HookEvent( "player_death", Event_PlayerDeath, EventHookMode_Post ); + HookEvent( "player_spawn", Event_PlayerSpawn, EventHookMode_Post ); + HookEvent( "update_roundscore", Event_RoundEnd, EventHookMode_Post ); + HookEvent( "player_changeclass", Event_PlayerChangeClassPre, EventHookMode_Pre ); + HookEvent( "player_changeclass", Event_PlayerChangeClass, EventHookMode_Post ); + HookEvent( "teamplay_point_captured", Event_PointCaptured, EventHookMode_Post ); + + AddGameLogHook( OnGameLog ); + AddTempEntHook( "Shotgun Shot", OnFireBullets ); +} + +public OnAllPluginsLoaded() +{ + for ( new i = 1; i <= MaxClients; i++ ) + { + if ( IsClientInGame( i ) ) + { + OnClientPutInServer( i ); + } + } +} + +public OnMapStart() +{ + static bool:bLoggedMap = false; + if( g_bLate && !bLoggedMap ) + { + LogMapLoad(); + } + + bLoggedMap = true; + + GetTeams(); +} + +public OnClientPutInServer( client ) +{ + SDKHook( client, SDKHook_TraceAttackPost, OnTraceAttack ); + SDKHook( client, SDKHook_OnTakeDamagePost, OnTakeDamage ); + reset_player_stats( client ); + + g_LastTeam[client] = -1; + g_LastClass[client] = -1; + g_DiloGoatBite[client] = false; +} + +public Action:OnFireBullets( const String:szName[], const clients[], clientCount, Float:flDelay ) +{ + new client = TE_ReadNum( "m_iPlayer" ) + 1; + new weapon_index = TE_ReadNum( "m_iWeaponID" ); + + if( weapon_index >= 0 ) + { + if( GetEntProp(client, Prop_Send, "m_iPlayerClass") == PLAYERCLASS_TREX && weapon_index == WEAPON_MG42 ) + { + weapon_index = WEAPON_TREX; + } + g_weapon_stats[client][weapon_index][LOG_HIT_SHOTS]++; + } + + return Plugin_Continue; +} + +public OnTraceAttack( victim, attacker, inflictor, Float:damage, damagetype, ammotype, hitbox, hitgroup ) +{ + if ( hitgroup > 0 && attacker > 0 && attacker <= MaxClients && victim > 0 && victim <= MaxClients ) + { + g_iNextHitgroup[victim] = hitgroup; + } +} + +public OnTakeDamage( victim, attacker, inflictor, Float:damage, damagetype ) +{ + if ( attacker > 0 && attacker <= MaxClients && victim > 0 && victim <= MaxClients ) + { + decl String: weapon[MAX_WEAPON_LEN + PREFIX_LEN]; + GetClientWeapon( attacker, weapon, sizeof(weapon) ); + + new weapon_index = get_weapon_index(weapon[PREFIX_LEN]); + new hitgroup = g_iNextHitgroup[victim]; + if ( hitgroup < 8 ) + { + hitgroup += LOG_HIT_OFFSET; + } + + new bool:headshot = ( !IsPlayerAlive( victim ) && g_iNextHitgroup[victim] == HITGROUP_HEAD ); + if ( weapon_index > -1 ) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HITS]++; + g_weapon_stats[attacker][weapon_index][LOG_HIT_DAMAGE] += RoundToNearest( damage ); + g_weapon_stats[attacker][weapon_index][hitgroup]++; + if ( headshot ) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HEADSHOTS]++; + LogPlayerEvent( attacker, "triggered", "headshot" ); + } + } + + g_iNextHitgroup[victim] = 0; + } +} + +public OnEntityCreated(entity, const String:classname[]) +{ + if(entity > MaxClients && IsValidEdict(entity) && StrEqual(classname, "npc_goat", true)) + { + SDKHook(entity, SDKHook_ThinkPost, OnGoatThinkPost); + } +} + +public OnGoatThinkPost(npc_goat) +{ + if(npc_goat > MaxClients && IsValidEdict(npc_goat)) + { + new client = GetEntPropEnt(npc_goat, Prop_Send, "moveparent"); + if(client >= 1 && client <= MaxClients && IsClientInGame(client) && IsPlayerAlive(client) && GetClientTeam(client) == DDD_TEAM_AXIS && GetEntProp(client, Prop_Send, "m_iPlayerClass") == PLAYERCLASS_DILOPHOSAURUS) + { + if(GetEntProp(client, Prop_Send, "m_nSequence") == GOAT_BITE) + { + g_DiloGoatBite[client] = true; + } + } + } +} + +public Action:OnGameLog( const String:szMessage[] ) +{ + if( g_bIgnoreLog ) + return Plugin_Handled; + + return Plugin_Continue; +} + +public Action:Event_PointCaptured( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new client; + decl String:cappers[256]; + GetEventString(event, "cappers", cappers, sizeof(cappers)); + for (new i = 0 ; i < strlen(cappers); i++) + { + client = cappers[i]; + if(client > 0 && client <= MaxClients && IsClientInGame(client)) + { + LogPlayerEvent(client, "triggered", "point_captured"); + } + } + + return Plugin_Continue; +} + +public Action:Event_PlayerChangeClassPre( Handle:event, const String:name[], bool:dontBroadcast ) +{ + PauseLogging(); + + return Plugin_Continue; +} + +public Event_PlayerChangeClass( Handle:event, const String:name[], bool:dontBroadcast ) +{ + ResumeLogging(); +} + +public Action:Event_PlayerDeathPre( Handle:event, const String:name[], bool:dontBroadcast ) +{ + PauseLogging(); + + new customkill = GetEventInt(event, "customkill"); + if( customkill == CUSTOMKILL_GOAT ) + { + new attacker = GetClientOfUserId( GetEventInt( event, "attacker" ) ); + new victim = GetClientOfUserId( GetEventInt( event, "userid" ) ); + if(attacker == victim && g_DiloGoatBite[attacker]) + { + SetEventBroadcast(event, true); + return Plugin_Continue; + } + } + + return Plugin_Continue; +} + +public Event_PlayerDeath( Handle:event, const String:name[], bool:dontBroadcast ) +{ + ResumeLogging(); + + new victim = GetClientOfUserId( GetEventInt( event, "userid" ) ); + + if ( victim > 0 ) + { + new attacker = GetClientOfUserId( GetEventInt( event, "attacker" ) ); + + decl String:weapon[32]; + GetEventString( event, "weapon", weapon, sizeof(weapon) ); + if( attacker > 0 && attacker != victim ) + { + new weapon_index = get_weapon_index( weapon ); + if ( weapon_index > -1 ) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_KILLS]++; + g_weapon_stats[victim][weapon_index][LOG_HIT_DEATHS]++; + if ( GetClientTeam( attacker ) == GetClientTeam( victim ) ) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_TEAMKILLS]++; + } + } + + LogKill( attacker, victim, weapon, true ); + + new dominated = GetEventInt(event, "dominated"); + new revenge = GetEventInt(event, "revenge"); + if(dominated == 1) + { + LogPlyrPlyrEvent(attacker, victim, "triggered", "dominated"); + } + else if(revenge == 1) + { + LogPlyrPlyrEvent(attacker, victim, "triggered", "revenge"); + } + + new assisteruid = GetEventInt(event, "assister"); + if(assisteruid != -1) + { + new assister = GetClientOfUserId(assisteruid); + if(assister > 0 && attacker != assister) + { + LogPlayerEvent(assister, "triggered", "assister"); + if(GetEventInt(event, "assister_revenge") == 1) + { + LogPlyrPlyrEvent(assister, victim, "triggered", "assister_revenge"); + } + else if(GetEventInt(event, "assister_dominated") == 1) + { + LogPlyrPlyrEvent(assister, victim, "triggered", "assister_dominated"); + } + } + } + + } + else + { + new customkill = GetEventInt(event, "customkill"); + if( customkill == CUSTOMKILL_GOAT ) + { + if(g_DiloGoatBite[attacker]) + { + g_DiloGoatBite[attacker] = false; + return; + } + LogPlayerEvent(attacker, "triggered", "kill_goat"); + return; + } + + LogPlayerEvent(victim, "committed suicide with", weapon); + } + + dump_player_stats( victim ); + } +} + +public Event_PlayerSpawn( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new client = GetClientOfUserId( GetEventInt( event, "userid" ) ); + if( client == 0 || !IsClientInGame(client) ) + return; + + reset_player_stats( client ); + + new currentTeam = GetClientTeam( client ); + if( currentTeam != DDD_TEAM_ALLIES && currentTeam != DDD_TEAM_AXIS ) + return; + + new currentClass = GetEntProp( client, Prop_Send, "m_iPlayerClass" ); + if( g_LastClass[client] != currentClass || g_LastTeam[client] != currentTeam ) + { + decl String:szRoleString[32]; + if( currentTeam == DDD_TEAM_ALLIES ) + Format( szRoleString, sizeof(szRoleString), "#class_blue_class%d", currentClass+1 ); + else // == DDD_TEAM_AXIS + Format( szRoleString, sizeof(szRoleString), "#class_red_class%d", currentClass+1 ); + + LogRoleChange( client, szRoleString ); + g_LastTeam[client] = currentTeam; + g_LastClass[client] = currentClass; + } + g_DiloGoatBite[client] = false; +} + +public Event_RoundEnd( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new winner = GetEventInt( event, "team_that_won" ); + if( winner == DDD_TEAM_ALLIES || winner == DDD_TEAM_AXIS ) + { + decl String:round_win[32]; + Format(round_win, sizeof(round_win), "%s", winner == DDD_TEAM_ALLIES ? "roundwin_allies" : "roundwin_axis"); + LogTeamEvent(winner, "triggered", round_win); + WstatsDumpAll(); + } +} + +public OnClientDisconnect( client ) +{ + OnPlayerDisconnect( client ); +} \ No newline at end of file diff --git a/sourcemod/scripting/superlogs-dods.sp b/sourcemod/scripting/superlogs-dods.sp new file mode 100644 index 0000000..82958de --- /dev/null +++ b/sourcemod/scripting/superlogs-dods.sp @@ -0,0 +1,410 @@ +/** + * HLstatsX Community Edition - SourceMod plugin to generate advanced weapon logging + * http://www.hlxcommunity.com + * Copyright (C) 2009 Nicholas Hastings (psychonic) + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma semicolon 1 + +#include +#include + +#define NAME "SuperLogs: DOD:S" +#define VERSION "1.1.3" + +#define MAX_LOG_WEAPONS 27 +#define IGNORE_SHOTS_START 20 +#define MAX_WEAPON_LEN 16 + + +new g_weapon_stats[MAXPLAYERS+1][MAX_LOG_WEAPONS][15]; +new const String: g_weapon_list[MAX_LOG_WEAPONS][MAX_WEAPON_LEN] = { + "amerknife", + "spade", + "colt", + "p38", + "c96", + "garand", + "m1carbine", + "k98", + "spring", + "k98_scoped", + "thompson", + "mp40", + "mp44", + "bar", + "30cal", + "mg42", + "bazooka", + "pschreck", + "frag_us", + "frag_ger", + "", + "", + "smoke_us", + "smoke_ger", + "riflegren_us", + "riflegren_ger", + "dod_bomb_target" + }; + + +new Handle:g_cvar_wstats = INVALID_HANDLE; +new Handle:g_cvar_headshots = INVALID_HANDLE; +new Handle:g_cvar_locations = INVALID_HANDLE; +new Handle:g_cvar_ktraj = INVALID_HANDLE; +new Handle:g_cvar_version = INVALID_HANDLE; + +new bool:g_logwstats = true; +new bool:g_logheadshots = true; +new bool:g_loglocations = true; +new bool:g_logktraj = true; + +#include +#include + + +public Plugin:myinfo = { + name = NAME, + author = "psychonic", + description = "Advanced logging for DOD:S. Generates auxilary logging for use with log parsers such as HLstatsX and Psychostats", + version = VERSION, + url = "http://www.hlxcommunity.com" +}; + +#if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 +public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) +#else +public bool:AskPluginLoad(Handle:myself, bool:late, String:error[], err_max) +#endif +{ + decl String:game_description[64]; + GetGameDescription(game_description, sizeof(game_description), true); + if (StrContains(game_description, "Day of Defeat", false) == -1) + { + decl String:game_folder[64]; + GetGameFolderName(game_folder, sizeof(game_folder)); + if (strncmp(game_folder, "dod", 3, false) != 0) + { + strcopy(error, err_max, "This plugin is only supported on DOD:S"); + #if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 + return APLRes_Failure; + #else + return false; + #endif + } + } +#if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 + return APLRes_Success; +#else + return true; +#endif +} + + +public OnPluginStart() +{ + CreatePopulateWeaponTrie(); + + g_cvar_wstats = CreateConVar("superlogs_wstats", "1", "Enable logging of weapon stats (default on)", 0, true, 0.0, true, 1.0); + g_cvar_headshots = CreateConVar("superlogs_headshots", "1", "Enable logging of headshot player action (default on)", 0, true, 0.0, true, 1.0); + g_cvar_locations = CreateConVar("superlogs_locations", "1", "Enable logging of location on player death (default on)", 0, true, 0.0, true, 1.0); + g_cvar_ktraj = CreateConVar("superlogs_ktraj", "0", "Enable Psychostats \"KTRAJ\" logging (default off)", 0, true, 0.0, true, 1.0); + HookConVarChange(g_cvar_wstats, OnCvarWstatsChange); + HookConVarChange(g_cvar_headshots, OnCvarHeadshotsChange); + HookConVarChange(g_cvar_locations, OnCvarLocationsChange); + HookConVarChange(g_cvar_ktraj, OnCvarKtrajChange); + g_cvar_version = CreateConVar("superlogs_dods_version", VERSION, NAME, FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY); + + hook_wstats(); + HookEvent("player_hurt", Event_PlayerHurt); + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + HookEvent("player_death", Event_PlayerDeath); + + CreateTimer(1.0, LogMap); + + GetTeams(); +} + + +public OnMapStart() +{ + GetTeams(); +} + + +public OnConfigsExecuted() +{ + decl String:version[255]; + GetConVarString(g_cvar_version, version, sizeof(version)); + SetConVarString(g_cvar_version, version); +} + + +hook_wstats() +{ + HookEvent("dod_stats_weapon_attack", Event_PlayerShoot); + HookEvent("player_spawn", Event_PlayerSpawn); + HookEvent("dod_round_win", Event_RoundEnd, EventHookMode_PostNoCopy); + HookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre); +} + +unhook_wstats() +{ + UnhookEvent("dod_stats_weapon_attack", Event_PlayerShoot); + UnhookEvent("player_spawn", Event_PlayerSpawn); + UnhookEvent("dod_round_win", Event_RoundEnd, EventHookMode_PostNoCopy); + UnhookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre); +} + +public OnClientPutInServer(client) +{ + reset_player_stats(client); +} + + +public Event_PlayerShoot(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "attacker" "short" + // "weapon" "byte" + + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + if (attacker > 0) + { + new weapon_index = GetEventInt(event, "weapon") - 1; + switch (weapon_index) + { + case 28, 29: + weapon_index = -1; + case 30: + weapon_index = 5; + case 32: + weapon_index = 8; + case 33: + weapon_index = 9; + case 34: + weapon_index = 14; + case 35: + weapon_index = 15; + case 37: + weapon_index = 12; + } + if (weapon_index > -1 && weapon_index < IGNORE_SHOTS_START) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_SHOTS]++; + } + } +} + + +public Event_PlayerHurt(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID who was hurt + // "attacker" "short" // user ID who attacked + // "weapon" "string" // weapon name attacker used + // "health" "byte" // health remaining + // "damage" "byte" // how much damage in this attack + // "hitgroup" "byte" // what hitgroup was hit + + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + new hitgroup = GetEventInt(event, "hitgroup"); + new bool:headshot = (GetEventInt(event, "health") <= 0 && hitgroup == HITGROUP_HEAD); + + if (g_logwstats && attacker > 0) + { + decl String: weapon[MAX_WEAPON_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + new weapon_index = get_weapon_index(weapon); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HITS]++; + g_weapon_stats[attacker][weapon_index][LOG_HIT_DAMAGE] += GetEventInt(event, "damage"); + if (hitgroup < 8) + { + g_weapon_stats[attacker][weapon_index][hitgroup + LOG_HIT_OFFSET]++; + } + if (headshot) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HEADSHOTS]++; + } + } + } + if (g_logheadshots && headshot) + { + LogPlayerEvent(attacker, "triggered", "headshot"); + } +} + +public Action:Event_PlayerDeathPre(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogKillLoc(GetClientOfUserId(GetEventInt(event, "attacker")), GetClientOfUserId(GetEventInt(event, "userid"))); + + return Plugin_Continue; +} + +public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast) +{ + // this extents the original player_death + // "userid" "short" // user ID who died + // "attacker" "short" // user ID who killed + // "weapon" "string" // weapon name killed used + + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + decl String: weapon[MAX_WEAPON_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + + if (g_logwstats && victim > 0 && attacker > 0) + { + new weapon_index = get_weapon_index(weapon); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_KILLS]++; + g_weapon_stats[victim][weapon_index][LOG_HIT_DEATHS]++; + if (GetClientTeam(attacker) == GetClientTeam(victim)) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_TEAMKILLS]++; + } + dump_player_stats(victim); + } + } + if (g_logktraj) + { + LogPSKillTraj(attacker, victim, weapon); + } +} + +public Event_PlayerSpawn(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID on server + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + reset_player_stats(client); + } +} + +public Event_RoundEnd(Handle:event, const String:name[], bool:dontBroadcast) +{ + WstatsDumpAll(); +} + +public Action:Event_PlayerDisconnect(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + OnPlayerDisconnect(client); + return Plugin_Continue; +} + + +public Action:LogMap(Handle:timer) +{ + // Called 1 second after OnPluginStart since srcds does not log the first map loaded. Idea from Stormtrooper's "mapfix.sp" for psychostats + LogMapLoad(); +} + + +public OnCvarWstatsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logwstats; + g_logwstats = GetConVarBool(g_cvar_wstats); + + if (old_value != g_logwstats) + { + if (g_logwstats) + { + hook_wstats(); + if (!g_logheadshots) + { + HookEvent("player_hurt", Event_PlayerHurt); + } + if (!g_logktraj) + { + HookEvent("player_death", Event_PlayerDeath); + } + } + else + { + unhook_wstats(); + if (!g_logheadshots) + { + UnhookEvent("player_hurt", Event_PlayerHurt); + } + if (!g_logktraj) + { + UnhookEvent("player_death", Event_PlayerDeath); + } + } + } +} + + +public OnCvarHeadshotsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logheadshots; + g_logheadshots = GetConVarBool(g_cvar_headshots); + + if (old_value != g_logheadshots) + { + if (g_logheadshots && !g_logwstats) + { + HookEvent("player_hurt", Event_PlayerHurt); + } + else if (!g_logwstats) + { + UnhookEvent("player_hurt", Event_PlayerHurt); + } + } +} + +public OnCvarLocationsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_loglocations; + g_loglocations = GetConVarBool(g_cvar_locations); + + if (old_value != g_loglocations) + { + if (g_loglocations) + { + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + else + { + UnhookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + } +} + +public OnCvarKtrajChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logktraj; + g_logktraj = GetConVarBool(g_cvar_ktraj); + + if (old_value != g_logktraj) + { + if (g_logktraj && !g_logwstats) + { + HookEvent("player_death", Event_PlayerDeath); + } + else if (!g_logwstats) + { + UnhookEvent("player_death", Event_PlayerDeath); + } + } +} \ No newline at end of file diff --git a/sourcemod/scripting/superlogs-generic.sp b/sourcemod/scripting/superlogs-generic.sp new file mode 100644 index 0000000..b367d8a --- /dev/null +++ b/sourcemod/scripting/superlogs-generic.sp @@ -0,0 +1,89 @@ +/** + * HLstatsX Community Edition - SourceMod plugin to generate advanced weapon logging + * http://www.hlxcommunity.com + * Copyright (C) 2009 Nicholas Hastings (psychonic) + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma semicolon 1 + +#include +#include + +#define NAME "SuperLogs: Generic" +#define VERSION "1.0" + +new Handle:g_cvar_enable = INVALID_HANDLE; + +new bool:g_log = true; + +#include + + +public Plugin:myinfo = { + name = NAME, + author = "psychonic", + description = "Advanced logging. Generates auxilary logging for use with log parsers such as HLstatsX and Psychostats", + version = VERSION, + url = "http://www.hlxcommunity.com" +}; + + +public OnPluginStart() +{ + g_cvar_enable = CreateConVar("superlogs_locations", "1", "Enable logging of kill coordinates (default on)", 0, true, 0.0, true, 1.0); + + HookConVarChange(g_cvar_enable, OnCvarEnableChange); + + CreateConVar("superlogs_generic_version", VERSION, NAME, FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY); + + HookEvent("player_death", Event_PlayerDeath, EventHookMode_Pre); + + CreateTimer(1.0, LogMap); +} + + +public Action:Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogKillLoc(GetClientOfUserId(GetEventInt(event, "attacker")), GetClientOfUserId(GetEventInt(event, "userid"))); + + return Plugin_Continue; +} + +public OnCvarEnableChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_log; + g_log = GetConVarBool(g_cvar_enable); + + if (old_value != g_log) + { + if (g_log) + { + HookEvent("player_death", Event_PlayerDeath, EventHookMode_Pre); + } + else + { + UnhookEvent("player_death", Event_PlayerDeath, EventHookMode_Pre); + } + } +} + +public Action:LogMap(Handle:timer) +{ + // Called 1 second after OnPluginStart since srcds does not log the first map loaded. Idea from Stormtrooper's "mapfix.sp" for psychostats + LogMapLoad(); +} diff --git a/sourcemod/scripting/superlogs-ges.sp b/sourcemod/scripting/superlogs-ges.sp new file mode 100644 index 0000000..2119be1 --- /dev/null +++ b/sourcemod/scripting/superlogs-ges.sp @@ -0,0 +1,478 @@ +/** + * HLstatsX Community Edition - SourceMod plugin to generate advanced weapon logging + * http://www.hlxcommunity.com + * Copyright (C) 2009 Nicholas Hastings (psychonic) + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma semicolon 1 + +#include +#include + +#define NAME "SuperLogs: GES" +#define VERSION "1.1.2" + +#define MAX_LOG_WEAPONS 18 +#define MAX_WEAPON_LEN 13 +#define PREFIX_LEN 7 + +#define GES + + +new g_weapon_stats[MAXPLAYERS+1][MAX_LOG_WEAPONS][20]; +new const String:g_weapon_list[MAX_LOG_WEAPONS][MAX_WEAPON_LEN] = { + "pp7", + "pp7_silenced", + "dd44", + "klobb", + "cmag", + "kf7", + "zmg", + "d5k", + "d5k_silenced", + "phantom", + "AR33", + "rcp90", + "shotgun", + "auto_shotgun", + "sniper_rifle", + "golden_gun", + "silver_pp7", + "golden_pp7" + }; + +new const String:g_weapon_loglist[MAX_LOG_WEAPONS][] = { + "#GE_PP7", + "#GE_PP7_SILENCED", + "#GE_DD44", + "#GE_Klobb", + "#GE_CougarMagnum", + "#GE_KF7Soviet", + "#GE_ZMG", + "#GE_D5K", + "#GE_D5K_SILENCED", + "#GE_Phantom", + "#GE_AR33", + "#GE_RCP90", + "#GE_Shotgun", + "#GE_AutoShotgun", + "#GE_SniperRifle", + "#GE_GoldenGun", + "#GE_SilverPP7", + "#GE_GoldPP7" + }; + +new Handle:g_cvar_wstats = INVALID_HANDLE; +new Handle:g_cvar_headshots = INVALID_HANDLE; +new Handle:g_cvar_locations = INVALID_HANDLE; +new Handle:g_cvar_actions = INVALID_HANDLE; +new Handle:g_cvar_classchanges = INVALID_HANDLE; + +new bool:g_logwstats = true; +new bool:g_logheadshots = true; +new bool:g_loglocations = true; +new bool:g_logactions = true; +new bool:g_logclasschanges = true; + +#include +#include + + +public Plugin:myinfo = { + name = NAME, + author = "psychonic", + description = "Advanced logging for GoldenEye: Source. Generates auxilary logging for use with log parsers such as HLstatsX and Psychostats", + version = VERSION, + url = "http://www.hlxcommunity.com" +}; + + +public OnPluginStart() +{ + CreatePopulateWeaponTrie(); + + g_cvar_wstats = CreateConVar("superlogs_wstats", "1", "Enable logging of weapon stats (default on)", 0, true, 0.0, true, 1.0); + g_cvar_headshots = CreateConVar("superlogs_headshots", "1", "Enable logging of headshot player action (default off)", 0, true, 0.0, true, 1.0); + g_cvar_locations = CreateConVar("superlogs_locations", "1", "Enable logging of location on player death (default on)", 0, true, 0.0, true, 1.0); + g_cvar_actions = CreateConVar("superlogs_actions", "1", "Enable logging of actions, such as round winner and awards won (default on)", 0, true, 0.0, true, 1.0); + g_cvar_classchanges = CreateConVar("superlogs_classchanges", "1", "Enable logging of character changes (default on)", 0, true, 0.0, true, 1.0); + HookConVarChange(g_cvar_wstats, OnCvarWstatsChange); + HookConVarChange(g_cvar_headshots, OnCvarHeadshotsChange); + HookConVarChange(g_cvar_locations, OnCvarLocationsChange); + HookConVarChange(g_cvar_actions, OnCvarActionsChange); + HookConVarChange(g_cvar_classchanges, OnCvarClasschangesChange); + CreateConVar("superlogs_ges_version", VERSION, NAME, FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY); + + hook_wstats(); + HookEvent("player_hurt", Event_PlayerHurt); + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + HookEvent("player_changeident", Event_RoleChange); + HookEvent("round_end", Event_RoundEnd); + + CreateTimer(1.0, LogMap); + + GetTeams(); +} + + +public OnMapStart() +{ + GetTeams(); +} + +hook_wstats() +{ + HookEvent("player_death", Event_PlayerDeath); + HookEvent("player_shoot", Event_PlayerShoot); + HookEvent("player_spawn", Event_PlayerSpawn); + HookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre); +} + +unhook_wstats() +{ + UnhookEvent("player_death", Event_PlayerDeath); + UnhookEvent("player_shoot", Event_PlayerShoot); + UnhookEvent("player_spawn", Event_PlayerSpawn); + UnhookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre); +} + +public OnClientPutInServer(client) +{ + reset_player_stats(client); +} + + +public Event_PlayerShoot(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "local" // user ID on server + // "weapon" "local" // weapon name + // "mode" "local" // weapon mode [0 normal, 1 aimed] + + new attacker = GetClientOfUserId(GetEventInt(event, "userid")); + if (g_logwstats && attacker > 0) + { + decl String:weapon[MAX_WEAPON_LEN+PREFIX_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + new weapon_index = get_weapon_index(weapon[PREFIX_LEN]); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_SHOTS]++; + } + } +} + + +public Event_PlayerHurt(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "local" // user ID who was hurt + // "attacker" "local" // user ID who attacked + // "weapon" "local" // weapon name attacker used + // "health" "local" // health remaining + // "armor" "local" // armor remaining + // "damage" "local" // how much damage in this attack + // "hitgroup" "local" // what hitgroup was hit + + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + new hitgroup = GetEventInt(event, "hitgroup"); + new bool:headshot = (GetEventInt(event, "health") <= 0 && hitgroup == HITGROUP_HEAD); + + if (g_logwstats && attacker > 0) + { + decl String: weapon[MAX_WEAPON_LEN+PREFIX_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + + new weapon_index = get_weapon_index(weapon[PREFIX_LEN]); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HITS]++; + g_weapon_stats[attacker][weapon_index][LOG_HIT_DAMAGE] += GetEventInt(event, "damage"); + if (hitgroup < 8) + { + g_weapon_stats[attacker][weapon_index][hitgroup + LOG_HIT_OFFSET]++; + } + if (headshot) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HEADSHOTS]++; + } + } + } + if (g_logheadshots && headshot) + { + LogPlayerEvent(attacker, "triggered", "headshot"); + } +} + +public Action:Event_PlayerDeathPre(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogKillLoc(GetClientOfUserId(GetEventInt(event, "attacker")), GetClientOfUserId(GetEventInt(event, "userid"))); + + return Plugin_Continue; +} + + +public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID who died + // "attacker" "short" // user ID who killed + // "weapon" "string" // weapon name killed used + // "weaponid" "short" // GE Weapon ID (for easy comparison) + // "custom" "byte" // Used for achievements + + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + if (g_logwstats && (victim > 0) && (attacker > 0)) + { + decl String: weapon[MAX_WEAPON_LEN+PREFIX_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + new weapon_index = get_weapon_index(weapon[PREFIX_LEN]); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_KILLS]++; + g_weapon_stats[victim][weapon_index][LOG_HIT_DEATHS]++; + if (GetClientTeam(attacker) == GetClientTeam(victim)) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_TEAMKILLS]++; + } + dump_player_stats(victim); + } + } +} + +public Event_PlayerSpawn(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID on server + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + reset_player_stats(client); + } +} + +public Event_RoundEnd(Handle:event, const String:name[], bool:dontBroadcast) +{ + if (g_logwstats) + { + WstatsDumpAll(); + } + if (g_logactions) + { + new winner = GetEventInt(event, "winnerid"); + if (winner > 0) + { + LogPlayerEvent(winner, "triggered", "Round_Win", true); + } + else + { + winner = GetEventInt(event, "teamid"); + if (winner > 0) + { + LogTeamEvent(winner, "triggered", "Round_Win_Team"); + } + } + new awards[6]; + awards[0] = GetEventInt(event, "award1_id"); + awards[1] = GetEventInt(event, "award2_id"); + awards[2] = GetEventInt(event, "award3_id"); + awards[3] = GetEventInt(event, "award4_id"); + awards[4] = GetEventInt(event, "award5_id"); + awards[5] = GetEventInt(event, "award6_id"); + + new winners[6]; + winners[0] = GetEventInt(event, "award1_winner"); + winners[1] = GetEventInt(event, "award2_winner"); + winners[2] = GetEventInt(event, "award3_winner"); + winners[3] = GetEventInt(event, "award4_winner"); + winners[4] = GetEventInt(event, "award5_winner"); + winners[5] = GetEventInt(event, "award6_winner"); + + for (new i = 0; i < 6; i++) + { + if (winners[i] > 0) + { + switch(awards[i]) + { + case 0: + LogPlayerEvent(winners[i], "triggered", "GE_AWARD_DEADLY", true); + case 1: + LogPlayerEvent(winners[i], "triggered", "GE_AWARD_HONORABLE", true); + case 2: + LogPlayerEvent(winners[i], "triggered", "GE_AWARD_PROFESSIONAL", true); + case 3: + LogPlayerEvent(winners[i], "triggered", "GE_AWARD_MARKSMANSHIP", true); + case 4: + LogPlayerEvent(winners[i], "triggered", "GE_AWARD_AC10", true); + case 5: + LogPlayerEvent(winners[i], "triggered", "GE_AWARD_FRANTIC", true); + case 6: + LogPlayerEvent(winners[i], "triggered", "GE_AWARD_WTA", true); + case 7: + LogPlayerEvent(winners[i], "triggered", "GE_AWARD_LEMMING", true); + case 8: + LogPlayerEvent(winners[i], "triggered", "GE_AWARD_LONGIN", true); + case 9: + LogPlayerEvent(winners[i], "triggered", "GE_AWARD_SHORTIN", true); + case 10: + LogPlayerEvent(winners[i], "triggered", "GE_AWARD_DISHONORABLE", true); + case 11: + LogPlayerEvent(winners[i], "triggered", "GE_AWARD_NOTAC10", true); + case 12: + LogPlayerEvent(winners[i], "triggered", "GE_AWARD_MOSTLYHARMLESS", true); + } + } + } + } +} + + +public Event_RoleChange(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "playerid" "short" + // "ident" "string" + + new client = GetEventInt(event, "playerid"); + + if (client > 0) + { + decl String:ident[32]; + GetEventString(event, "ident", ident, sizeof(ident)); + LogRoleChange(client, ident); + } +} + +public Action:Event_PlayerDisconnect(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + OnPlayerDisconnect(client); + return Plugin_Continue; +} + +public Action:LogMap(Handle:timer) +{ + // Called 1 second after OnPluginStart since srcds does not log the first map loaded. Idea from Stormtrooper's "mapfix.sp" for psychostats + LogMapLoad(); +} + + +public OnCvarWstatsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logwstats; + g_logwstats = GetConVarBool(g_cvar_wstats); + + if (old_value != g_logwstats) + { + if (g_logwstats) + { + hook_wstats(); + if (!g_logheadshots) + { + HookEvent("player_hurt", Event_PlayerHurt); + } + if (!g_logactions) + { + HookEvent("round_end", Event_RoundEnd); + } + } + else + { + unhook_wstats(); + if (!g_logheadshots) + { + UnhookEvent("player_hurt", Event_PlayerHurt); + } + if (!g_logactions) + { + UnhookEvent("round_end", Event_RoundEnd); + } + } + } +} + + + +public OnCvarHeadshotsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logheadshots; + g_logheadshots = GetConVarBool(g_cvar_headshots); + + if (old_value != g_logheadshots) + { + if (g_logheadshots && !g_logwstats) + { + HookEvent("player_hurt", Event_PlayerHurt); + } + else if (!g_logwstats) + { + UnhookEvent("player_hurt", Event_PlayerHurt); + } + } +} + +public OnCvarLocationsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_loglocations; + g_loglocations = GetConVarBool(g_cvar_locations); + + if (old_value != g_loglocations) + { + if (g_loglocations) + { + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + else + { + UnhookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + } +} + +public OnCvarActionsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logactions; + g_logactions = GetConVarBool(g_cvar_actions); + + if (old_value != g_logactions) + { + if (g_logactions && !g_logwstats) + { + HookEvent("round_end", Event_RoundEnd); + } + else if (!g_logactions) + { + UnhookEvent("round_end", Event_RoundEnd); + } + } +} + +public OnCvarClasschangesChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logclasschanges; + g_logclasschanges = GetConVarBool(g_cvar_classchanges); + + if (old_value != g_logclasschanges) + { + if (g_logclasschanges) + { + HookEvent("player_changeident", Event_RoleChange); + } + else + { + UnhookEvent("player_changeident", Event_RoleChange); + } + } +} diff --git a/sourcemod/scripting/superlogs-hl2mp.sp b/sourcemod/scripting/superlogs-hl2mp.sp new file mode 100644 index 0000000..9ea1a8c --- /dev/null +++ b/sourcemod/scripting/superlogs-hl2mp.sp @@ -0,0 +1,402 @@ +/** + * HLstatsX Community Edition - SourceMod plugin to generate advanced weapon logging + * http://www.hlxcommunity.com + * Copyright (C) 2009-2010 Nicholas Hastings (psychonic) + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma semicolon 1 + +#include +#include +#include + +#define NAME "SuperLogs: HL2MP" +#define VERSION "1.1.3" + +#define MAX_LOG_WEAPONS 6 +#define MAX_WEAPON_LEN 14 +#define PREFIX_LEN 7 +#define CROSSBOW 0 + + +new g_weapon_stats[MAXPLAYERS+1][MAX_LOG_WEAPONS][15]; +new const String:g_weapon_list[MAX_LOG_WEAPONS][MAX_WEAPON_LEN] = { + "crossbow_bolt", + "pistol", + "357", + "smg1", + "ar2", + "shotgun" + }; + +new Handle:g_cvar_headshots = INVALID_HANDLE; +new Handle:g_cvar_locations = INVALID_HANDLE; +new Handle:g_cvar_teamplay = INVALID_HANDLE; + +new bool:g_logheadshots = true; +new bool:g_loglocations = true; + +new g_iNextHitgroup[MAXPLAYERS+1]; +new g_iNextBowHitgroup[MAXPLAYERS+1]; + +new g_bTeamPlay; + +new g_iCrossBowOwnerOffs = -1; + +new Handle:g_hBoltChecks = INVALID_HANDLE; + +#include +#include + + +public Plugin:myinfo = { + name = NAME, + author = "psychonic", + description = "Advanced logging for HL2DM & Sourceforts. Generates auxilary logging for use with log parsers such as HLstatsX and Psychostats", + version = VERSION, + url = "http://www.hlxcommunity.com" +}; + +#if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 +public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) +#else +public bool:AskPluginLoad(Handle:myself, bool:late, String:error[], err_max) +#endif +{ + decl String:szGameDesc[64]; + GetGameDescription(szGameDesc, sizeof(szGameDesc), true); + if (StrContains(szGameDesc, "Half-Life 2 Deathmatch", false) == -1 && StrContains(szGameDesc, "SourceForts", false) == -1) + { + decl String:szGameDir[64]; + GetGameFolderName(szGameDir, sizeof(szGameDir)); + if (StrContains(szGameDir, "hl2mp", false) == -1 && StrContains(szGameDir, "sourceforts", false) == -1) + { + strcopy(error, err_max, "This plugin is only supported on HL2MP & SourceForts"); + #if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 + return APLRes_Failure; + #else + return false; + #endif + } + } +#if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 + return APLRes_Success; +#else + return true; +#endif +} + +public OnPluginStart() +{ + CreatePopulateWeaponTrie(); + + g_cvar_headshots = CreateConVar("superlogs_headshots", "1", "Enable logging of headshot player action (default on)", 0, true, 0.0, true, 1.0); + g_cvar_locations = CreateConVar("superlogs_locations", "1", "Enable logging of location on player death (default on)", 0, true, 0.0, true, 1.0); + HookConVarChange(g_cvar_headshots, OnCvarHeadshotsChange); + HookConVarChange(g_cvar_locations, OnCvarLocationsChange); + CreateConVar("superlogs_hl2mp_version", VERSION, NAME, FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY); + + g_iCrossBowOwnerOffs = FindSendPropInfo("CCrossbowBolt", "m_hOwnerEntity"); + + hook_wstats(); + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + + CreateTimer(1.0, LogMap); + + g_cvar_teamplay = FindConVar("mp_teamplay"); + if (g_cvar_teamplay != INVALID_HANDLE) + { + g_bTeamPlay = GetConVarBool(g_cvar_teamplay); + HookConVarChange(g_cvar_teamplay, OnTeamPlayChange); + } + + g_hBoltChecks = CreateStack(); +} + +public OnAllPluginsLoaded() +{ + if (GetExtensionFileStatus("sdkhooks.ext") != 1) + { + SetFailState("SDK Hooks v1.3 or higher is required for SuperLogs: HL2MP"); + } + for (new i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i)) + { + SDKHook(i, SDKHook_FireBulletsPost, OnFireBullets); + SDKHook(i, SDKHook_TraceAttackPost, OnTraceAttack); + SDKHook(i, SDKHook_OnTakeDamagePost, OnTakeDamage); + } + } +} + +public OnMapStart() +{ + GetTeams(); +} + +hook_wstats() +{ + HookEvent("player_death", Event_PlayerDeath); + HookEvent("player_spawn", Event_PlayerSpawn); + HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy); + HookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre); +} + +public OnClientPutInServer(client) +{ + SDKHook(client, SDKHook_FireBulletsPost, OnFireBullets); + SDKHook(client, SDKHook_TraceAttackPost, OnTraceAttack); + SDKHook(client, SDKHook_OnTakeDamagePost, OnTakeDamage); + reset_player_stats(client); +} + +public OnEntityCreated(entity, const String:classname[]) +{ + if (strcmp(classname, "crossbow_bolt") == 0) + { + PushStackCell(g_hBoltChecks, entity); + } +} + +public OnGameFrame() +{ + new bowent; + while (PopStackCell(g_hBoltChecks, bowent)) + { + if (!IsValidEntity(bowent)) + continue; + + new owner = GetEntDataEnt2(bowent, g_iCrossBowOwnerOffs); + if (owner < 0 || owner > MaxClients) + continue; + + g_weapon_stats[owner][CROSSBOW][LOG_HIT_SHOTS]++; + } +} + +public OnFireBullets(attacker, shots, String:weaponname[]) +{ + if (attacker > 0 && attacker <= MaxClients) + { + new weapon_index = get_weapon_index(weaponname[PREFIX_LEN]); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_SHOTS]++; + } + } +} + +public OnTraceAttack(victim, attacker, inflictor, Float:damage, damagetype, ammotype, hitbox, hitgroup) +{ + if (hitgroup > 0 && attacker > 0 && attacker <= MaxClients && victim > 0 && victim <= MaxClients) + { + if (IsValidEntity(inflictor)) + { + decl String:inflictorclsname[64]; + if (GetEntityNetClass(inflictor, inflictorclsname, sizeof(inflictorclsname)) && strcmp(inflictorclsname, "CCrossbowBolt") == 0) + { + g_iNextBowHitgroup[victim] = hitgroup; + return; + } + } + g_iNextHitgroup[victim] = hitgroup; + } +} + +public OnTakeDamage(victim, attacker, inflictor, Float:damage, damagetype) +{ + if (attacker > 0 && attacker <= MaxClients && victim > 0 && victim <= MaxClients) + { + decl String: weapon[MAX_WEAPON_LEN + PREFIX_LEN]; + GetClientWeapon(attacker, weapon, sizeof(weapon)); + new weapon_index = -1; + if (IsValidEntity(inflictor)) + { + decl String:inflictorclsname[64]; + if (GetEntityNetClass(inflictor, inflictorclsname, sizeof(inflictorclsname)) && strcmp(inflictorclsname, "CCrossbowBolt") == 0) + { + weapon_index = CROSSBOW; + } + } + if (weapon_index == -1) + { + weapon_index = get_weapon_index(weapon[PREFIX_LEN]); + } + new hitgroup = ((weapon_index == CROSSBOW)?g_iNextBowHitgroup[victim]:g_iNextHitgroup[victim]); + if (hitgroup < 8) + { + hitgroup += LOG_HIT_OFFSET; + } + new bool:headshot = (GetClientHealth(victim) <= 0 && hitgroup == HITGROUP_HEAD); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HITS]++; + g_weapon_stats[attacker][weapon_index][LOG_HIT_DAMAGE] += RoundToNearest(damage); + g_weapon_stats[attacker][weapon_index][hitgroup]++; + if (headshot) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HEADSHOTS]++; + } + } + if (weapon_index == CROSSBOW) + { + g_iNextBowHitgroup[victim] = 0; + } + else + { + g_iNextHitgroup[victim] = 0; + } + } +} + + +public Action:Event_PlayerDeathPre(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID who died + // "attacker" "short" // user ID who killed + // "weapon" "string" // weapon name killer used + + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + + if (g_loglocations) + { + LogKillLoc(attacker, victim); + } + if (g_logheadshots) + { + decl String:weapon[64]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + if (strcmp(weapon, "crossbow_bolt") == 0) + { + if (g_iNextBowHitgroup[victim] == HITGROUP_HEAD) + { + LogPlayerEvent(attacker, "triggered", "headshot"); + } + } + else if (g_iNextHitgroup[victim] == HITGROUP_HEAD) + { + LogPlayerEvent(attacker, "triggered", "headshot"); + } + } + + return Plugin_Continue; +} + +public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast) +{ + // this extents the original player_death by a new fields + // "userid" "short" // user ID who died + // "attacker" "short" // user ID who killed + // "weapon" "string" // weapon name killer used + + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + + if (victim > 0) + { + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + if (attacker != victim && attacker > 0) + { + decl String: weapon[MAX_WEAPON_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + new weapon_index = get_weapon_index(weapon); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_KILLS]++; + g_weapon_stats[victim][weapon_index][LOG_HIT_DEATHS]++; + if (g_bTeamPlay && GetClientTeam(attacker) == GetClientTeam(victim)) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_TEAMKILLS]++; + } + } + } + dump_player_stats(victim); + } +} + +public Event_PlayerSpawn(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID on server + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + reset_player_stats(client); + } +} + +public Event_RoundEnd(Handle:event, const String:name[], bool:dontBroadcast) +{ + WstatsDumpAll(); +} + +public Action:Event_PlayerDisconnect(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + OnPlayerDisconnect(client); + return Plugin_Continue; +} + + +public Action:LogMap(Handle:timer) +{ + // Called 1 second after OnPluginStart since srcds does not log the first map loaded. Idea from Stormtrooper's "mapfix.sp" for psychostats + LogMapLoad(); +} + +public OnCvarHeadshotsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logheadshots; + g_logheadshots = GetConVarBool(g_cvar_headshots); + + if (old_value != g_logheadshots) + { + if (g_logheadshots && !g_loglocations) + { + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + else if (!g_loglocations) + { + UnhookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + } +} + +public OnCvarLocationsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_loglocations; + g_loglocations = GetConVarBool(g_cvar_locations); + + if (old_value != g_loglocations) + { + if (g_loglocations && !g_logheadshots) + { + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + else if (!g_logheadshots) + { + UnhookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + } +} + +public OnTeamPlayChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + g_bTeamPlay = GetConVarBool(g_cvar_teamplay); +} \ No newline at end of file diff --git a/sourcemod/scripting/superlogs-ins.sp b/sourcemod/scripting/superlogs-ins.sp new file mode 100644 index 0000000..77e1517 --- /dev/null +++ b/sourcemod/scripting/superlogs-ins.sp @@ -0,0 +1,508 @@ +/** + * HLstatsX Community Edition - SourceMod plugin to generate advanced weapon logging + * http://www.hlxcommunity.com + * Copyright (C) 2009 Nicholas Hastings (psychonic) + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma semicolon 1 + +#include +#include + +#define NAME "SuperLogs: Insurgency" +#define VERSION "1.1.4" + +#define MAX_LOG_WEAPONS 19 +#define MAX_WEAPON_LEN 8 + +#define PREFIX_LEN 7 + +#define INS + +new g_weapon_stats[MAXPLAYERS+1][MAX_LOG_WEAPONS][15]; +new const String: g_weapon_list[MAX_LOG_WEAPONS][MAX_WEAPON_LEN] = { + "makarov", + "m9", + "sks", + "m1014", + "toz", + "svd", + "rpk", + "m249", + "m16m203", + "l42a1", + "m4med", + "m4", + "m16a4", + "m14", + "fnfal", + "aks74u", + "ak47", + "kabar", + "bayonet" + }; + +new Handle:g_cvar_wstats = INVALID_HANDLE; +new Handle:g_cvar_actions = INVALID_HANDLE; +new Handle:g_cvar_headshots = INVALID_HANDLE; +new Handle:g_cvar_chat = INVALID_HANDLE; +new Handle:g_cvar_captures = INVALID_HANDLE; +new Handle:g_cvar_locations = INVALID_HANDLE; +new Handle:g_cvar_ktraj = INVALID_HANDLE; + +new bool:g_logwstats = true; +new bool:g_logheadshots = true; +new bool:g_logactions = true; +new bool:g_logchat = true; +new bool:g_logcaptures = true; +new bool:g_loglocations = true; +new bool:g_logktraj = false; + +new g_client_last_weapon[MAXPLAYERS+1] = {-1, ...}; +new String:g_client_last_weaponstring[MAXPLAYERS+1][64]; + +#include +#include + + +public Plugin:myinfo = { + name = NAME, + author = "psychonic", + description = "Advanced logging for Insurgency. Generates auxilary logging for use with log parsers such as HLstatsX and Psychostats", + version = VERSION, + url = "http://www.hlxcommunity.com" +}; + +#if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 +public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) +#else +public bool:AskPluginLoad(Handle:myself, bool:late, String:error[], err_max) +#endif +{ + decl String:game_description[64]; + GetGameDescription(game_description, sizeof(game_description), true); + if (StrContains(game_description, "Insurgency", false) == -1) + { + decl String:game_folder[64]; + GetGameFolderName(game_folder, sizeof(game_folder)); + if (StrContains(game_folder, "insurgency", false) == -1) + { + #if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 + return APLRes_Failure; + #else + return false; + #endif + } + } +#if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 + return APLRes_Success; +#else + return true; +#endif +} + +public OnPluginStart() +{ + CreatePopulateWeaponTrie(); + + g_cvar_wstats = CreateConVar("superlogs_wstats", "1", "Enable logging of weapon stats (default on)", 0, true, 0.0, true, 1.0); + g_cvar_actions = CreateConVar("superlogs_actions", "1", "Enable logging of actions, such as \"Round_Win\" (default on)", 0, true, 0.0, true, 1.0); + g_cvar_headshots = CreateConVar("superlogs_headshots", "1", "Enable logging of headshot player action (default on)", 0, true, 0.0, true, 1.0); + g_cvar_chat = CreateConVar("superlogs_chat", "1", "Enable logging of chat (default on)", 0, true, 0.0, true, 1.0); + g_cvar_captures = CreateConVar("superlogs_captures", "1", "Enable logging of capturing objectives (default on)", 0, true, 0.0, true, 1.0); + g_cvar_locations = CreateConVar("superlogs_locations", "1", "Enable logging of location on player death (default on)", 0, true, 0.0, true, 1.0); + g_cvar_ktraj = CreateConVar("superlogs_ktraj", "0", "Enable Psychostats \"KTRAJ\" logging (default off)", 0, true, 0.0, true, 1.0); + HookConVarChange(g_cvar_wstats, OnCvarWstatsChange); + HookConVarChange(g_cvar_actions, OnCvarActionsChange); + HookConVarChange(g_cvar_headshots, OnCvarHeadshotsChange); + HookConVarChange(g_cvar_chat, OnCvarChatChange); + HookConVarChange(g_cvar_captures, OnCvarCapturesChange); + HookConVarChange(g_cvar_locations, OnCvarLocationsChange); + HookConVarChange(g_cvar_ktraj, OnCvarKtrajChange); + CreateConVar("superlogs_ins_version", VERSION, NAME, FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY); + + hook_wstats(); + HookUserMessage(GetUserMessageId("ObjMsg"), objmsg); + HookEvent("player_hurt", Event_PlayerHurt); + HookEvent("round_end", Event_RoundEnd); + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + HookEvent("player_death", Event_PlayerDeath); + + RegConsoleCmd("say2", Command_Chat); + + CreateTimer(1.0, LogMap); + + GetTeams(true); +} + + +public OnMapStart() +{ + GetTeams(true); +} + + +hook_wstats() +{ + HookEvent("player_spawn", Event_PlayerSpawn); + HookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre); +} + +unhook_wstats() +{ + UnhookEvent("player_spawn", Event_PlayerSpawn); + UnhookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre); +} + +public OnClientPutInServer(client) +{ + reset_player_stats(client); +} + +public Event_PlayerHurt(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID on server + // "attacker" "short" // CLIENT INDEX! on server of the attacker + // "dmg_health" "short" // lost health points + // "hitgroup" "short" // Hit groups + // "weapon" "string" // Weapon name, like WEAPON_AK47 + + new attacker = GetEventInt(event, "attacker"); + new victim = GetEventInt(event, "userid"); + + if (attacker > 0 && attacker != victim) + { + // ... wtf insurgency... userid is userid and attacker is client index? + //attacker = GetClientOfUserId(attacker); + + new hitgroup = GetEventInt(event, "hitgroup"); + if (hitgroup < 8) + { + hitgroup += LOG_HIT_OFFSET; + } + + if (g_logwstats) + { + decl String:weapon[MAX_WEAPON_LEN+PREFIX_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + + new weapon_index = get_weapon_index(weapon[PREFIX_LEN]); + + if (weapon_index > -1) { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HITS]++; + g_weapon_stats[attacker][weapon_index][LOG_HIT_DAMAGE] += GetEventInt(event, "dmg_health"); + g_weapon_stats[attacker][weapon_index][hitgroup]++; + + if (hitgroup == (HITGROUP_HEAD+LOG_HIT_OFFSET)) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HEADSHOTS]++; + } + g_client_last_weapon[attacker] = weapon_index; + g_client_last_weaponstring[attacker] = weapon; + } + } + + if (g_logheadshots && hitgroup == (HITGROUP_HEAD+LOG_HIT_OFFSET)) + { + LogPlayerEvent(attacker, "triggered", "headshot"); + } + } +} + +public Action:Event_PlayerDeathPre(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogKillLoc(GetClientOfUserId(GetEventInt(event, "attacker")), GetClientOfUserId(GetEventInt(event, "userid"))); + + return Plugin_Continue; +} + +public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID who died + // "attacker" "short" // user ID who killed + // "type" "byte" // type of death + // "nodeath" "bool" // true if death messages were off when player died + + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + if (attacker == 0 || victim == 0 || attacker == victim) + { + return; + } + + if (g_logwstats) + { + new weapon_index = g_client_last_weapon[attacker]; + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_KILLS]++; + g_weapon_stats[victim][weapon_index][LOG_HIT_DEATHS]++; + if (GetClientTeam(attacker) == GetClientTeam(victim)) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_TEAMKILLS]++; + } + dump_player_stats(victim); + } + } + + if (g_logktraj) + { + LogPSKillTraj(attacker, victim, g_client_last_weaponstring[attacker]); + } +} + +public Event_PlayerSpawn(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID on server + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + reset_player_stats(client); + } +} + +public Event_RoundEnd(Handle:event, const String:name[], bool:dontBroadcast) +{ + if (g_logwstats) + { + WstatsDumpAll(); + } + + if (g_logactions) + { + LogTeamEvent(GetEventInt(event, "winner"), "triggered", "Round_Win"); + } +} + +public Action:Event_PlayerDisconnect(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + OnPlayerDisconnect(client); + return Plugin_Continue; +} + + +public Action:objmsg(UserMsg:msg_id, Handle:bf, const players[], playersNum, bool:reliable, bool:init) +{ + new point = BfReadByte(bf); // Objective Point: 1 = point A, 2 = point B, 3 = point C, etc. + new capstatus = BfReadByte(bf); // Capture Status: 1 on starting capture, 2 on finished capture + new team = BfReadByte(bf); // Team Index: 1 = Marines, 2 = Insurgents + + if (capstatus == 2) + { + switch (point) + { + case 1: + LogTeamEvent(team, "triggered", "captured_a"); + case 2: + LogTeamEvent(team, "triggered", "captured_b"); + case 3: + LogTeamEvent(team, "triggered", "captured_c"); + case 4: + LogTeamEvent(team, "triggered", "captured_d"); + case 5: + LogTeamEvent(team, "triggered", "captured_e"); + } + } + + return Plugin_Continue; +} + + +public Action:Command_Chat(client, args) +{ + // method partially taken from "Insurgency Chat" by "Stevo.TVR" + + if (g_logchat) + { + new String:message[192]; + new startidx = 4; + + if (GetCmdArgString(message, sizeof(message)) < 1 || client == 0) + { + return Plugin_Continue; + } + + new lastchar = strlen(message) - 1; + if (message[lastchar] == '"') + { + message[lastchar] = '\0'; + startidx += 1; + } + + if (message[0] == '1') + { + LogPlayerEvent(client, "say", message[startidx], false); + } + else + { + LogPlayerEvent(client, "say_team", message[startidx], false); + } + } + return Plugin_Continue; +} + + +public Action:LogMap(Handle:timer) +{ + // Called 1 second after OnPluginStart since srcds does not log the first map loaded. Idea from Stormtrooper's "mapfix.sp" for psychostats + LogMapLoad(); +} + + +public OnCvarWstatsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logwstats; + g_logwstats = GetConVarBool(g_cvar_wstats); + + if (old_value != g_logwstats) + { + if (g_logwstats) + { + hook_wstats(); + if (!g_logheadshots) + { + HookEvent("player_hurt", Event_PlayerHurt); + } + if (!g_logactions) + { + HookEvent("round_end", Event_RoundEnd); + } + if (!g_logktraj) + { + HookEvent("player_death", Event_PlayerDeath); + } + } + else + { + unhook_wstats(); + if (!g_logheadshots) + { + UnhookEvent("player_hurt", Event_PlayerHurt); + } + if (!g_logactions) + { + UnhookEvent("round_end", Event_RoundEnd); + } + if (!g_logktraj) + { + UnhookEvent("player_death", Event_PlayerDeath); + } + } + } +} + + +public OnCvarActionsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logactions; + g_logactions = GetConVarBool(g_cvar_actions); + + if (old_value != g_logactions) + { + if (g_logactions && !g_logwstats) + { + HookEvent("round_end", Event_RoundEnd); + } + else if (!g_logwstats) + { + UnhookEvent("round_end", Event_RoundEnd); + } + } +} + + +public OnCvarHeadshotsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logheadshots; + g_logheadshots = GetConVarBool(g_cvar_headshots); + + if (old_value != g_logheadshots) + { + if (g_logheadshots && !g_logwstats) + { + HookEvent("player_hurt", Event_PlayerHurt); + } + else if (!g_logwstats) + { + UnhookEvent("player_hurt", Event_PlayerHurt); + } + } +} + + +public OnCvarCapturesChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logcaptures; + g_logcaptures = GetConVarBool(g_cvar_captures); + + if (old_value != g_logcaptures) + { + if (g_logcaptures) + { + HookUserMessage(GetUserMessageId("ObjMsg"), objmsg); + } + else + { + UnhookUserMessage(GetUserMessageId("ObjMsg"), objmsg); + } + } +} + + +public OnCvarChatChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + g_logchat = GetConVarBool(g_cvar_chat); +} + +public OnCvarLocationsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_loglocations; + g_loglocations = GetConVarBool(g_cvar_locations); + + if (old_value != g_loglocations) + { + if (g_loglocations) + { + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + else + { + UnhookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + } +} + +public OnCvarKtrajChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logktraj; + g_logktraj = GetConVarBool(g_cvar_ktraj); + + if (old_value != g_logktraj) + { + if (g_logktraj && !g_logwstats) + { + HookEvent("player_death", Event_PlayerDeath); + } + else if (!g_logwstats) + { + UnhookEvent("player_death", Event_PlayerDeath); + } + } +} \ No newline at end of file diff --git a/sourcemod/scripting/superlogs-l4d.sp b/sourcemod/scripting/superlogs-l4d.sp new file mode 100644 index 0000000..05518bf --- /dev/null +++ b/sourcemod/scripting/superlogs-l4d.sp @@ -0,0 +1,686 @@ +/** + * HLstatsX Community Edition - SourceMod plugin to generate advanced weapon logging + * http://www.hlxcommunity.com + * Copyright (C) 2009 Nicholas Hastings (psychonic) + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma semicolon 1 + +#include +#include + +#define NAME "SuperLogs: L4D" +#define VERSION "1.3.3" + +#define MAX_LOG_WEAPONS 27 +#define MAX_WEAPON_LEN 16 + + +new g_weapon_stats[MAXPLAYERS+1][MAX_LOG_WEAPONS][15]; +new const String:g_weapon_list[MAX_LOG_WEAPONS][MAX_WEAPON_LEN] = { + "autoshotgun", + "rifle", + "pumpshotgun", + "smg", + "dual_pistols", + "pipe_bomb", + "hunting_rifle", + "pistol", + "prop_minigun", + "tank_claw", + "hunter_claw", + "smoker_claw", + "boomer_claw", + "smg_silenced", //l4d2 start 14 [13] + "pistol_magnum", + "rifle_ak47", + "rifle_desert", + "shotgun_chrome", + "shotgun_spas", + "sniper_military", + "rifle_sg552", + "smg_mp5", + "sniper_awp", + "sniper_scout", + "jockey_claw", + "splitter_claw", + "charger_claw" + }; + +new Handle:g_cvar_wstats = INVALID_HANDLE; +new Handle:g_cvar_actions = INVALID_HANDLE; +new Handle:g_cvar_headshots = INVALID_HANDLE; +new Handle:g_cvar_meleeoverride = INVALID_HANDLE; + +new bool:g_logwstats = true; +new bool:g_logactions = true; +new bool:g_logheadshots = false; +new bool:g_logmeleeoverride = true; + +new g_iActiveWeaponOffset; + +new bool:g_bIsL4D2; + +#include +#include + + +public Plugin:myinfo = { + name = NAME, + author = "psychonic", + description = "Advanced logging for Left 4 Dead. Generates auxilary logging for use with log parsers such as HLstatsX and Psychostats", + version = VERSION, + url = "http://www.hlxcommunity.com" +}; + +#if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 +public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) +#else +public bool:AskPluginLoad(Handle:myself, bool:late, String:error[], err_max) +#endif +{ + new String:szGameDesc[64]; + GetGameDescription(szGameDesc, sizeof(szGameDesc), true); + if (strncmp(szGameDesc, "L4D", 3, false) != 0 && StrContains(szGameDesc, "Left 4 D", false) == -1) + { + new String:szGameDir[64]; + GetGameFolderName(szGameDir, sizeof(szGameDir)); + if (StrContains(szGameDir, "left4dead", false) == -1) + { + strcopy(error, err_max, "This plugin is only supported on L4D & L4D2"); + #if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 + return APLRes_Failure; + #else + return false; + #endif + } + } +#if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 + return APLRes_Success; +#else + return true; +#endif +} + +public OnPluginStart() +{ + CreatePopulateWeaponTrie(); + + g_cvar_wstats = CreateConVar("superlogs_wstats", "1", "Enable logging of weapon stats (default on)", 0, true, 0.0, true, 1.0); + g_cvar_actions = CreateConVar("superlogs_actions", "1", "Enable logging of player actions, such as \"Got_The_Bomb\" (default on)", 0, true, 0.0, true, 1.0); + g_cvar_headshots = CreateConVar("superlogs_headshots", "0", "Enable logging of headshot player action (default off)", 0, true, 0.0, true, 1.0); + g_cvar_meleeoverride = CreateConVar("superlogs_meleeoverride", "1", "Enable changing \"melee\" weapon in server logs to specific weapon (L4D2-only) (default on)", 0, true, 0.0, true, 1.0); + HookConVarChange(g_cvar_wstats, OnCvarWstatsChange); + HookConVarChange(g_cvar_actions, OnCvarActionsChange); + HookConVarChange(g_cvar_headshots, OnCvarHeadshotsChange); + HookConVarChange(g_cvar_meleeoverride, OnCvarMeleeOverrideChange); + CreateConVar("superlogs_l4d_version", VERSION, NAME, FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY); + + if (GuessSDKVersion() != SOURCE_SDK_LEFT4DEAD) + { + g_bIsL4D2 = true; + } + + hook_actions(); + hook_wstats(); + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + CreateTimer(120.0, FlushWeaponLogs, 0, TIMER_REPEAT); + + CreateTimer(1.0, LogMap); + + GetTeams(); + + g_iActiveWeaponOffset = FindSendPropInfo("CTerrorPlayer", "m_hActiveWeapon"); +} + + +public OnMapStart() +{ + GetTeams(); +} + + +hook_actions() +{ + HookEvent("survivor_rescued", Event_RescueSurvivor); + HookEvent("heal_success", Event_Heal); + HookEvent("revive_success", Event_Revive); + HookEvent("witch_harasser_set", Event_StartleWitch); + HookEvent("lunge_pounce", Event_Pounce); + HookEvent("player_now_it", Event_Boomered); + HookEvent("friendly_fire", Event_FF); + HookEvent("witch_killed", Event_WitchKilled); + HookEvent("award_earned", Event_Award); + if (g_bIsL4D2) + { + HookEvent("defibrillator_used", Event_Defib); + HookEvent("adrenaline_used", Event_Adrenaline); + HookEvent("jockey_ride", Event_JockeyRide); + HookEvent("charger_pummel_start", Event_ChargerPummelStart); + HookEvent("vomit_bomb_tank", Event_VomitBombTank); + HookEvent("scavenge_match_finished", Event_ScavengeEnd); + HookEvent("versus_match_finished", Event_VersusEnd); + } +} + +unhook_actions() +{ + UnhookEvent("survivor_rescued", Event_RescueSurvivor); + UnhookEvent("heal_success", Event_Heal); + UnhookEvent("revive_success", Event_Revive); + UnhookEvent("witch_harasser_set", Event_StartleWitch); + UnhookEvent("lunge_pounce", Event_Pounce); + UnhookEvent("player_now_it", Event_Boomered); + UnhookEvent("friendly_fire", Event_FF); + UnhookEvent("witch_killed", Event_WitchKilled); + UnhookEvent("award_earned", Event_Award); + + if (g_bIsL4D2) + { + UnhookEvent("defibrillator_used", Event_Defib); + UnhookEvent("adrenaline_used", Event_Adrenaline); + UnhookEvent("jockey_ride", Event_JockeyRide); + UnhookEvent("charger_pummel_start", Event_ChargerPummelStart); + UnhookEvent("vomit_bomb_tank", Event_VomitBombTank); + UnhookEvent("scavenge_match_finished", Event_ScavengeEnd); + UnhookEvent("versus_match_finished", Event_VersusEnd); + } +} + +hook_wstats() +{ + HookEvent("weapon_fire", Event_PlayerShoot); + HookEvent("weapon_fire_on_empty", Event_PlayerShoot); + HookEvent("player_hurt", Event_PlayerHurt); + HookEvent("infected_hurt", Event_InfectedHurt); + HookEvent("player_death", Event_PlayerDeath); + HookEvent("player_spawn", Event_PlayerSpawn); + HookEvent("round_end_message", Event_RoundEnd, EventHookMode_PostNoCopy); + HookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre); +} + +unhook_wstats() +{ + UnhookEvent("weapon_fire", Event_PlayerShoot); + UnhookEvent("weapon_fire_on_empty", Event_PlayerShoot); + UnhookEvent("player_hurt", Event_PlayerHurt); + UnhookEvent("infected_hurt", Event_InfectedHurt); + UnhookEvent("player_death", Event_PlayerDeath); + UnhookEvent("player_spawn", Event_PlayerSpawn); + UnhookEvent("round_end_message", Event_RoundEnd, EventHookMode_PostNoCopy); + UnhookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre); +} + +public OnClientPutInServer(client) +{ + reset_player_stats(client); +} + + +public Action:FlushWeaponLogs(Handle:timer, any:index) +{ + WstatsDumpAll(); +} + +public Event_PlayerShoot(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "local" "1" // don't network this, its way too spammy + // "userid" "short" + // "weapon" "string" // used weapon name + // "weaponid" "short" // used weapon ID + // "count" "short" // number of bullets + + new attacker = GetClientOfUserId(GetEventInt(event, "userid")); + if (attacker > 0) + { + decl String: weapon[MAX_WEAPON_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + new weapon_index = get_weapon_index(weapon); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_SHOTS]++; + } + } +} + +public Event_PlayerHurt(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "local" "1" // Not networked + // "userid" "short" // user ID who was hurt + // "attacker" "short" // user id who attacked + // "attackerentid" "long" // entity id who attacked, if attacker not a player, and userid therefore invalid + // "health" "short" // remaining health points + // "armor" "byte" // remaining armor points + // "weapon" "string" // weapon name attacker used, if not the world + // "dmg_health" "short" // damage done to health + // "dmg_armor" "byte" // damage done to armor + // "hitgroup" "byte" // hitgroup that was damaged + // "type" "long" // damage type + + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + + if (attacker > 0) + { + decl String: weapon[MAX_WEAPON_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + new weapon_index = get_weapon_index(weapon); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HITS]++; + g_weapon_stats[attacker][weapon_index][LOG_HIT_DAMAGE] += GetEventInt(event, "dmg_health"); + new hitgroup = GetEventInt(event, "hitgroup"); + if (hitgroup < 8) + { + g_weapon_stats[attacker][weapon_index][hitgroup + LOG_HIT_OFFSET]++; + } + } + else if (g_logactions && !strcmp(weapon, "insect_swarm")) + { + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + if (victim > 0 && IsClientInGame(victim) && GetClientTeam(victim) == 2 && !GetEntProp(victim, Prop_Send, "m_isIncapacitated")) + { + LogPlyrPlyrEvent(attacker, victim, "triggered", "spit_hurt", true); + } + } + } +} + +public Event_InfectedHurt(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "local" "1" // don't network this, its way too spammy + // "attacker" "short" // player userid who attacked + // "entityid" "long" // entity id of infected + // "hitgroup" "byte" // hitgroup that was damaged + // "amount" "short" // how much damage was done + // "type" "long" // damage type + + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + + if (attacker > 0) + { + decl String: weapon[MAX_WEAPON_LEN]; + GetClientWeapon(attacker, weapon, sizeof(weapon)); + + new weapon_index = get_weapon_index(weapon[7]); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HITS]++; + g_weapon_stats[attacker][weapon_index][LOG_HIT_DAMAGE] += GetEventInt(event, "amount"); + new hitgroup = GetEventInt(event, "hitgroup"); + if (hitgroup < 8) + { + g_weapon_stats[attacker][weapon_index][hitgroup + LOG_HIT_OFFSET]++; + } + } + } +} + +public Event_PlayerDeathPre(Handle:event, const String:name[], bool:dontBroadcast) +{ + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + + if (g_logheadshots && GetEventBool(event, "headshot")) + { + LogPlayerEvent(attacker, "triggered", "headshot"); + } + if (g_logmeleeoverride && g_bIsL4D2 && attacker > 0 && IsClientInGame(attacker)) + { + decl String:szWeapon[64]; + GetEventString(event, "weapon", szWeapon, sizeof(szWeapon)); + if (strncmp(szWeapon, "melee", 5) == 0) + { + new iWeapon = GetEntDataEnt2(attacker, g_iActiveWeaponOffset); + if (IsValidEdict(iWeapon)) + { + // They have time to switch weapons after the kill before the death event + GetEdictClassname(iWeapon, szWeapon, sizeof(szWeapon)); + if (strncmp(szWeapon[7], "melee", 5) == 0) + { + GetEntPropString(iWeapon, Prop_Data, "m_strMapSetScriptName", szWeapon, sizeof(szWeapon)); + SetEventString(event, "weapon", szWeapon); + } + } + } + } +} + +public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID who died + // "entityid" "long" // entity ID who died, userid should be used first, to get the dead Player. Otherwise, it is not a player, so use this. $ + // "attacker" "short" // user ID who killed + // "attackername" "string" // What type of zombie, so we don't have zombie names + // "attackerentid" "long" // if killer not a player, the entindex of who killed. Again, use attacker first + // "weapon" "string" // weapon name killer used + // "headshot" "bool" // signals a headshot + // "attackerisbot" "bool" // is the attacker a bot + // "victimname" "string" // What type of zombie, so we don't have zombie names + // "victimisbot" "bool" // is the victim a bot + // "abort" "bool" // did the victim abort + // "type" "long" // damage type + // "victim_x" "float" + // "victim_y" "float" + // "victim_z" "float" + + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + + if (g_logwstats && victim > 0 && attacker > 0) + { + decl String: weapon[MAX_WEAPON_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + + new weapon_index = get_weapon_index(weapon); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_KILLS]++; + if (GetEventBool(event, "headshot")) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HEADSHOTS]++; + } + g_weapon_stats[victim][weapon_index][LOG_HIT_DEATHS]++; + if (GetClientTeam(attacker) == GetClientTeam(victim)) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_TEAMKILLS]++; + } + dump_player_stats(victim); + } + } +} + +public Event_RoundEnd(Handle:event, const String:name[], bool:dontBroadcast) +{ + WstatsDumpAll(); +} + +public Event_PlayerSpawn(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID on server + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + reset_player_stats(client); + } +} + +public Action:Event_PlayerDisconnect(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + OnPlayerDisconnect(client); + return Plugin_Continue; +} + +public Action:LogMap(Handle:timer) +{ + // Called 1 second after OnPluginStart since srcds does not log the first map loaded. Idea from Stormtrooper's "mapfix.sp" for psychostats + LogMapLoad(); + + return Plugin_Continue; +} + +public Event_RescueSurvivor(Handle:event, const String:name[], bool:dontBroadcast) +{ + new player = GetClientOfUserId(GetEventInt(event, "rescuer")); + + if (player > 0) + { + LogPlayerEvent(player, "triggered", "rescued_survivor", true); + } +} + +public Event_Heal(Handle:event, const String:name[], bool:dontBroadcast) +{ + new player = GetClientOfUserId(GetEventInt(event, "userid")); + + if (player > 0 && player != GetClientOfUserId(GetEventInt(event, "subject"))) + { + LogPlayerEvent(player, "triggered", "healed_teammate", true); + } +} + +public Event_Revive(Handle:event, const String:name[], bool:dontBroadcast) +{ + new player = GetClientOfUserId(GetEventInt(event, "userid")); + + if (player > 0) + { + LogPlayerEvent(player, "triggered", "revived_teammate", true); + } +} + +public Event_StartleWitch(Handle:event, const String:name[], bool:dontBroadcast) +{ + new player = GetClientOfUserId(GetEventInt(event, "userid")); + + if (player > 0 && (!g_bIsL4D2 || GetEventBool(event, "first"))) + { + LogPlayerEvent(player, "triggered", "startled_witch", true); + } +} + +public Event_Pounce(Handle:event, const String:name[], bool:dontBroadcast) +{ + new player = GetClientOfUserId(GetEventInt(event, "userid")); + new victim = GetClientOfUserId(GetEventInt(event, "victim")); + + if (victim > 0) + { + LogPlyrPlyrEvent(player, victim, "triggered", "pounce", true); + } + else + { + LogPlayerEvent(player, "triggered", "pounce", true); + } +} + +public Event_Boomered(Handle:event, const String:name[], bool:dontBroadcast) +{ + new player = GetClientOfUserId(GetEventInt(event, "attacker")); + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + + if (player > 0 && (!g_bIsL4D2 || GetEventBool(event, "by_boomer"))) + { + if (victim > 0) + { + LogPlyrPlyrEvent(player, victim, "triggered", "vomit", true); + } + else + { + LogPlayerEvent(player, "triggered", "vomit", true); + } + } +} + +public Event_FF(Handle:event, const String:name[], bool:dontBroadcast) +{ + new player = GetClientOfUserId(GetEventInt(event, "attacker")); + new victim = GetClientOfUserId(GetEventInt(event, "victim")); + + if (player > 0 && player == GetClientOfUserId(GetEventInt(event, "guilty"))) + { + if (victim > 0) + { + LogPlyrPlyrEvent(player, victim, "triggered", "friendly_fire", true); + } + else + { + LogPlayerEvent(player, "triggered", "friendly_fire", true); + } + } +} + +public Event_WitchKilled(Handle:event, const String:name[], bool:dontBroadcast) +{ + if (GetEventBool(event, "oneshot")) + { + LogPlayerEvent(GetClientOfUserId(GetEventInt(event, "userid")), "triggered", "cr0wned", true); + } +} + +public Event_Defib(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogPlayerEvent(GetClientOfUserId(GetEventInt(event, "userid")), "triggered", "defibrillated_teammate", true); +} + +public Event_Adrenaline(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogPlayerEvent(GetClientOfUserId(GetEventInt(event, "userid")), "triggered", "used_adrenaline", true); +} + +public Event_JockeyRide(Handle:event, const String:name[], bool:dontBroadcast) +{ + new player = GetClientOfUserId(GetEventInt(event, "userid")); + new victim = GetClientOfUserId(GetEventInt(event, "victim")); + + if (player > 0) + { + if (victim > 0) + { + LogPlyrPlyrEvent(player, victim, "triggered", "jockey_ride", true); + } + else + { + LogPlayerEvent(player, "triggered", "jockey_ride", true); + } + } +} + +public Event_ChargerPummelStart(Handle:event, const String:name[], bool:dontBroadcast) +{ + new player = GetClientOfUserId(GetEventInt(event, "userid")); + new victim = GetClientOfUserId(GetEventInt(event, "victim")); + + if (victim > 0) + { + LogPlyrPlyrEvent(player, victim, "triggered", "charger_pummel", true); + } + else + { + LogPlayerEvent(player, "triggered", "charger_pummel", true); + } +} + +public Event_VomitBombTank(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogPlayerEvent(GetClientOfUserId(GetEventInt(event, "userid")), "triggered", "bilebomb_tank", true); +} + +public Event_ScavengeEnd(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogTeamEvent(GetEventInt(event, "winners"), "triggered", "Scavenge_Win"); +} + +public Event_VersusEnd(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogTeamEvent(GetEventInt(event, "winners"), "triggered", "Versus_Win"); +} + +public Action:Event_Award(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // player who earned the award + // "entityid" "long" // client likes ent id + // "subjectentid" "long" // entity id of other party in the award, if any + // "award" "short" // id of award earned + + switch(GetEventInt(event, "award")) + { + case 21: + LogPlayerEvent(GetClientOfUserId(GetEventInt(event, "userid")), "triggered", "hunter_punter", true); + case 27: + LogPlayerEvent(GetClientOfUserId(GetEventInt(event, "userid")), "triggered", "tounge_twister", true); + case 67: + LogPlayerEvent(GetClientOfUserId(GetEventInt(event, "userid")), "triggered", "protect_teammate", true); + case 80: + LogPlayerEvent(GetClientOfUserId(GetEventInt(event, "userid")), "triggered", "no_death_on_tank", true); + case 136: + LogPlayerEvent(GetClientOfUserId(GetEventInt(event, "userid")), "triggered", "killed_all_survivors", true); + } +} + +public OnCvarWstatsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logwstats; + g_logwstats = GetConVarBool(g_cvar_wstats); + + if (old_value != g_logwstats) + { + if (g_logwstats) + { + hook_wstats(); + } + else + { + unhook_wstats(); + } + } +} + +public OnCvarActionsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logactions; + g_logactions = GetConVarBool(g_cvar_actions); + + if (old_value != g_logactions) + { + if (g_logactions) + { + hook_actions(); + } + else + { + unhook_actions(); + } + } +} + +public OnCvarHeadshotsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logheadshots; + g_logheadshots = GetConVarBool(g_cvar_headshots); + + if (old_value != g_logheadshots) + { + if (g_logheadshots && !g_logmeleeoverride) + { + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + else if (!g_logmeleeoverride) + { + UnhookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + } +} + +public OnCvarMeleeOverrideChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logmeleeoverride; + g_logmeleeoverride = GetConVarBool(g_cvar_meleeoverride); + + if (old_value != g_logmeleeoverride) + { + if (g_logmeleeoverride && !g_logheadshots) + { + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + else if (!g_logheadshots) + { + UnhookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + } +} diff --git a/sourcemod/scripting/superlogs-neotokyo.sp b/sourcemod/scripting/superlogs-neotokyo.sp new file mode 100644 index 0000000..96f75a5 --- /dev/null +++ b/sourcemod/scripting/superlogs-neotokyo.sp @@ -0,0 +1,122 @@ +/** + * HLstatsX Community Edition - SourceMod plugin to generate advanced weapon logging + * http://www.hlxcommunity.com + * Copyright (C) 2009 Nicholas Hastings (psychonic) + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma semicolon 1 + +#include +#include + +#define NAME "SuperLogs: NeoTokyo" +#define VERSION "1.0.2" + +new Handle:g_cvar_headshots = INVALID_HANDLE; +new Handle:g_cvar_locations = INVALID_HANDLE; + +new bool:g_logheadshots = true; +new bool:g_loglocations = true; + +#include + + +public Plugin:myinfo = { + name = NAME, + author = "psychonic", + description = "Advanced logging for NeoTokyo. Generates auxilary logging for use with log parsers such as HLstatsX and Psychostats", + version = VERSION, + url = "http://www.hlxcommunity.com" +}; + + +public OnPluginStart() +{ + g_cvar_headshots = CreateConVar("superlogs_headshots", "1", "Enable logging of headshot player action (default off)", 0, true, 0.0, true, 1.0); + g_cvar_locations = CreateConVar("superlogs_locations", "1", "Enable logging of location on player death (default on)", 0, true, 0.0, true, 1.0); + + HookConVarChange(g_cvar_headshots, OnCvarHeadshotsChange); + HookConVarChange(g_cvar_locations, OnCvarLocationsChange); + + CreateConVar("superlogs_nts_version", VERSION, NAME, FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY); + + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + + CreateTimer(1.0, LogMap); +} + + +public Action:Event_PlayerDeathPre(Handle:event, const String:name[], bool:dontBroadcast) +{ + new attacker = GetEventInt(event, "attacker"); + if (attacker > 0) + { + if (g_loglocations) + { + LogKillLoc(GetClientOfUserId(attacker), GetClientOfUserId(GetEventInt(event, "userid"))); + } + if (g_logheadshots && GetEventInt(event, "icon") == 2) + { + LogPlayerEvent(GetClientOfUserId(attacker), "triggered", "headshot"); + } + } + + return Plugin_Continue; +} + +public OnCvarHeadshotsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logheadshots; + g_logheadshots = GetConVarBool(g_cvar_headshots); + + if (old_value != g_logheadshots) + { + if (g_logheadshots && !g_loglocations) + { + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + else if (!g_loglocations) + { + UnhookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + } +} + +public OnCvarLocationsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_loglocations; + g_loglocations = GetConVarBool(g_cvar_locations); + + if (old_value != g_loglocations) + { + if (g_loglocations && !g_logheadshots) + { + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + else if (!g_logheadshots) + { + UnhookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + } +} + +public Action:LogMap(Handle:timer) +{ + // Called 1 second after OnPluginStart since srcds does not log the first map loaded. Idea from Stormtrooper's "mapfix.sp" for psychostats + LogMapLoad(); +} diff --git a/sourcemod/scripting/superlogs-nucleardawn.sp b/sourcemod/scripting/superlogs-nucleardawn.sp new file mode 100644 index 0000000..daf1b78 --- /dev/null +++ b/sourcemod/scripting/superlogs-nucleardawn.sp @@ -0,0 +1,445 @@ +/** + * HLstatsX Community Edition - SourceMod plugin to generate advanced weapon logging + * http://www.hlxcommunity.com + * Copyright (C) 2009 Nicholas Hastings (psychonic) + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma semicolon 1 + +#include +#include +#include + +#define NAME "SuperLogs: Nuclear Dawn" +#define VERSION "1.0" + + +#define MAX_LOG_WEAPONS 28 +#define IGNORE_SHOTS_START 16 +#define MAX_WEAPON_LEN 20 + +#define BUNKER_DAMAGE_TIMES 10 + +#define ND_TEAM_EMP 3 +#define ND_TEAM_CT 2 + +new g_weapon_stats[MAXPLAYERS+1][MAX_LOG_WEAPONS][15]; +new const String:g_weapon_list[MAX_LOG_WEAPONS][MAX_WEAPON_LEN] = { + "avenger", + "bag90", + "chaingun", + "daisy cutter", + "f2000", + "grenade launcher", + "m95", + "mp500", + "mp7", + "nx300", + "p900", + "paladin", + "pp22", + "psg", + "shotgun", + "sp5", + "x01", + "medpack", + "armblade", + "mine", + "emp grenade", + "p12 grenade", + "remote grenade", + "repair tool", + "svr grenade", + "u23 grenade", + "armknives", + "frag grenade" + }; + +#include +#include + +new g_bReadyToShoot[MAXPLAYERS+1] = {false,...}; +new g_iBunkerAttacked[2] = {0,...}; + +public Plugin:myinfo = { + name = NAME, + author = "Peace-Maker", + description = "Advanced logging. Generates auxilary logging for use with log parsers such as HLstatsX and Psychostats", + version = VERSION, + url = "http://www.hlxcommunity.com" +}; + + +public OnPluginStart() +{ + CreatePopulateWeaponTrie(); + CreateConVar("superlogs_nucleardawn_version", VERSION, NAME, FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY); + + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + HookEvent("promoted_to_commander", Event_PromotedToCommander); + HookEvent("resource_captured", Event_ResourceCaptured); + HookEvent("structure_damage_sparse", Event_StructureDamageSparse); + HookEvent("structure_death", Event_StructureDeath); + + // wstats + HookEvent("player_spawn", Event_PlayerSpawn); + HookEvent("round_win", Event_RoundWin); + HookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre); + + CreateTimer(1.0, LogMap); + + GetTeams(); + // GetTeamName gets #ND_Consortium and #ND_Empire in release version -.-. Game logs with CONSORTIUM and EMPIRE translated + strcopy(g_team_list[ND_TEAM_CT], sizeof(g_team_list[]), "CONSORTIUM"); + strcopy(g_team_list[ND_TEAM_EMP], sizeof(g_team_list[]), "EMPIRE"); + + for(new i=1;i<=MaxClients;i++) + { + if(IsClientInGame(i)) + OnClientPutInServer(i); + } +} + +public OnMapStart() +{ + GetTeams(); + // GetTeamName gets #ND_Consortium and #ND_Empire in release version -.-. Game logs with CONSORTIUM and EMPIRE translated + strcopy(g_team_list[ND_TEAM_CT], sizeof(g_team_list[]), "CONSORTIUM"); + strcopy(g_team_list[ND_TEAM_EMP], sizeof(g_team_list[]), "EMPIRE"); + + g_iBunkerAttacked[0] = 0; + g_iBunkerAttacked[1] = 0; +} + +public OnClientPutInServer(client) +{ + g_bReadyToShoot[client] = false; + SDKHook(client, SDKHook_TraceAttackPost, Hook_TraceAttackPost); + SDKHook(client, SDKHook_PostThink, Hook_PostThink); + SDKHook(client, SDKHook_PostThinkPost, Hook_PostThinkPost); + reset_player_stats(client); +} + +public Action:Event_PlayerDeathPre(Handle:event, const String:name[], bool:dontBroadcast) +{ + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + + decl String:weapon[MAX_WEAPON_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + + if (attacker <= 0 || victim <= 0) + { + return Plugin_Continue; + } + + // Which commander ablilty?! + if(StrEqual(weapon, "commander ability")) + { + new damagebits = GetEventInt(event, "damagebits"); + if(damagebits & DMG_ENERGYBEAM) + { + Format(weapon, sizeof(weapon), "commander poison"); + SetEventString(event, "weapon", weapon); + } + else if(damagebits & DMG_BLAST) + { + Format(weapon, sizeof(weapon), "commander damage"); + SetEventString(event, "weapon", weapon); + } + } + + if(attacker != victim) + { + // Check if victim was commander? + if(GameRules_GetPropEnt("m_hCommanders", ND_TEAM_CT-2) == victim || GameRules_GetPropEnt("m_hCommanders", ND_TEAM_EMP-2) == victim) + LogPlayerEvent(attacker, "triggered", "killed_commander"); + } + + new weapon_index = get_weapon_index(weapon); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_KILLS]++; + g_weapon_stats[victim][weapon_index][LOG_HIT_DEATHS]++; + if (GetClientTeam(attacker) == GetClientTeam(victim)) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_TEAMKILLS]++; + } + dump_player_stats(victim); + } + + return Plugin_Continue; +} + +public Hook_PostThink(client) +{ + if(!IsPlayerAlive(client)) + return; + + new iWeapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon"); + if(iWeapon == -1 || !IsValidEdict(iWeapon)) + { + g_bReadyToShoot[client] = false; + return; + } + + decl String:sWeapon[32]; + GetEdictClassname(iWeapon, sWeapon, sizeof(sWeapon)); + if(StrContains(sWeapon, "weapon_", false) != 0) + return; + + new Float:flNextAttackTime = GetEntPropFloat(iWeapon, Prop_Send, "m_flNextPrimaryAttack"); + if(flNextAttackTime <= GetGameTime() && GetEntProp(iWeapon, Prop_Send, "m_iClip1") > 0) + g_bReadyToShoot[client] = true; + else + g_bReadyToShoot[client] = false; +} + +public Hook_PostThinkPost(client) +{ + if(!IsPlayerAlive(client)) + return; + + new iWeapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon"); + if(iWeapon == -1 || !IsValidEdict(iWeapon)) + return; + + decl String:sWeapon[30]; + GetEdictClassname(iWeapon, sWeapon, sizeof(sWeapon)); + if(StrContains(sWeapon, "weapon_", false) != 0) + return; + + ReplaceString(sWeapon, sizeof(sWeapon), "weapon_", "", false); + FixWeaponLoggingName(sWeapon, sizeof(sWeapon)); + + new Float:flNextAttackTime = GetEntPropFloat(iWeapon, Prop_Send, "m_flNextPrimaryAttack"); + if(g_bReadyToShoot[client] && flNextAttackTime > GetGameTime()) + { + new weapon_index = get_weapon_index(sWeapon); + if (weapon_index > -1 && weapon_index < IGNORE_SHOTS_START) + { + g_weapon_stats[client][weapon_index][LOG_HIT_SHOTS]++; + } + g_bReadyToShoot[client] = false; + } +} + +public Hook_TraceAttackPost(victim, attacker, inflictor, Float:damage, damagetype, ammotype, hitbox, hitgroup) +{ + if(IsClientInGame(victim)) + { + if(1 <= attacker <= MaxClients && IsClientInGame(attacker)) + { + new iWeapon = GetEntPropEnt(attacker, Prop_Send, "m_hActiveWeapon"); + new String:sWeapon[64]; + if(iWeapon > 0) + GetEdictClassname(iWeapon, sWeapon, sizeof(sWeapon)); + + ReplaceString(sWeapon, sizeof(sWeapon), "weapon_", "", false); + FixWeaponLoggingName(sWeapon, sizeof(sWeapon)); + + new weapon_index = get_weapon_index(sWeapon); + + // player_death + if((GetClientHealth(victim) - RoundToCeil(damage)) < 0) + { + if (hitgroup == HITGROUP_HEAD) + { + LogPlayerEvent(attacker, "triggered", "headshot"); + if (weapon_index > -1) + g_weapon_stats[attacker][weapon_index][LOG_HIT_HEADSHOTS]++; + } + } + // player_hurt + else + { + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HITS]++; + g_weapon_stats[attacker][weapon_index][LOG_HIT_DAMAGE] += RoundToCeil(damage); + if (hitgroup < 8) + { + g_weapon_stats[attacker][weapon_index][hitgroup + LOG_HIT_OFFSET]++; + } + } + } + } + } +} + +public Event_PlayerSpawn(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID on server + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + reset_player_stats(client); + } +} + +public Action:Event_PlayerDisconnect(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + OnPlayerDisconnect(client); + return Plugin_Continue; +} + +public Event_RoundWin(Handle:event, const String:name[], bool:dontBroadcast) +{ + new team = GetEventInt(event, "team"); + if(team >= 2) + { + LogTeamEvent(team, "triggered", "round_win"); + LogTeamEvent(GetOtherTeam(team), "triggered", "round_lose"); + } + + g_iBunkerAttacked[0] = 0; + g_iBunkerAttacked[1] = 0; + WstatsDumpAll(); +} + +public Event_PromotedToCommander(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogPlayerEvent(GetClientOfUserId(GetEventInt(event, "userid")), "triggered", "promoted_to_commander"); +} + +public Event_ResourceCaptured(Handle:event, const String:name[], bool:dontBroadcast) +{ + new team = GetEventInt(event, "team"); + if(team >= 2) + LogTeamEvent(team, "triggered", "resource_captured"); +} + +public Event_StructureDamageSparse(Handle:event, const String:name[], bool:dontBroadcast) +{ + if(!GetEventBool(event, "bunker")) + return; + new team = GetEventInt(event, "ownerteam"); + if(team >= 2) + { + g_iBunkerAttacked[team-2]++; + + if(g_iBunkerAttacked[team-2] == BUNKER_DAMAGE_TIMES) + { + LogTeamEvent(GetOtherTeam(team), "triggered", "damaged_opposite_bunker"); + g_iBunkerAttacked[team-2] = 0; + } + } +} + +public Event_StructureDeath(Handle:event, const String:name[], bool:dontBroadcast) +{ + new iEnt = GetEventInt(event, "entindex"); + new iAttacker = GetClientOfUserId(GetEventInt(event, "attacker")); + if(iAttacker > 0 && iAttacker <= MaxClients && iEnt != -1 && IsValidEntity(iEnt)) + { + decl String:sBuffer[32]; + GetEdictClassname(iEnt, sBuffer, sizeof(sBuffer)); + PrintToChatAll("%N destroyed %s", iAttacker, sBuffer); + + if(StrEqual(sBuffer, "struct_armoury")) + { + LogPlayerEvent(iAttacker, "triggered", "armoury_destroyed"); + } + else if(StrEqual(sBuffer, "struct_artillery_explosion")) + { + LogPlayerEvent(iAttacker, "triggered", "artillery_destroyed"); + } + else if(StrEqual(sBuffer, "struct_assembler")) + { + LogPlayerEvent(iAttacker, "triggered", "assembler_destroyed"); + } + else if(StrEqual(sBuffer, "struct_flamethrower_turret")) + { + LogPlayerEvent(iAttacker, "triggered", "flamethrowerturret_destroyed"); + } + else if(StrEqual(sBuffer, "struct_fusion_reactor")) + { + LogPlayerEvent(iAttacker, "triggered", "wirelessrepeater_destroyed"); + } + else if(StrEqual(sBuffer, "struct_power_station")) + { + LogPlayerEvent(iAttacker, "triggered", "powerstation_destroyed"); + } + else if(StrEqual(sBuffer, "struct_radar")) + { + LogPlayerEvent(iAttacker, "triggered", "radar_destroyed"); + } + else if(StrEqual(sBuffer, "struct_power_relay")) + { + LogPlayerEvent(iAttacker, "triggered", "powerrelay_destroyed"); + } + else if(StrEqual(sBuffer, "struct_rocket_turret")) + { + LogPlayerEvent(iAttacker, "triggered", "rocketturret_destroyed"); + } + else if(StrEqual(sBuffer, "struct_sonic_turret")) + { + LogPlayerEvent(iAttacker, "triggered", "sonicturret_destroyed"); + } + else if(StrEqual(sBuffer, "struct_support_station")) + { + LogPlayerEvent(iAttacker, "triggered", "supply_destroyed"); + } + else if(StrEqual(sBuffer, "struct_transport_gate")) + { + LogPlayerEvent(iAttacker, "triggered", "transportgate_destroyed"); + } + else if(StrEqual(sBuffer, "struct_machinegun_turret")) + { + LogPlayerEvent(iAttacker, "triggered", "machineguneturret_destroyed"); + } + } +} + +public Action:LogMap(Handle:timer) +{ + // Called 1 second after OnPluginStart since srcds does not log the first map loaded. Idea from Stormtrooper's "mapfix.sp" for psychostats + LogMapLoad(); +} + +stock GetOtherTeam(team) +{ + if(team == 2) + return 3; + else if(team == 3) + return 2; + + return team; +} + +stock FixWeaponLoggingName(String:sWeapon[], maxlength) +{ + if(StrEqual(sWeapon, "daisycutter")) + strcopy(sWeapon, maxlength, "daisy cutter"); + else if(StrEqual(sWeapon, "emp_grenade")) + strcopy(sWeapon, maxlength, "emp grenade"); + else if(StrEqual(sWeapon, "frag_grenade")) + strcopy(sWeapon, maxlength, "frag grenade"); + else if(StrEqual(sWeapon, "grenade_launcher")) + strcopy(sWeapon, maxlength, "grenade launcher"); + else if(StrEqual(sWeapon, "p12_grenade")) + strcopy(sWeapon, maxlength, "p12 grenade"); + else if(StrEqual(sWeapon, "remote_grenade")) + strcopy(sWeapon, maxlength, "remote grenade"); + //else if(StrEqual(sWeapon, "repair_tool")) + // strcopy(sWeapon, maxlength, "repair tool"); + else if(StrEqual(sWeapon, "u23_grenade")) + strcopy(sWeapon, maxlength, "u23 grenade"); +} \ No newline at end of file diff --git a/sourcemod/scripting/superlogs-pvkii.sp b/sourcemod/scripting/superlogs-pvkii.sp new file mode 100644 index 0000000..872ab4a --- /dev/null +++ b/sourcemod/scripting/superlogs-pvkii.sp @@ -0,0 +1,326 @@ +/** + * HLstatsX Community Edition - SourceMod plugin to generate advanced weapon logging + * http://www.hlxcommunity.com + * Copyright (C) 2009-2010 Nicholas Hastings (psychonic) + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma semicolon 1 + +#include +#include + +#define NAME "SuperLogs: PVKII" +#define VERSION "1.0.1" + +#define MAX_LOG_WEAPONS 6 +#define MAX_WEAPON_LEN 14 + + +new g_weapon_stats[MAXPLAYERS+1][MAX_LOG_WEAPONS][15]; +new const String:g_weapon_list[MAX_LOG_WEAPONS][MAX_WEAPON_LEN] = { + "blunderbuss", + "flintlock", + "arrow", + "crossbow_bolt", + "throwaxe", + "javalin" + }; + +new Handle:g_cvar_ktraj = INVALID_HANDLE; + +new bool:g_logktraj = true; + +new bool:g_bHasChest[MAXPLAYERS+1] = {false,...}; +new bool:g_bHasGrail[MAXPLAYERS+1] = {false,...}; + +new g_iLastClass = -1; + +new const String:g_szClassNames[][] = { + "Skirmisher", + "Captain", + "", + "Berserker", + "Huscarl", + "Gestir", + "Heavy Knight", + "Archer" +}; + +#define PVKII + +#include +#include + + +public Plugin:myinfo = { + name = NAME, + author = "psychonic", + description = "Advanced logging for PVKII. Generates auxilary logging for use with log parsers such as HLstatsX and Psychostats", + version = VERSION, + url = "http://www.hlxce.com" +}; + +#if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 +public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) +#else +public bool:AskPluginLoad(Handle:myself, bool:late, String:error[], err_max) +#endif +{ + decl String:szGameDesc[64]; + GetGameDescription(szGameDesc, sizeof(szGameDesc), true); + if (StrContains(szGameDesc, "PVKII", false) == -1) + { + decl String:szGameDir[64]; + GetGameFolderName(szGameDir, sizeof(szGameDir)); + if (StrContains(szGameDir, "pvkii", false) == -1) + { + strcopy(error, err_max, "This plugin is only supported on Pirate, Vikings, and Knights"); + #if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 + return APLRes_Failure; + #else + return false; + #endif + } + } +#if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 + return APLRes_Success; +#else + return true; +#endif +} + + +public OnPluginStart() +{ + CreatePopulateWeaponTrie(); + + g_cvar_ktraj = CreateConVar("superlogs_ktraj", "0", "Enable Psychostats \"KTRAJ\" logging (default off)", 0, true, 0.0, true, 1.0); + HookConVarChange(g_cvar_ktraj, OnCvarKtrajChange); + CreateConVar("superlogs_pvkii_version", VERSION, NAME, FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY); + + HookEvent("player_ranged_impact", Event_PlayerRangedImpact); + HookEvent("player_death", Event_PlayerDeath); + HookEvent("player_spawn", Event_PlayerSpawn); + HookEvent("update_mvp_panel", Event_RoundEnd); + HookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre); + + HookEvent("player_nemesis", Event_PlayerNemesis); + HookEvent("player_revenge", Event_PlayerRevenge); + HookEvent("player_objective", Event_PlayerObjective); + HookEvent("grail_pickup", Event_GrailPickup); + HookEvent("grail_drop", Event_GrailDrop); + HookEvent("chest_pickup", Event_ChestPickup); + HookEvent("chest_drop", Event_ChestDrop); + HookEvent("chest_capture", Event_ChestCapture); + + CreateTimer(1.0, LogMap); +} + +public OnMapStart() +{ + GetTeams(); +} + +public OnClientPutInServer(client) +{ + g_iLastClass = -1; + reset_player_stats(client); + g_bHasChest[client] = false; + g_bHasGrail[client] = false; +} + +public Event_PlayerNemesis(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogPlyrPlyrEvent(GetClientOfUserId(GetEventInt(event, "userid")), GetClientOfUserId(GetEventInt(event, "victim")), "triggered", "domination"); +} + +public Event_PlayerRevenge(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogPlyrPlyrEvent(GetClientOfUserId(GetEventInt(event, "userid")), GetClientOfUserId(GetEventInt(event, "victim")), "triggered", "revenge"); +} + +public Event_PlayerObjective(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogPlayerEvent(GetClientOfUserId(GetEventInt(event, "userid")), "triggered", "obj_complete"); +} + +public Event_ChestPickup(Handle:event, const String:name[], bool:dontBroadcast) +{ + g_bHasChest[GetClientOfUserId(GetEventInt(event, "userid"))] = true; +} + +public Event_ChestDrop(Handle:event, const String:name[], bool:dontBroadcast) +{ + g_bHasChest[GetClientOfUserId(GetEventInt(event, "userid"))] = false; +} + +public Event_GrailPickup(Handle:event, const String:name[], bool:dontBroadcast) +{ + g_bHasGrail[GetClientOfUserId(GetEventInt(event, "userid"))] = true; +} + +public Event_GrailDrop(Handle:event, const String:name[], bool:dontBroadcast) +{ + g_bHasGrail[GetClientOfUserId(GetEventInt(event, "userid"))] = false; +} + +public Event_ChestCapture(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogPlayerEvent(GetClientOfUserId(GetEventInt(event, "userid")), "triggered", "chest_capture"); +} + +public Event_PlayerRangedImpact(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID of player who fired + // "victim" "short" // entindex of entity that was hit (if any) + // "weapon" "string" // weapon that was fired + // "damage" "float" // how much damage was dealt, if 0 obviously missed or blocked by shield if victim is set + + new attacker = GetClientOfUserId(GetEventInt(event, "userid")); + if (attacker == 0 || !IsClientInGame(attacker)) + return; + + decl String:weapon[MAX_WEAPON_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + new weapon_index = get_weapon_index(weapon); + if (weapon_index == -1) + return; + + if (!strcmp(weapon, "blunderbuss")) + { + // buckshot of 8 + g_weapon_stats[attacker][weapon_index][LOG_HIT_SHOTS] += 8; + } + else + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_SHOTS]++; + } + + new victim = GetEventInt(event, "victim"); + if (victim < 1 || victim > MaxClients || !IsClientInGame(victim)) + return; + + g_weapon_stats[attacker][weapon_index][LOG_HIT_HITS]++; + g_weapon_stats[attacker][weapon_index][LOG_HIT_DAMAGE] += RoundToNearest(GetEventFloat(event, "damage")); +} + +public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast) +{ + // this extents the original player_death by a new fields + // "userid" "short" // user ID who died + // "attacker" "short" // user ID who killed + // "weapon" "string" // weapon name killer used + + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + decl String: weapon[MAX_WEAPON_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + + if (victim > 0 && attacker > 0) + { + new weapon_index = get_weapon_index(weapon); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_KILLS]++; + g_weapon_stats[victim][weapon_index][LOG_HIT_DEATHS]++; + if (GetClientTeam(attacker) == GetClientTeam(victim)) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_TEAMKILLS]++; + } + } + dump_player_stats(victim); + + LogPlyrPlyrEvent(GetEventInt(event, "assistid"), victim, "triggered", "kill assist", true); + + if (g_bHasGrail[victim]) + { + LogPlayerEvent(attacker, "triggered", "grail_defend"); + } + if (g_bHasChest[victim]) + { + LogPlayerEvent(attacker, "triggered", "chest_defend"); + } + g_bHasGrail[victim] = false; + g_bHasChest[victim] = false; + } + + if (g_logktraj) + { + LogPSKillTraj(attacker, victim, weapon); + } +} + +public Event_PlayerSpawn(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID on server + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + reset_player_stats(client); + if (IsClientInGame(client)) + { + new iCurrentClass = GetEntProp(client, Prop_Send, "m_iPlayerClass"); + if (iCurrentClass > -1 && iCurrentClass != g_iLastClass) + { + LogRoleChange(client, g_szClassNames[iCurrentClass]); + } + g_iLastClass = iCurrentClass; + } + } +} + +public Event_RoundEnd(Handle:event, const String:name[], bool:dontBroadcast) +{ + new winner = GetEventInt(event, "winner"); + if (winner > 1) + { + LogTeamEvent(winner, "triggered", "Round_Win"); + } + + LogPlayerEvent(GetEventInt(event, "pid_1"), "triggered", "mvp1"); + LogPlayerEvent(GetEventInt(event, "pid_2"), "triggered", "mvp2"); + LogPlayerEvent(GetEventInt(event, "pid_3"), "triggered", "mvp3"); + + for (new i = 1; i <= MaxClients; i++) + { + g_bHasChest[i] = false; + g_bHasGrail[i] = false; + } + + WstatsDumpAll(); +} + +public Action:Event_PlayerDisconnect(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + OnPlayerDisconnect(client); + return Plugin_Continue; +} + + +public Action:LogMap(Handle:timer) +{ + // Called 1 second after OnPluginStart since srcds does not log the first map loaded. Idea from Stormtrooper's "mapfix.sp" for psychostats + LogMapLoad(); +} + +public OnCvarKtrajChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + g_logktraj = GetConVarBool(g_cvar_ktraj); +} \ No newline at end of file diff --git a/sourcemod/scripting/superlogs-tf2.sp b/sourcemod/scripting/superlogs-tf2.sp new file mode 100644 index 0000000..14d0b52 --- /dev/null +++ b/sourcemod/scripting/superlogs-tf2.sp @@ -0,0 +1,1512 @@ +/* + * HLstatsX Community Edition - SourceMod plugin to generate advanced weapon logging + * http://www.hlxcommunity.com + * Copyright (C) 2009-2010 Nicholas Hastings (psychonic) + * Copyright (C) 2010 Thomas "CmptrWz" Berezansky + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma semicolon 1 + +#include +#include // NOTE: Gives us tf2 AND sdktools +#include // http://forums.alliedmods.net/showthread.php?t=100084 +#undef REQUIRE_EXTENSIONS +#include // http://forums.alliedmods.net/showthread.php?t=106748 +#define REQUIRE_EXTENSIONS + +#define VERSION "2.0.32" +#define NAME "SuperLogs: TF2" + +#define UNLOCKABLE_BIT (1<<30) +#define MAX_LOG_WEAPONS 28 +#define MAX_WEAPON_LEN 29 +#define MAX_BULLET_WEAPONS 14 +#define MAX_UNLOCKABLE_WEAPONS 6 +#define MAX_LOADOUT_SLOTS 8 +#define WEAPON_PREFIX_LENGTH 10 +#define WEAPON_FULL_LENGTH (WEAPON_PREFIX_LENGTH + MAX_WEAPON_LEN) +#define TELEPORT_AGAIN_TIME 10.0 +#define OBJ_DISPENSER 0 +#define OBJ_TELEPORTER_ENTRANCE 1 +#define OBJ_TELEPORTER_EXIT 2 +#define OBJ_SENTRYGUN 3 +#define OBJ_ATTACHMENT_SAPPER 4 +#define OBJ_SENTRYGUN_MINI 20 +#define ITEMINDEX_DEMOSHIELD 131 +#define ITEMINDEX_GUNBOATS 133 +#define JUMP_NONE 0 +#define JUMP_ROCKET_START 1 +#define JUMP_ROCKET 2 +#define JUMP_STICKY 3 +#define LOG_SHOTS 0 +#define LOG_HITS 1 +#define LOG_KILLS 2 +#define LOG_HEADSHOTS 3 +#define LOG_TEAMKILLS 4 +#define LOG_DAMAGE 5 +#define LOG_DEATHS 6 +#define LUNCHBOX_CHOCOLATE 159 +#define LUNCHBOX_STEAK 311 + +public Plugin:myinfo = { + name = NAME, + author = "Thomas \"CmptrWz\" Berezansky & psychonic", + description = "Advanced logging for TF2. Generates auxilary logging for use with log parsers such as HLstatsX and Psychostats", + version = VERSION, + url = "http://www.hlxcommunity.com" +}; + +// Convars we need to monitor +new Handle:cvar_crits; // tf_weapon_criticals - Needs to be on for weapon stats + +// Our convars +new Handle:cvar_actions; +new Handle:cvar_teleports; +new Handle:cvar_teleports_again; +new Handle:cvar_headshots; +new Handle:cvar_backstabs; +new Handle:cvar_sandvich; +new Handle:cvar_fire; +new Handle:cvar_wstats; +new Handle:cvar_heals; +new Handle:cvar_rolelogfix; +new Handle:cvar_objlogfix; + +// Booleans to keep track of them +new bool:b_actions; +new bool:b_teleports; +new bool:b_teleports_again; +new bool:b_headshots; +new bool:b_backstabs; +new bool:b_sandvich; +new bool:b_fire; +new bool:b_wstats; +new bool:b_heals; +new bool:b_rolelogfix; +new bool:b_objlogfix; + +// Monitoring outside libraries/cvars +new bool:b_sdkhookloaded = false; + +// Weapon trie +new Handle:h_weapontrie; +// Weapon Stats +new weaponStats[MAXPLAYERS+1][MAX_LOG_WEAPONS][7]; +new nextHurt[MAXPLAYERS+1] = {-1, ...}; +// Loadout Info +new playerLoadout[MAXPLAYERS+1][MAX_LOADOUT_SLOTS][2]; +new bool:playerLoadoutUpdated[MAXPLAYERS+1]; +new Handle:itemsKv; +new Handle:slotsTrie; +// Stunball id (so we aren't looking it up in gameframe) +new stunBallId = -1; +// Stacks for "object destroyed at spawn" +new Handle:h_objList[MAXPLAYERS+1]; +// Time storage for the same +new Float:f_objRemoved[MAXPLAYERS+1]; +// Stunball Stack +new Handle:h_stunBalls; +// Wearables Stack +new Handle:h_wearables; + +// Teleporter Stat-Padding Fix: Keep track of last use of teleporter +new Float:f_lastTeleport[MAXPLAYERS+1][MAXPLAYERS+1]; +// Heals +new healPoints[MAXPLAYERS+1]; +// Rocket/Sticky Jump Status +new jumpStatus[MAXPLAYERS+1]; +// Last dalokohs eaten +new Float:dalokohs[MAXPLAYERS+1]; + +// Last known class of players +// Likely less intensive to keep track of this for sound hooks +new TFClassType:playerClass[MAXPLAYERS+1]; + +// Is player carrying a building +new bool:g_bCarryingObject[MAXPLAYERS+1] = {false,...}; +new g_iCarryingOffs = -1; +new bool:g_bBlockLog = false; + +// Bullet Weapons Variable +new bulletWeapons = 0; + +// Arrays +// ONLY RULE OF THIS WEAPON LIST: +// Unlockable variants of a weapon go AFTER the original variant +// This will need to be tweaked if they make a second alt version of a weapon we track +new const String:weaponList[MAX_LOG_WEAPONS][MAX_WEAPON_LEN] = { + "ball", + "flaregun", + "minigun", + "natascha", + "pistol", + "pistol_scout", + "revolver", + "ambassador", + "scattergun", + "force_a_nature", + "shotgun_hwg", + "shotgun_primary", + "shotgun_pyro", + "shotgun_soldier", + "smg", + "sniperrifle", + "syringegun_medic", + "blutsauger", + "tf_projectile_arrow", + "tf_projectile_pipe", + "tf_projectile_pipe_remote", + "sticky_resistance", + "tf_projectile_rocket", + "rocketlauncher_directhit", + "deflect_rocket", + "deflect_promode", + "deflect_flare", + "deflect_arrow" +}; +// This list has none of the above rules +new const String:weaponBullet[MAX_BULLET_WEAPONS][MAX_WEAPON_LEN] = { + "ambassador", + "force_a_nature", + "minigun", + "natascha", + "pistol", + "pistol_scout", + "revolver", + "scattergun", + "shotgun_hwg", + "shotgun_primary", + "shotgun_pyro", + "shotgun_soldier", + "smg", + "sniperrifle" +}; +// This list is the list of weapons with an alternate in the next slot in the first list +new const String:weaponUnlockables[MAX_UNLOCKABLE_WEAPONS][MAX_WEAPON_LEN] = { + "minigun", + "revolver", + "scattergun", + "syringegun_medic", + "tf_projectile_pipe_remote", + "tf_projectile_rocket" +}; + +public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) +{ + MarkNativeAsOptional("SDKHook"); // This needs to be marked optional for a number of reasons + return APLRes_Success; +} + +public OnPluginStart() +{ + CreateConVar("superlogs_tf_version", VERSION, NAME, FCVAR_PLUGIN|FCVAR_NOTIFY); + + cvar_crits = FindConVar("tf_weapon_criticals"); + cvar_actions = CreateConVar("superlogs_actions", "1", "Enable logging of most player actions, such as \"stun\" (default on)", 0, true, 0.0, true, 1.0); + cvar_teleports = CreateConVar("superlogs_teleports", "1", "Enable logging of teleports (default on)", 0, true, 0.0, true, 1.0); + cvar_teleports_again = CreateConVar("superlogs_teleports_again", "1", "Repeated use of same teleporter in 10 seconds adds _again to event (default on)", 0, true, 0.0, true, 1.0); + cvar_headshots = CreateConVar("superlogs_headshots", "0", "Enable logging of headshot player action (default off)", 0, true, 0.0, true, 1.0); + cvar_backstabs = CreateConVar("superlogs_backstabs", "1", "Enable logging of backstab player action (default on)", 0, true, 0.0, true, 1.0); + cvar_sandvich = CreateConVar("superlogs_sandvich", "1", "Enable logging of sandvich eating (default on)", 0, true, 0.0, true, 1.0); + cvar_fire = CreateConVar("superlogs_fire", "1", "Enable logging of fiery arrows as a separate weapon from regular arrows (default on)", 0, true, 0.0, true, 1.0); + cvar_wstats = CreateConVar("superlogs_wstats", "1", "Enable logging of weapon stats (default on, only works when tf_weapon_criticals is 1)", 0, true, 0.0, true, 1.0); + cvar_heals = CreateConVar("superlogs_heals", "1", "Enable logging of healpoints upon death (default on)", 0, true, 0.0, true, 1.0); + cvar_rolelogfix = CreateConVar("superlogs_rolelogfix", "1", "Enable logging of healpoints upon death (default on)", 0, true, 0.0, true, 1.0); + cvar_objlogfix = CreateConVar("superlogs_objlogfix", "1", "Enable logging of owner object destruction on team/class change (default on)", 0, true, 0.0, true, 1.0); + + HookConVarChange(cvar_crits,OnConVarStatsChange); + HookConVarChange(cvar_actions,OnConVarActionsChange); + HookConVarChange(cvar_teleports,OnConVarTeleportsChange); + HookConVarChange(cvar_teleports_again,OnConVarTeleportsAgainChange); + HookConVarChange(cvar_headshots,OnConVarHeadshotsChange); + HookConVarChange(cvar_backstabs,OnConVarBackstabsChange); + HookConVarChange(cvar_sandvich,OnConVarSandvichChange); + HookConVarChange(cvar_fire,OnConVarFireChange); + HookConVarChange(cvar_wstats,OnConVarStatsChange); + HookConVarChange(cvar_heals,OnConVarHealsChange); + HookConVarChange(cvar_rolelogfix,OnConVarRolelogfixChange); + HookConVarChange(cvar_objlogfix,OnConVarObjlogfixChange); + + h_stunBalls = CreateStack(); + h_wearables = CreateStack(); + itemsKv = CreateKeyValues("items_game"); + if(FileToKeyValues(itemsKv, "scripts/items/items_game.txt")) + KvJumpToKey(itemsKv, "items"); + slotsTrie = CreateTrie(); + SetTrieValue(slotsTrie, "primary", 0); + SetTrieValue(slotsTrie, "secondary", 1); + SetTrieValue(slotsTrie, "melee", 2); + SetTrieValue(slotsTrie, "pda", 3); + SetTrieValue(slotsTrie, "pda2", 4); + SetTrieValue(slotsTrie, "building", 5); + SetTrieValue(slotsTrie, "head", 6); + SetTrieValue(slotsTrie, "misc", 7); + + // Populate the Weapon Trie + // Creates it too, technically + PopulateWeaponTrie(); + + // Populate stacks + for(new i = 0; i <= MAXPLAYERS; i++) + h_objList[i] = CreateStack(); + + // Hook Events + HookEvent("player_death", Event_PlayerDeath); + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + HookEvent("object_destroyed", Event_ObjectDestroyed); + HookEvent("object_destroyed", Event_ObjectDestroyedPre, EventHookMode_Pre); + HookEvent("player_builtobject", Event_PlayerBuiltObject); + HookEvent("player_builtobject", Event_PlayerBuiltObjectPre, EventHookMode_Pre); + HookEvent("player_hurt", Event_PlayerHurt); + HookEvent("player_spawn", Event_PlayerSpawn); + HookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre); + HookEvent("post_inventory_application", Event_PostInventoryApplication); + + HookEvent("arena_win_panel", Event_WinPanel); + HookEvent("teamplay_win_panel", Event_WinPanel); + + g_iCarryingOffs = FindSendPropInfo("CTFPlayer", "m_bCarryingObject"); + + //AutoExecConfig(); // Create/request load of config file + AutoExecConfig(false); // Dont auto-make, but load if you find it. + + CreateTimer(1.0, LogMap); + + AddGameLogHook(OnGameLog); +} + +public OnMapStart() +{ + GetTeams(); // Loghelper says to put this here. Who am I to argue? +} + +public OnConfigsExecuted() +{ + OnConVarStatsChange(cvar_wstats, "", ""); + OnConVarActionsChange(cvar_actions, "", ""); + OnConVarTeleportsChange(cvar_teleports, "", ""); + OnConVarTeleportsAgainChange(cvar_teleports_again, "", ""); + OnConVarHeadshotsChange(cvar_headshots, "", ""); + OnConVarBackstabsChange(cvar_backstabs, "", ""); + OnConVarSandvichChange(cvar_sandvich, "", ""); + OnConVarFireChange(cvar_fire, "", ""); + OnConVarHealsChange(cvar_heals, "", ""); + OnConVarRolelogfixChange(cvar_rolelogfix, "", ""); + OnConVarObjlogfixChange(cvar_objlogfix, "", ""); +} + +public Action:TF2_CalcIsAttackCritical(attacker, weapon, String:weaponname[], &bool:result) +{ + if(b_wstats && attacker > 0 && attacker <= MaxClients) + { + new weapon_index = GetWeaponIndex(weaponname[WEAPON_PREFIX_LENGTH], attacker); + if(weapon_index != -1) + { + weaponStats[attacker][weapon_index][LOG_SHOTS]++; + if((1< 0 && attacker <= MaxClients && attacker != victim && inflictor > MaxClients && damage > 0.0 && IsValidEntity(inflictor) && (GetEntityFlags(victim) & (FL_ONGROUND | FL_INWATER)) == 0) + { + decl String:weapon[WEAPON_FULL_LENGTH]; + GetEdictClassname(inflictor, weapon, sizeof(weapon)); + if(weapon[3] == 'p' && weapon[4] == 'r') // Eliminate pumpkin bomb with the r + { + switch(weapon[14]) + { + case 'r': + { + LogPlayerEvent(attacker, "triggered", "airshot_rocket"); + if(jumpStatus[attacker] == JUMP_ROCKET) + LogPlayerEvent(attacker, "triggered", "air2airshot_rocket"); + } + case 'p': + { + if(weapon[18] != 0) + { + LogPlayerEvent(attacker, "triggered", "airshot_sticky"); + if(jumpStatus[attacker] == JUMP_STICKY) + LogPlayerEvent(attacker, "triggered", "air2airshot_sticky"); + } + else + { + LogPlayerEvent(attacker, "triggered", "airshot_pipebomb"); + if(jumpStatus[attacker] == JUMP_STICKY) + LogPlayerEvent(attacker, "triggered", "air2airshot_pipebomb"); + } + } + case 'a': + LogPlayerEvent(attacker, "triggered", "airshot_arrow"); + case 'f': + if(damage > 10.0) + LogPlayerEvent(attacker, "triggered", "airshot_flare"); + } + } + } + return Plugin_Continue; +} + +public OnTakeDamage_Post(victim, attacker, inflictor, Float:damage, damagetype) +{ + if(b_wstats && attacker > 0 && attacker <= MaxClients) + { + new weapon_index = -1; + new idamage = RoundFloat(damage); + decl String:weapon[WEAPON_FULL_LENGTH]; + if (inflictor <= MaxClients) // Inflictor is a player + { + if (damagetype & DMG_BURN) + return; + if(inflictor == attacker && damagetype & 1 && damage == 1000.0) // Telefrag + return; + GetClientWeapon(attacker, weapon, sizeof(weapon)); + weapon_index = GetWeaponIndex(weapon[WEAPON_PREFIX_LENGTH], attacker); + } + else if (IsValidEdict(inflictor)) + { + GetEdictClassname(inflictor, weapon, sizeof(weapon)); + if (weapon[WEAPON_PREFIX_LENGTH] == 'g') + return; // grenadelauncher, but the projectile does damage, not the weapon. So this must be Charge N Targe. + else if(weapon[3] == 'p') + { + weapon_index = GetWeaponIndex(weapon, attacker, inflictor); + } + else + { + // Baseballs are funky. + // Inflictor is the BAT + // But the melee Crush damage (and the nevergib I don't check here) aren't set + // Still has CLUB damage though, and on a melee strike the inflictor is the PLAYER, not the weapon + // Just in case either way, forcing it to get the index of "ball" + if(!(damagetype & DMG_CRUSH) && (damagetype & DMG_CLUB) && StrEqual(weapon, "tf_weapon_bat_wood")) + weapon_index = GetWeaponIndex("ball", attacker); + else + weapon_index = GetWeaponIndex(weapon[WEAPON_PREFIX_LENGTH], attacker); + } + } + if(b_wstats && weapon_index > -1) + { + weaponStats[attacker][weapon_index][LOG_DAMAGE] += idamage; + weaponStats[attacker][weapon_index][LOG_HITS]++; + } + } +} + +public OnGameFrame() +{ + new entity; + new owner; + new itemindex, slot; + decl String:tempstring[15]; + if(stunBallId > -1) + { + while(PopStackCell(h_stunBalls, entity)) + { + if(IsValidEntity(entity)) + { + owner = GetEntPropEnt(entity, Prop_Send, "m_hThrower"); + if(owner > 0 && owner <= MaxClients) + weaponStats[owner][stunBallId][LOG_SHOTS]++; + } + } + } + while(PopStackCell(h_wearables, entity)) + { + if(IsValidEntity(entity)) + { + owner = GetEntPropEnt(entity, Prop_Send, "m_hOwnerEntity"); + if(owner > 0 && owner <= MaxClients) + { + itemindex = GetEntProp(entity, Prop_Send, "m_iItemDefinitionIndex"); + Format(tempstring, sizeof(tempstring), "%d", itemindex); + if(KvJumpToKey(itemsKv, tempstring)) + { + KvGetString(itemsKv, "item_slot", tempstring, sizeof(tempstring)); + if(GetTrieValue(slotsTrie, tempstring, slot)) + { + if(slot == 0 && playerClass[owner] == TFClass_DemoMan) + slot++; + if(playerLoadout[owner][slot][0] != itemindex) + { + playerLoadout[owner][slot][0] = itemindex; + playerLoadoutUpdated[owner] = true; + } + playerLoadout[owner][slot][1] = entity; + } + KvGoBack(itemsKv); + } + } + } + } + + new cnt = GetClientCount(); + for (new i = 1; i <= cnt; i++) + { + if (IsClientInGame(i) && GetEntData(i, g_iCarryingOffs, 1)) + g_bCarryingObject[i] = true; + } +} + +public OnClientPutInServer(client) +{ + if (b_sdkhookloaded) + { + SDKHook(client, SDKHook_OnTakeDamagePost, OnTakeDamage_Post); + SDKHook(client, SDKHook_OnTakeDamage, OnTakeDamage); + } + playerLoadoutUpdated[client] = true; + + g_bCarryingObject[client] = false; + + for(new i = 0; i <= MaxClients; i++) + { + // Clear both "we built" (client first) and "we used" (i first) + f_lastTeleport[client][i] = 0.0; + f_lastTeleport[i][client] = 0.0; + } + f_objRemoved[client] = 0.0; + playerClass[client] = TFClass_Unknown; + healPoints[client] = 0; + for(new i = 0; i < MAX_LOADOUT_SLOTS; i++) + playerLoadout[client][i] = {-1, -1}; + ResetWeaponStats(client); +} + +public Action:Event_PlayerDisconnect(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if(client > 0 && IsClientInGame(client)) + { + if(b_wstats) DumpWeaponStats(client); + if(b_heals) DumpHeals(client, " (disconnect)"); + } + + return Plugin_Continue; +} + +HookAllClients() +{ + for (new i = 1; i <= MaxClients; i++) + if (IsClientInGame(i)) + { + SDKHook(i, SDKHook_OnTakeDamagePost, OnTakeDamage_Post); + SDKHook(i, SDKHook_OnTakeDamage, OnTakeDamage); + } +} + +public Action:Event_PlayerChangeclassPre(Handle:event, const String:name[], bool:dontBroadcast) +{ + // Stop log entry! + return Plugin_Handled; +} + +public Event_PlayerSpawn(Handle:event, const String:name[], bool:dontBroadcast) +{ + new Float:time = GetGameTime(); + new userid = GetEventInt(event, "userid"); + new client = GetClientOfUserId(userid); + new TFClassType:spawnClass = TFClassType:GetEventInt(event, "class"); + jumpStatus[client] = JUMP_NONE; // Play it safe + if(b_wstats) DumpWeaponStats(client); // Changed class without death, dump and reset stats + //if(b_wstats) ResetWeaponStats(client); + if(b_heals) DumpHeals(client, " (spawn)"); + if(time == f_objRemoved[client]) + { + decl String:owner[96]; + decl String:player_authid[32]; + decl String:objname[24]; + if (!GetClientAuthString(client, player_authid, sizeof(player_authid))) + strcopy(player_authid, sizeof(player_authid), "UNKNOWN"); + Format(owner, sizeof(owner), "\"%N<%d><%s><%s>\"", client, userid, player_authid, g_team_list[GetClientTeam(client)]); + new objecttype; + while(PopStackCell(h_objList[client], objecttype)) + { + switch(objecttype) + { + case OBJ_DISPENSER: + objname = "OBJ_DISPENSER"; + case OBJ_TELEPORTER_ENTRANCE: + objname = "OBJ_TELEPORTER_ENTRANCE"; + case OBJ_TELEPORTER_EXIT: + objname = "OBJ_TELEPORTER_EXIT"; + case OBJ_SENTRYGUN: + objname = "OBJ_SENTRYGUN"; + case OBJ_ATTACHMENT_SAPPER: + objname = "OBJ_ATTACHMENT_SAPPER"; + case OBJ_SENTRYGUN_MINI: + objname = "OBJ_SENTRYGUN_MINI"; + default: + continue; + } + LogToGame("%s triggered \"killedobject\" (object \"%s\") (weapon \"pda_engineer\") (objectowner %s) (spawn)", owner, objname, owner); + } + } + if(b_rolelogfix && playerClass[client] != spawnClass) + { + switch(spawnClass) + { + case TFClass_Scout: + LogRoleChange(client, "scout"); + case TFClass_Sniper: + LogRoleChange(client, "sniper"); + case TFClass_Soldier: + LogRoleChange(client, "soldier"); + case TFClass_DemoMan: + LogRoleChange(client, "demoman"); + case TFClass_Medic: + LogRoleChange(client, "medic"); + case TFClass_Heavy: + LogRoleChange(client, "heavyweapons"); + case TFClass_Pyro: + LogRoleChange(client, "pyro"); + case TFClass_Spy: + LogRoleChange(client, "spy"); + case TFClass_Engineer: + LogRoleChange(client, "engineer"); + default: + LogRoleChange(client, "unknown"); + } + } + playerClass[client] = spawnClass; + dalokohs[client] = -30.0; +} + +public Event_ObjectRemoved(Handle:event, const String:name[], bool:dontBroadcast) +{ + new Float:time = GetGameTime(); + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if(time != f_objRemoved[client]) + { + f_objRemoved[client] = time; + while(PopStack(h_objList[client])) + continue; + } + new objtype = GetEventInt(event, "objecttype"); + new objindex = GetEventInt(event, "index"); + if (IsValidEdict(objindex) && GetEntProp(GetEventInt(event, "index"), Prop_Send, "m_bMiniBuilding", 1)) + { + objtype = OBJ_SENTRYGUN_MINI; + } + PushStackCell(h_objList[client], objtype); +} + +public Event_PlayerStealsandvich(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogPlyrPlyrEvent(GetClientOfUserId(GetEventInt(event, "target")), GetClientOfUserId(GetEventInt(event, "owner")), "triggered", "steal_sandvich", true); +} + +public Event_PlayerStunned(Handle:event, const String:name[], bool:dontBroadcast) +{ + new String: properties[33]; + new stunner = GetClientOfUserId(GetEventInt(event, "stunner")); + if(stunner > 0) // Stunner == 0 would be map stun (ghost/trigger), natascha stun (slowdown), taunt kill stun (medic, sniper) + { + new victim = GetClientOfUserId(GetEventInt(event, "victim")); + if(GetEventBool(event, "victim_capping")) StrCat(properties, sizeof(properties), " (victim_capping)"); + if(GetEventBool(event, "big_stun")) StrCat(properties, sizeof(properties), " (big_stun)"); + LogPlyrPlyrEvent(stunner, victim, "triggered", "stun", true); + if((GetEntityFlags(victim) & (FL_ONGROUND | FL_INWATER)) == 0) + LogPlayerEvent(stunner, "triggered", "airshot_stun"); + } +} + +public Event_PlayerTeleported(Handle:event, const String:name[], bool:dontBroadcast) +{ + new builderid = GetClientOfUserId(GetEventInt(event, "builderid")); + new userid = GetClientOfUserId(GetEventInt(event, "userid")); + new Float:curTime = GetGameTime(); + if(b_teleports_again && f_lastTeleport[builderid][userid] > curTime - TELEPORT_AGAIN_TIME) + { + if(userid == builderid) + { + LogPlayerEvent(userid, "triggered", "teleport_self_again"); + } + else + { + LogPlayerEvent(builderid, "triggered", "teleport_again"); + LogPlayerEvent(userid, "triggered", "teleport_used_again"); + } + } + else + { + if(userid == builderid) + { + LogPlayerEvent(userid, "triggered", "teleport_self"); + } + else + { + LogPlayerEvent(builderid, "triggered", "teleport"); + LogPlayerEvent(userid, "triggered", "teleport_used"); + } + } + f_lastTeleport[builderid][userid] = curTime; +} + +public Event_DeployBuffBanner(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogPlayerEvent(GetClientOfUserId(GetEventInt(event, "buff_owner")), "triggered", "buff_deployed"); +} + +public Event_MedicDefended(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogPlayerEvent(GetClientOfUserId(GetEventInt(event, "userid")), "triggered", "defended_medic"); +} + +public Event_PlayerEscortScore(Handle:event, const String:name[], bool:dontBroadcast) +{ + LogPlayerEvent(GetEventInt(event, "player"), "triggered", "escort_score"); +} + +public Event_MedicDeath(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + healPoints[client] = GetEntProp(client, Prop_Send, "m_iHealPoints"); +} + +public Event_PlayerHurt(Handle:event, const String:name[], bool:dontBroadcast) +{ + // NOTE: The weaponid in this event is the weapon the player is HOLDING as of the event happening + // Thus, only if we don't have SDK Hooks do we use it for airshot detection. + if(!b_sdkhookloaded) + { + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + if(b_wstats) + { + if(nextHurt[attacker] > -1) + weaponStats[attacker][nextHurt[attacker]][LOG_HITS]++; + nextHurt[attacker] = -1; + } + if(b_actions) + { + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if(client != attacker && client > 0 && client <= MaxClients && IsClientInGame(client) + && IsPlayerAlive(client) && (GetEntityFlags(client) & (FL_ONGROUND | FL_INWATER)) == 0) + { + switch(GetEventInt(event, "weaponid")) + { + case TF_WEAPON_ROCKETLAUNCHER, TF_WEAPON_DIRECTHIT: + { + LogPlayerEvent(attacker, "triggered", "airshot_rocket"); + if(jumpStatus[attacker] == JUMP_ROCKET) + LogPlayerEvent(attacker, "triggered", "air2airshot_rocket"); + } + case TF_WEAPON_GRENADELAUNCHER: + { + LogPlayerEvent(attacker, "triggered", "airshot_pipebomb"); + if(jumpStatus[attacker] == JUMP_STICKY) + LogPlayerEvent(attacker, "triggered", "air2airshot_pipebomb"); + } + case TF_WEAPON_PIPEBOMBLAUNCHER: + { + LogPlayerEvent(attacker, "triggered", "airshot_sticky"); + if(jumpStatus[attacker] == JUMP_STICKY) + LogPlayerEvent(attacker, "triggered", "air2airshot_sticky"); + } + case TF_WEAPON_FLAREGUN: + { + if(GetEventInt(event, "damageamount") > 10) + { + LogPlayerEvent(attacker, "triggered", "airshot_flare"); + } + } + case TF_WEAPON_COMPOUND_BOW: + { + LogPlayerEvent(attacker, "triggered", "airshot_arrow"); + } + } + } + } + } +} + +public Action:Event_ObjectDestroyedPre(Handle:event, const String:name[], bool:dontBroadcast) +{ + if (GetEntProp(GetEventInt(event, "index"), Prop_Send, "m_bMiniBuilding", 1)) + { + g_bBlockLog = true; + } + + return Plugin_Continue; +} + +public Event_ObjectDestroyed(Handle:event, const String:name[], bool:dontBroadcast) +{ + if (g_bBlockLog) + { + g_bBlockLog = false; + decl String:weapon[64]; + decl String:team[64]; + decl String:auth[32]; + decl String:properties[255]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + new victimuid = GetEventInt(event, "userid"); + new victim = GetClientOfUserId(victimuid); + if (victim == 0 || !IsClientInGame(victim)) + return; + GetClientAuthString(victim, auth, sizeof(auth)); + GetTeamName(GetClientTeam(victim), team, sizeof(team)); + Format(properties, sizeof(properties), " (object \"OBJ_SENTRYGUN_MINI\") (weapon \"%s\") (objectowner \"%N<%d><%s><%s>\")", weapon, victim, victimuid, auth, team); + LogPlayerEvent(GetClientOfUserId(GetEventInt(event, "attacker")), "triggered", "killedobject", true, properties); + } +} + +public Action:Event_PlayerBuiltObjectPre(Handle:event, const String:name[], bool:dontBroadcast) +{ + if (g_bCarryingObject[GetClientOfUserId(GetEventInt(event, "userid"))] || GetEntProp(GetEventInt(event, "index"), Prop_Send, "m_bMiniBuilding", 1)) + { + g_bBlockLog = true; + } + + return Plugin_Continue; +} + +public Event_PlayerBuiltObject(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + + if (g_bBlockLog) + { + g_bBlockLog = false; + if (!g_bCarryingObject[client] && GetEntProp(GetEventInt(event, "index"), Prop_Send, "m_bMiniBuilding", 1)) + { + LogPlayerEvent(client, "triggered", "builtobject", true, " (object \"OBJ_SENTRYGUN_MINI\")"); + } + } + + g_bCarryingObject[client] = false; +} + +public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast) +{ + new death_flags = GetEventInt(event, "death_flags"); + if((death_flags & TF_DEATHFLAG_DEADRINGER) == TF_DEATHFLAG_DEADRINGER) // Not a dead ringer death? + { + return; + } + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + new customkill = GetEventInt(event, "customkill"); + new bits = GetEventInt(event, "damagebits"); + if(b_heals && playerClass[client] != TFClass_Medic) // medic_death event handles this for dead medics + DumpHeals(client); + jumpStatus[client] = JUMP_NONE; // Not jumping + g_bCarryingObject[client] = false; + if(b_actions) + { + if(attacker == client && customkill == TF_CUSTOM_SUICIDE) + LogPlayerEvent(client, "triggered", "force_suicide"); + else + { + switch(jumpStatus[client]) + { + case 2: + { + LogPlayerEvent(client, "triggered", "rocket_failjump"); + if(attacker > 0 && attacker != client) + LogPlayerEvent(attacker, "triggered", "rocket_jumper_kill"); + } + case 3: + { + LogPlayerEvent(client, "triggered", "sticky_failjump"); + if(attacker > 0 && attacker != client) + LogPlayerEvent(attacker, "triggered", "sticky_jumper_kill"); + } + } + if(bits & DMG_DROWN) + { + LogPlayerEvent(client, "triggered", "drowned"); + } + else if(attacker != client) + { + switch(jumpStatus[attacker]) // Don't need to check attacker != 0 here as world will never rocket/sticky jump + { + case 2: + LogPlayerEvent(attacker, "triggered", "rocket_jump_kill"); + case 3: + LogPlayerEvent(attacker, "triggered", "sticky_jump_kill"); + } + if ((bits & DMG_ACID) && attacker > 0 && customkill != TF_CUSTOM_HEADSHOT) + LogPlayerEvent(attacker, "triggered", "crit_kill"); + else if((death_flags & TF_DEATHFLAG_FIRSTBLOOD) == TF_DEATHFLAG_FIRSTBLOOD) + LogPlayerEvent(attacker, "triggered", "first_blood"); + if (customkill == TF_CUSTOM_HEADSHOT && client > 0 && client <= MaxClients + && IsClientInGame(client) && (GetEntityFlags(client) & (FL_ONGROUND | FL_INWATER)) == 0) + LogPlayerEvent(attacker, "triggered", "airshot_headshot"); + } + } + } + if(b_wstats && client > 0 && attacker > 0 && attacker <= MaxClients) + { + decl String:weaponlogname[MAX_WEAPON_LEN]; + GetEventString(event, "weapon_logclassname", weaponlogname, sizeof(weaponlogname)); + new weapon_index = GetWeaponIndex(weaponlogname, attacker); + if(weapon_index != -1) + { + weaponStats[attacker][weapon_index][LOG_KILLS]++; + if(customkill == TF_CUSTOM_HEADSHOT) + weaponStats[attacker][weapon_index][LOG_HEADSHOTS]++; + weaponStats[client][weapon_index][LOG_DEATHS]++; + if(GetClientTeam(client) == GetClientTeam(attacker)) + weaponStats[attacker][weapon_index][LOG_TEAMKILLS]++; + } + DumpWeaponStats(client); + } +} + +public Action:Event_PlayerDeathPre(Handle:event, const String:name[], bool:dontBroadcast) +{ + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + new customkill = GetEventInt(event, "customkill"); + new inflictor = GetEventInt(event, "inflictor_entindex"); + if (!IsValidEdict(inflictor)) + { + inflictor = 0; + } + + switch (customkill) + { + case TF_CUSTOM_HEADSHOT: + if(b_headshots) + { + LogPlyrPlyrEvent(attacker, victim, "triggered", "headshot"); + } + case TF_CUSTOM_BACKSTAB: + if(b_backstabs) + { + LogPlyrPlyrEvent(attacker, victim, "triggered", "backstab"); + } + case TF_CUSTOM_BURNING_ARROW, TF_CUSTOM_FLYINGBURN: + if(b_fire) + { + decl String:logweapon[64]; + GetEventString(event, "weapon_logclassname", logweapon, sizeof(logweapon)); + if(logweapon[0] != 'd') // No changing reflects - was 'r' but it is deflects + { + SetEventString(event, "weapon_logclassname", "tf_projectile_arrow_fire"); + } + } + case TF_CUSTOM_TAUNT_UBERSLICE: + { + if(GetEventInt(event, "weaponid") == TF_WEAPON_BONESAW) + { + SetEventString(event, "weapon_logclassname", "taunt_medic"); + + // Might as well fix the kill icon, too, as long as we're here + // Courtesy of FlaminSarge + SetEventString(event, "weapon", "taunt_medic"); + } + } + + case TF_CUSTOM_DECAPITATION_BOSS: + { + LogPlayerEvent(attacker, "triggered", "killed_by_horseman", true); + } + } + + return Plugin_Continue; +} + +public Event_WinPanel(Handle:event, const String:name[], bool:dontBroadcast) +{ + if(b_actions) + { + LogPlayerEvent(GetEventInt(event, "player_1"), "triggered", "mvp1"); + LogPlayerEvent(GetEventInt(event, "player_2"), "triggered", "mvp2"); + LogPlayerEvent(GetEventInt(event, "player_3"), "triggered", "mvp3"); + } + if(b_wstats) + DumpAllWeaponStats(); +} + +public Event_RocketJump(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + new status = jumpStatus[client]; + if(status == JUMP_ROCKET_START) // Taunt kills trigger one event, rocket jumps two + { + jumpStatus[client] = JUMP_ROCKET; + LogPlayerEvent(client, "triggered", "rocket_jump"); + } + else if(status != JUMP_ROCKET) + jumpStatus[client] = JUMP_ROCKET_START; +} + +public Event_StickyJump(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if(jumpStatus[client] != JUMP_STICKY) + { + jumpStatus[client] = JUMP_STICKY; + LogPlayerEvent(client, "triggered", "sticky_jump"); + } +} + +public Event_JumpLanded(Handle:event, const String:name[], bool:dontBroadcast) +{ + jumpStatus[GetClientOfUserId(GetEventInt(event, "userid"))] = JUMP_NONE; +} + +public Event_ObjectDeflected(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + new owner = GetClientOfUserId(GetEventInt(event, "ownerid")); + switch(GetEventInt(event, "weaponid")) + { + case TF_WEAPON_NONE: + { + LogPlyrPlyrEvent(client, owner, "triggered", "airblast_player", true); + } + case TF_WEAPON_ROCKETLAUNCHER: + { + LogPlyrPlyrEvent(client, owner, "triggered", "deflected_rocket", true); + if(b_wstats && b_sdkhookloaded) + { + new weapon_index = GetWeaponIndex("deflect_rocket"); + if(weapon_index > -1) + weaponStats[client][weapon_index][LOG_SHOTS]++; + } + } + case TF_WEAPON_GRENADE_DEMOMAN: + { + LogPlyrPlyrEvent(client, owner, "triggered", "deflected_pipebomb", true); + if(b_wstats && b_sdkhookloaded) + { + new weapon_index = GetWeaponIndex("deflect_promode"); + if(weapon_index > -1) + weaponStats[client][weapon_index][LOG_SHOTS]++; + } + } + case TF_WEAPON_FLAREGUN: + { + LogPlyrPlyrEvent(client, owner, "triggered", "deflected_flare", true); + if(b_wstats && b_sdkhookloaded) + { + new weapon_index = GetWeaponIndex("deflect_flare"); + if(weapon_index > -1) + weaponStats[client][weapon_index][LOG_SHOTS]++; + } + } + case TF_WEAPON_JAR: + { + LogPlyrPlyrEvent(client, owner, "triggered", "deflected_jarate", true); + } + case TF_WEAPON_COMPOUND_BOW: + { + LogPlyrPlyrEvent(client, owner, "triggered", "deflected_arrow", true); + if(b_wstats && b_sdkhookloaded) + { + new weapon_index = GetWeaponIndex("deflect_arrow"); + if(weapon_index > -1) + weaponStats[client][weapon_index][LOG_SHOTS]++; + } + } + case TF_WEAPON_DIRECTHIT: + { + LogPlyrPlyrEvent(client, owner, "triggered", "deflected_rocket_dh", true); + if(b_wstats && b_sdkhookloaded) + { + new weapon_index = GetWeaponIndex("deflect_rocket"); + if(weapon_index > -1) + weaponStats[client][weapon_index][LOG_SHOTS]++; + } + } + case TF_WEAPON_GRENADE_STUNBALL: + { + LogPlyrPlyrEvent(client, owner, "triggered", "deflected_baseball", true); + } + } +} + +public Event_PostInventoryApplication(Handle:event, const String:name[], bool:dontBroadcast) +{ + CreateTimer(0.2, CheckPlayerLoadout, GetEventInt(event, "userid")); +} + +public Action:CheckPlayerLoadout(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client == 0 || !IsClientInGame(client)) + { + return Plugin_Stop; + } + + new ent = -1; + new bool:newLoadout = false; + new TFClassType:pClass = playerClass[client]; + for(new checkslot = 0; checkslot <=5; checkslot++) + { + if(playerLoadout[client][checkslot][1] != 0 && IsValidEntity(playerLoadout[client][checkslot][1])) + { + continue; + } + ent = GetPlayerWeaponSlot(client, checkslot); + if(ent == -1) + { + // Nothing in slot? + if(b_sdkhookloaded && checkslot < 3 && (pClass == TFClass_Soldier || pClass == TFClass_DemoMan)) // Maybe gunboats? Or charge n targe? + { + playerLoadout[client][checkslot][1] = -1; + continue; + } + if(playerLoadout[client][checkslot][0] == -1) + continue; + playerLoadout[client][checkslot] = {-1, -1}; + newLoadout = true; + } + else + { + new itemindex = GetEntProp(ent, Prop_Send, "m_iItemDefinitionIndex"); + if(playerLoadout[client][checkslot][0] != itemindex) + { + playerLoadout[client][checkslot][0] = itemindex; + newLoadout = true; + } + playerLoadout[client][checkslot][1] = EntIndexToEntRef(ent); + } + } + if(b_sdkhookloaded) + { + if(newLoadout) // Just in case we already updated it due to a hat spawning or being a new client + playerLoadoutUpdated[client] = true; + CreateTimer(0.2, LogWeaponLoadout, userid); + return Plugin_Stop; + } + if (newLoadout) + LogWeaponLoadout(INVALID_HANDLE, userid); + + return Plugin_Stop; +} + +public Action:LogWeaponLoadout(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client > 0 && IsClientInGame(client)) + { + for (new i = 0; i < MAX_LOADOUT_SLOTS; i++) + { + if(playerLoadout[client][i][0] != -1 && !IsValidEntity(playerLoadout[client][i][1]) || playerLoadout[client][i][1] == 0) + { + playerLoadout[client][i] = {-1, -1}; + playerLoadoutUpdated[client] = true; + } + } + if (playerLoadoutUpdated[client] == false) + return Plugin_Stop; + playerLoadoutUpdated[client] = false; + + decl String:logString[255]; + Format(logString, sizeof(logString), " (primary \"%d\") (secondary \"%d\") (melee \"%d\") (pda \"%d\") (pda2 \"%d\") (building \"%d\") (head \"%d\") (misc \"%d\")", playerLoadout[client][0][0], playerLoadout[client][1][0], playerLoadout[client][2][0], playerLoadout[client][3][0], playerLoadout[client][4][0], playerLoadout[client][5][0], playerLoadout[client][6][0], playerLoadout[client][7][0]); + + LogPlayerEvent(client, "triggered", "player_loadout", _, logString); + } + return Plugin_Stop; +} + +public Action:Event_PlayerJarated(UserMsg:msg_id, Handle:bf, const players[], playersNum, bool:reliable, bool:init) +{ + new client = BfReadByte(bf); + new victim = BfReadByte(bf); + + if (!victim || !IsClientInGame(victim)) + { + return Plugin_Continue; + } + + + if (TF2_IsPlayerInCondition(victim, TFCond_Jarated)) + { + LogPlyrPlyrEvent(client, victim, "triggered", "jarate", true); + } + else if (TF2_IsPlayerInCondition(victim, TFCond_Milked)) + { + LogPlyrPlyrEvent(client, victim, "triggered", "madmilk", true); + } + + return Plugin_Continue; +} + +public Action:Event_PlayerShieldBlocked(UserMsg:msg_id, Handle:bf, const players[], playersNum, bool:reliable, bool:init) +{ + new victim = BfReadByte(bf); + new client = BfReadByte(bf); + + LogPlyrPlyrEvent(client, victim, "triggered", "shield_blocked", true); + return Plugin_Continue; +} + +// Modified Octo's method a bit to try and reduce checking of sound strings +public Action:SoundHook(clients[64], &numClients, String:sample[PLATFORM_MAX_PATH], &entity, &channel, &Float:volume, &level, &pitch, &flags) +{ + if(entity <= MaxClients && clients[0] == entity && playerClass[entity] == TFClass_Heavy && StrEqual(sample,"vo/SandwichEat09.wav")) + { + switch(playerLoadout[entity][1][0]) + { + case LUNCHBOX_CHOCOLATE: + { + LogPlayerEvent(entity, "triggered", "dalokohs"); + new Float:time = GetGameTime(); + if(time - dalokohs[entity] > 30) + LogPlayerEvent(entity, "triggered", "dalokohs_healthboost"); + dalokohs[entity] = time; + if(GetClientHealth(entity) < 350) + LogPlayerEvent(entity, "triggered", "dalokohs_healself"); + } + case LUNCHBOX_STEAK: + { + LogPlayerEvent(entity, "triggered", "steak"); + } + default: + { + LogPlayerEvent(entity, "triggered", "sandvich"); + if(GetClientHealth(entity) < 300) + LogPlayerEvent(entity, "triggered", "sandvich_healself"); + } + } + } + return Plugin_Continue; +} + +public Action:LogMap(Handle:timer) +{ + // Called 1 second after OnPluginStart since srcds does not log the first map loaded. Idea from Stormtrooper's "mapfix.sp" for psychostats + LogMapLoad(); +} + +PopulateWeaponTrie() +{ + // Create a Trie + h_weapontrie = CreateTrie(); + + // Initial populate + for(new i = 0; i < MAX_LOG_WEAPONS; i++) + SetTrieValue(h_weapontrie, weaponList[i], i); + + // Figure out the Bullet Weapon ids (based on the list of bullet weapons) + new index; + bulletWeapons = 0; + for(new i = 0; i < MAX_BULLET_WEAPONS; i++) + if(GetTrieValue(h_weapontrie, weaponBullet[i], index)) + bulletWeapons |= (1< -1) // Projectile? + { + if(client == GetEntProp(weapon, Prop_Send, "m_iDeflected")) + { + switch(weaponname[14]) + { + case 'a': + reflectindex = GetWeaponIndex("deflect_arrow"); + case 'f': + reflectindex = GetWeaponIndex("deflect_flare"); + case 'p': + if(weaponname[19] == 0) // we aren't a _remote + reflectindex = GetWeaponIndex("deflect_promode"); + case 'r': + reflectindex = GetWeaponIndex("deflect_rocket"); + } + } + } + if(reflectindex > -1) + return reflectindex; + if(unlockable && client > 0) + { + new slot = 0; + if(playerClass[client] == TFClass_DemoMan) + slot = 1; + new itemindex = playerLoadout[client][slot][0]; + switch(itemindex) // Hell of a lot easier than a set of ifs. <_< + { + case 36, 41, 45, 61, 127, 130: + index++; + } + } + return index; + } + else + return -1; +} + +DumpHeals(client, String:addProp[] = "") +{ + new curHeals = GetEntProp(client, Prop_Send, "m_iHealPoints"); + new lifeHeals = curHeals - healPoints[client]; + if(lifeHeals > 0) + { + decl String:szProperties[32]; + Format(szProperties, sizeof(szProperties), " (healing \"%d\")%s", lifeHeals, addProp); + LogPlayerEvent(client, "triggered", "healed", _, szProperties); + } + healPoints[client] = curHeals; +} + +DumpAllWeaponStats() +{ + for(new i = 1; i <= MaxClients; i++) + DumpWeaponStats(i); +} + +DumpWeaponStats(client) +{ + if(IsClientInGame(client)) + { + decl String:player_authid[64]; + if(!GetClientAuthString(client, player_authid, sizeof(player_authid))) + strcopy(player_authid, sizeof(player_authid), "UNKNOWN"); + new player_team = GetClientTeam(client); + new player_userid = GetClientUserId(client); + for (new i = 0; i < MAX_LOG_WEAPONS; i++) + if(weaponStats[client][i][LOG_SHOTS] > 0 || weaponStats[client][i][LOG_DEATHS] > 0) + LogToGame("\"%N<%d><%s><%s>\" triggered \"weaponstats\" (weapon \"%s\") (shots \"%d\") (hits \"%d\") (kills \"%d\") (headshots \"%d\") (tks \"%d\") (damage \"%d\") (deaths \"%d\")", client, player_userid, player_authid, g_team_list[player_team], weaponList[i], weaponStats[client][i][LOG_SHOTS], weaponStats[client][i][LOG_HITS], weaponStats[client][i][LOG_KILLS], weaponStats[client][i][LOG_HEADSHOTS], weaponStats[client][i][LOG_TEAMKILLS], weaponStats[client][i][LOG_DAMAGE], weaponStats[client][i][LOG_DEATHS]); + } + ResetWeaponStats(client); +} + +ResetWeaponStats(client) +{ + for(new i = 0; i < MAX_LOG_WEAPONS; i++) + weaponStats[client][i] = {0,0,0,0,0,0,0}; +} + +public OnConVarActionsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:newval = GetConVarBool(cvar_actions); + if(newval != b_actions) + { + if(newval) + { + HookEvent("player_escort_score", Event_PlayerEscortScore); + HookEvent("player_stealsandvich", Event_PlayerStealsandvich); + HookEvent("player_stunned", Event_PlayerStunned); + HookEvent("deploy_buff_banner", Event_DeployBuffBanner); + HookEvent("medic_defended", Event_MedicDefended); + HookEvent("rocket_jump", Event_RocketJump); + HookEvent("rocket_jump_landed", Event_JumpLanded); + HookEvent("sticky_jump", Event_StickyJump); + HookEvent("sticky_jump_landed", Event_JumpLanded); + HookEvent("object_deflected", Event_ObjectDeflected); + HookUserMessage(GetUserMessageId("PlayerJarated"), Event_PlayerJarated); + HookUserMessage(GetUserMessageId("PlayerShieldBlocked"), Event_PlayerShieldBlocked); + } + else + { + UnhookEvent("player_escort_score", Event_PlayerEscortScore); + UnhookEvent("player_stealsandvich", Event_PlayerStealsandvich); + UnhookEvent("player_stunned", Event_PlayerStunned); + UnhookEvent("deploy_buff_banner", Event_DeployBuffBanner); + UnhookEvent("medic_defended", Event_MedicDefended); + UnhookEvent("rocket_jump", Event_RocketJump); + UnhookEvent("rocket_jump_landed", Event_JumpLanded); + UnhookEvent("sticky_jump", Event_StickyJump); + UnhookEvent("sticky_jump_landed", Event_JumpLanded); + UnhookEvent("object_deflected", Event_ObjectDeflected); + UnhookUserMessage(GetUserMessageId("PlayerJarated"), Event_PlayerJarated); + UnhookUserMessage(GetUserMessageId("PlayerShieldBlocked"), Event_PlayerShieldBlocked); + for(new i = 1; i <= MaxClients; i++) + jumpStatus[i] = 0; + } + b_actions = newval; + } +} + + +public OnConVarTeleportsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:newval = GetConVarBool(cvar_teleports); + if(newval != b_teleports) + { + if(newval) + HookEvent("player_teleported", Event_PlayerTeleported); + else + UnhookEvent("player_teleported", Event_PlayerTeleported); + b_teleports = newval; + } +} + +public OnConVarTeleportsAgainChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + b_teleports_again = GetConVarBool(cvar_teleports_again); +} + +public OnConVarHeadshotsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + b_headshots = GetConVarBool(cvar_headshots); +} + +public OnConVarBackstabsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + b_backstabs = GetConVarBool(cvar_backstabs); +} + +public OnConVarSandvichChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:newval = GetConVarBool(cvar_sandvich); + if(newval != b_sandvich) + { + if(newval) + AddNormalSoundHook(SoundHook); + else + RemoveNormalSoundHook(SoundHook); + b_sandvich = newval; + } +} + +public OnConVarFireChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + b_fire = GetConVarBool(cvar_fire); +} + +public OnConVarStatsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:newval = GetConVarBool(cvar_crits) && GetConVarBool(cvar_wstats); + if(newval != b_wstats) + { + if(!newval) + { + DumpAllWeaponStats(); + } + b_wstats = newval; + } +} + +public OnConVarHealsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:newval = GetConVarBool(cvar_heals); + if(newval != b_heals) + { + if(newval) + { + HookEvent("medic_death", Event_MedicDeath); + for(new i = 1; i <= MaxClients; i++) + if(IsClientInGame(i)) + healPoints[i] = GetEntProp(i, Prop_Send, "m_iHealPoints"); + } + else + { + UnhookEvent("medic_death", Event_MedicDeath); + } + b_heals = newval; + } +} + +public OnConVarRolelogfixChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:newval = GetConVarBool(cvar_rolelogfix); + if(newval != b_rolelogfix) + { + if(newval) + HookEvent("player_changeclass", Event_PlayerChangeclassPre, EventHookMode_Pre); + else + UnhookEvent("player_changeclass", Event_PlayerChangeclassPre, EventHookMode_Pre); + b_rolelogfix = newval; + } +} + +public OnConVarObjlogfixChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:newval = GetConVarBool(cvar_objlogfix); + if(newval != b_objlogfix) + { + if(newval) + HookEvent("object_removed", Event_ObjectRemoved); + else + UnhookEvent("object_removed", Event_ObjectRemoved); + b_objlogfix = newval; + } +} \ No newline at end of file diff --git a/sourcemod/scripting/superlogs-zps.sp b/sourcemod/scripting/superlogs-zps.sp new file mode 100644 index 0000000..316ec8e --- /dev/null +++ b/sourcemod/scripting/superlogs-zps.sp @@ -0,0 +1,325 @@ +/** + * HLstatsX Community Edition - SourceMod plugin to generate advanced weapon logging + * http://www.hlxcommunity.com + * Copyright (C) 2009-2010 Nicholas Hastings (psychonic) + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma semicolon 1 + +#include +#include + +#include + +#define NAME "SuperLogs: ZPS" +#define VERSION "1.1.2" + +#define MAX_LOG_WEAPONS 11 +#define MAX_WEAPON_LEN 12 +#define PREFIX_LEN 7 + + +new g_weapon_stats[MAXPLAYERS+1][MAX_LOG_WEAPONS][15]; +new const String:g_weapon_list[MAX_LOG_WEAPONS][MAX_WEAPON_LEN] = { + "870", + "revolver", + "ak47", + "usp", + "glock18c", + "glock", + "mp5", + "m4", + "supershorty", + "winchester", + "ppk" + }; + +new Handle:g_cvar_headshots = INVALID_HANDLE; +new Handle:g_cvar_locations = INVALID_HANDLE; +new Handle:g_cvar_ktraj = INVALID_HANDLE; + +new bool:g_logheadshots = true; +new bool:g_loglocations = true; +new bool:g_logktraj = true; + +new g_iNextHitgroup[MAXPLAYERS+1]; + +#include +#include + + +public Plugin:myinfo = { + name = NAME, + author = "psychonic", + description = "Advanced logging for ZPS. Generates auxilary logging for use with log parsers such as HLstatsX and Psychostats", + version = VERSION, + url = "http://www.hlxcommunity.com" +}; + +#if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 +public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) +#else +public bool:AskPluginLoad(Handle:myself, bool:late, String:error[], err_max) +#endif +{ + new String:szGameDesc[64]; + GetGameDescription(szGameDesc, sizeof(szGameDesc), true); + if (StrContains(szGameDesc, "ZPS", false) == -1) + { + new String:szGameDir[64]; + GetGameFolderName(szGameDir, sizeof(szGameDir)); + if (StrContains(szGameDir, "zps", false) == -1) + { + strcopy(error, err_max, "This plugin is only supported on Zombie Panic: Source"); + #if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 + return APLRes_Failure; + #else + return false; + #endif + } + } +#if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 3 + return APLRes_Success; +#else + return true; +#endif +} + + +public OnPluginStart() +{ + CreatePopulateWeaponTrie(); + + g_cvar_headshots = CreateConVar("superlogs_headshots", "1", "Enable logging of headshot player action (default on)", 0, true, 0.0, true, 1.0); + g_cvar_locations = CreateConVar("superlogs_locations", "1", "Enable logging of location on player death (default on)", 0, true, 0.0, true, 1.0); + g_cvar_ktraj = CreateConVar("superlogs_ktraj", "0", "Enable Psychostats \"KTRAJ\" logging (default off)", 0, true, 0.0, true, 1.0); + HookConVarChange(g_cvar_headshots, OnCvarHeadshotsChange); + HookConVarChange(g_cvar_locations, OnCvarLocationsChange); + HookConVarChange(g_cvar_ktraj, OnCvarKtrajChange); + CreateConVar("superlogs_zps_version", VERSION, NAME, FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY); + + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + HookEvent("player_death", Event_PlayerDeath); + HookEvent("player_spawn", Event_PlayerSpawn); + HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy); + HookEvent("player_disconnect", Event_PlayerDisconnect, EventHookMode_Pre); + + CreateTimer(1.0, LogMap); +} + +public OnAllPluginsLoaded() +{ + if (GetExtensionFileStatus("sdkhooks.ext") != 1) + { + SetFailState("SDK Hooks v1.3 or higher is required for SuperLogs: ZPS"); + } + for (new i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i)) + { + SDKHook(i, SDKHook_FireBulletsPost, OnFireBullets); + SDKHook(i, SDKHook_TraceAttackPost, OnTraceAttack); + SDKHook(i, SDKHook_OnTakeDamagePost, OnTakeDamage); + } + } +} + +public OnMapStart() +{ + GetTeams(); +} + +public OnClientPutInServer(client) +{ + SDKHook(client, SDKHook_FireBulletsPost, OnFireBullets); + SDKHook(client, SDKHook_TraceAttackPost, OnTraceAttack); + SDKHook(client, SDKHook_OnTakeDamagePost, OnTakeDamage); + reset_player_stats(client); +} + +public OnFireBullets(attacker, shots, String:weaponname[]) +{ + if (attacker > 0 && attacker <= MaxClients) + { + new weapon_index = get_weapon_index(weaponname[PREFIX_LEN]); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_SHOTS]++; + } + } +} + +public OnTraceAttack(victim, attacker, inflictor, Float:damage, damagetype, ammotype, hitbox, hitgroup) +{ + if (hitgroup > 0 && attacker > 0 && attacker <= MaxClients && victim > 0 && victim <= MaxClients) + { + g_iNextHitgroup[victim] = hitgroup; + } +} + +public OnTakeDamage(victim, attacker, inflictor, Float:damage, damagetype) +{ + if (attacker > 0 && attacker <= MaxClients && victim > 0 && victim <= MaxClients) + { + new hitgroup = g_iNextHitgroup[victim]; + if (hitgroup < 8) + { + hitgroup += LOG_HIT_OFFSET; + } + new bool:headshot = (GetClientHealth(victim) <= 0 && hitgroup == HITGROUP_HEAD); + + decl String: weapon[MAX_WEAPON_LEN + PREFIX_LEN]; + GetClientWeapon(attacker, weapon, sizeof(weapon)); + new weapon_index = get_weapon_index(weapon[PREFIX_LEN]); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HITS]++; + g_weapon_stats[attacker][weapon_index][LOG_HIT_DAMAGE] += RoundToNearest(damage); + if (headshot) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_HEADSHOTS]++; + } + } + g_iNextHitgroup[victim] = 0; + } +} + + +public Action:Event_PlayerDeathPre(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID who died + // "attacker" "short" // user ID who killed + // "weapon" "string" // weapon name killer used + + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + + if (g_loglocations) + { + LogKillLoc(attacker, victim); + } + if (g_logheadshots && g_iNextHitgroup[victim] == HITGROUP_HEAD) + { + LogPlayerEvent(attacker, "triggered", "headshot"); + } + + return Plugin_Continue; +} + +public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast) +{ + // this extents the original player_death by a new fields + // "userid" "short" // user ID who died + // "attacker" "short" // user ID who killed + // "weapon" "string" // weapon name killer used + + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + decl String: weapon[MAX_WEAPON_LEN]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + + if (victim > 0 && attacker > 0) + { + new weapon_index = get_weapon_index(weapon); + if (weapon_index > -1) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_KILLS]++; + g_weapon_stats[victim][weapon_index][LOG_HIT_DEATHS]++; + if (GetClientTeam(attacker) == GetClientTeam(victim)) + { + g_weapon_stats[attacker][weapon_index][LOG_HIT_TEAMKILLS]++; + } + } + dump_player_stats(victim); + } + if (g_logktraj) + { + LogPSKillTraj(attacker, victim, weapon); + } +} + +public Event_PlayerSpawn(Handle:event, const String:name[], bool:dontBroadcast) +{ + // "userid" "short" // user ID on server + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + reset_player_stats(client); + } +} + +public Event_RoundEnd(Handle:event, const String:name[], bool:dontBroadcast) +{ + WstatsDumpAll(); +} + +public Action:Event_PlayerDisconnect(Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + OnPlayerDisconnect(client); + return Plugin_Continue; +} + + +public Action:LogMap(Handle:timer) +{ + // Called 1 second after OnPluginStart since srcds does not log the first map loaded. Idea from Stormtrooper's "mapfix.sp" for psychostats + LogMapLoad(); +} + +public OnCvarHeadshotsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_logheadshots; + g_logheadshots = GetConVarBool(g_cvar_headshots); + + if (old_value != g_logheadshots) + { + if (g_logheadshots && !g_loglocations) + { + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + else if (!g_loglocations) + { + UnhookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + } +} + +public OnCvarLocationsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + new bool:old_value = g_loglocations; + g_loglocations = GetConVarBool(g_cvar_locations); + + if (old_value != g_loglocations) + { + if (g_loglocations && !g_logheadshots) + { + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + else if (!g_logheadshots) + { + UnhookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + } + } +} + +public OnCvarKtrajChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + g_logktraj = GetConVarBool(g_cvar_ktraj); +} \ No newline at end of file