diff --git a/plugins/activitymonitor.smx b/plugins/activitymonitor.smx index 5fe8f71..588401a 100644 Binary files a/plugins/activitymonitor.smx and b/plugins/activitymonitor.smx differ diff --git a/plugins/globalbans.smx b/plugins/globalbans.smx index 26ce3cc..3674f75 100644 Binary files a/plugins/globalbans.smx and b/plugins/globalbans.smx differ diff --git a/plugins/l4d2_TKStopper.smx b/plugins/l4d2_TKStopper.smx index 3da0a5a..e42e9f9 100644 Binary files a/plugins/l4d2_TKStopper.smx and b/plugins/l4d2_TKStopper.smx differ diff --git a/plugins/l4d2_extraplayeritems.smx b/plugins/l4d2_extraplayeritems.smx index adb7f37..5fc0cf4 100644 Binary files a/plugins/l4d2_extraplayeritems.smx and b/plugins/l4d2_extraplayeritems.smx differ diff --git a/plugins/l4d2_feedthetrolls.smx b/plugins/l4d2_feedthetrolls.smx index 24ddb45..92c1260 100644 Binary files a/plugins/l4d2_feedthetrolls.smx and b/plugins/l4d2_feedthetrolls.smx differ diff --git a/plugins/l4d2_tank_priority.smx b/plugins/l4d2_tank_priority.smx index 69c4cf6..df3f8dc 100644 Binary files a/plugins/l4d2_tank_priority.smx and b/plugins/l4d2_tank_priority.smx differ diff --git a/scripting/L4D2FFKickProtection.sp b/scripting/L4D2FFKickProtection.sp index a7d97ac..93438b2 100644 --- a/scripting/L4D2FFKickProtection.sp +++ b/scripting/L4D2FFKickProtection.sp @@ -55,8 +55,14 @@ public void Event_PlayerTeam(Event event, const char[] name, bool dontBroadcast) } } } +/* +Dropped BabybackRibs from server (Disconnect by user.) L 02/16/2022 - 10:38:53: [SM] Exception reported: No valid ban method flags specified L 02/16/2022 - 10:38:53: [SM] Blaming: L4D2FFKickProtection.smx L 02/16/2022 - 10:38:53: [SM] Call stack trace: L 02/16/2022 - 10:38:53: [SM] [0] BanClient L 02/16/2022 - 10:38:53: [SM] [1] Line 78, s:\Jackz\Documents\Sourcepawn\scripting\L4D2FFKickProtection.sp::VoteStart Potential vote being called Client "Andean Brain Surgeon" connected (70.112.126.195:27005). String Table dictionary for downloadables should be rebuilt, only found 39 of 51 strings in dictionary String Table dictionary for soundprecache */ public Action VoteStart(int client, const char[] command, int argc) { + if(!IsClientInGame(client)) { + PrintToServer("Preventing vote from user not in game: %N", client); + return Plugin_Handled; + } if(GetClientCount(true) == 0 || client == 0) return Plugin_Handled; //prevent votes while server is empty or if server tries calling vote if(argc >= 1) { static char issue[32]; @@ -67,7 +73,7 @@ public Action VoteStart(int client, const char[] command, int argc) { static char option[32]; GetCmdArg(2, option, sizeof(option)); - if(strlen(option) < 1) { //empty userid/console can't call votes + if(strlen(option) > 1) { //empty userid/console can't call votes int target = GetClientOfUserId(StringToInt(option)); if(target == 0) return Plugin_Continue; //invalid, pass it through AdminId callerAdmin = GetUserAdmin(client); diff --git a/scripting/activitymonitor.sp b/scripting/activitymonitor.sp index 6cd7e1b..9688c53 100644 --- a/scripting/activitymonitor.sp +++ b/scripting/activitymonitor.sp @@ -39,6 +39,8 @@ static EngineVersion g_Game; //L4d2 Specific static char L4D2_ZDifficulty[16]; +//Generic +static char currentGamemode[32]; public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) { @@ -58,7 +60,7 @@ public void OnPluginStart() { } hLogCvarChanges = CreateConVar("sm_activitymonitor_log_cvar", "0", "Should this plugin log cvar changes (when using sm_cvar from console)"); - ConVar hServerID = CreateConVar("sm_activitymonitor_id", "", "The name to use for the 'server' column"); + ConVar hServerID = CreateConVar("sm_activitymonitor_id", "", "The name to use for the 'server' column", FCVAR_DONTRECORD); hServerID.GetString(serverID, sizeof(serverID)); hServerID.AddChangeHook(CVAR_ServerIDChanged); @@ -71,31 +73,49 @@ public void OnPluginStart() { zDifficulty.GetString(L4D2_ZDifficulty, sizeof(L4D2_ZDifficulty)); CVAR_DifficultyChanged(zDifficulty, "", L4D2_ZDifficulty); zDifficulty.AddChangeHook(CVAR_DifficultyChanged); + + zDifficulty.GetString(L4D2_ZDifficulty, sizeof(L4D2_ZDifficulty)); + CVAR_DifficultyChanged(zDifficulty, "", L4D2_ZDifficulty); + zDifficulty.AddChangeHook(CVAR_DifficultyChanged); } + ConVar mpGamemode = FindConVar("mp_gamemode"); + if(mpGamemode != null) { + mpGamemode.GetString(currentGamemode, sizeof(currentGamemode)); + mpGamemode.AddChangeHook(CVAR_GamemodeChanged); + } + + if(!lateLoaded) { AddLog("INFO", "", "", "Server has started up"); } pushTimer = CreateTimer(60.0, Timer_PushLogs, _, TIMER_REPEAT); - // AutoExecConfig(true, "activitymonitor"); } +public void OnPluginEnd() { + TriggerTimer(pushTimer, true); +} + public void OnMapStart() { static char curMap[64]; GetCurrentMap(curMap, sizeof(curMap)); if(!StrEqual(lastMap, curMap)) { strcopy(lastMap, sizeof(lastMap), curMap); if(g_Game == Engine_Left4Dead2 || g_Game == Engine_Left4Dead) - Format(curMap, sizeof(curMap), "Map changed to %s (%s)", curMap, L4D2_ZDifficulty); + Format(curMap, sizeof(curMap), "Map changed to %s (%s %s)", curMap, L4D2_ZDifficulty, currentGamemode); else - Format(curMap, sizeof(curMap), "Map changed to %s", curMap); + Format(curMap, sizeof(curMap), "Map changed to %s (%s)", curMap, currentGamemode); AddLog("INFO", "", "", curMap); } TriggerTimer(pushTimer, true); } +public void CVAR_GamemodeChanged(ConVar convar, const char[] oldValue, const char[] newValue) { + strcopy(currentGamemode, sizeof(currentGamemode), newValue); +} + public void CVAR_ServerIDChanged(ConVar convar, const char[] oldValue, const char[] newValue) { strcopy(serverID, sizeof(serverID), newValue); } @@ -131,6 +151,7 @@ public Action Timer_PushLogs(Handle h) { static char query[1024]; static Log log; int length = logs.Length; + Transaction transaction = new Transaction(); if(length > 0) { for(int i = 0; i < length; i++) { logs.GetArray(i, log, sizeof(log)); @@ -142,26 +163,30 @@ public Action Timer_PushLogs(Handle h) { log.targetSteamID, log.message ); - g_db.Query(SQL_Callback, query, _, DBPrio_Low); + // g_db.Query(DB_PushLogsCB, query, _, DBPrio_Low); + transaction.AddQuery(query); } - // Incase a new item was added while pushing, don't clear it: logs.Resize(logs.Length - length); } + g_db.Execute(transaction, _, SQL_TransactionFailed, length, DBPrio_Low); } -public void SQL_Callback(Database db, DBResultSet results, const char[] error, any data) { +public void DB_PushLogsCB(Database db, DBResultSet results, const char[] error, any data) { if(results == null) PrintToServer("[ACTM] Log error: %s", error); } +public void SQL_TransactionFailed(Database db, any data, int numQueries, const char[] error, int failIndex, any[] queryData) { + PrintToServer("[ActivityMonitor] Push failure: %s at query %d/%d", error, failIndex, numQueries); +} public void Event_Connection(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); if(client > 0 && !IsFakeClient(client)) { static char clientName[32]; - GetClientAuthId(client, AuthId_Steam2, clientName, sizeof(clientName)); - if((name[7] == 'f')) { - //connection - AddLog("JOIN", clientName, "", ""); - } else { - AddLog("QUIT", clientName, "", ""); + if(GetClientAuthId(client, AuthId_Steam2, clientName, sizeof(clientName))) { + if(name[7] == 'f') { + AddLog("JOIN", clientName, "", ""); + } else { + AddLog("QUIT", clientName, "", ""); + } } } } @@ -198,7 +223,7 @@ public void Event_L4D2_Death(Event event, const char[] name, bool dontBroadcast) if(IsFakeClient(victim)) GetClientName(victim, victimName, sizeof(victimName)); else GetClientAuthId(victim, AuthId_Steam2, victimName, sizeof(victimName)); - if(attacker > 0) { + if(attacker > 0 && attacker != victim) { if(IsFakeClient(attacker)) GetClientName(attacker, attackerName, sizeof(attackerName)); else GetClientAuthId(attacker, AuthId_Steam2, attackerName, sizeof(attackerName)); @@ -220,7 +245,7 @@ public void Event_L4D2_Incapped(Event event, const char[] name, bool dontBroadca if(IsFakeClient(victim)) GetClientName(victim, victimName, sizeof(victimName)); else GetClientAuthId(victim, AuthId_Steam2, victimName, sizeof(victimName)); - if(attacker > 0) { + if(attacker > 0 && attacker != victim) { if(IsFakeClient(attacker)) GetClientName(attacker, attackerName, sizeof(attackerName)); else GetClientAuthId(attacker, AuthId_Steam2, attackerName, sizeof(attackerName)); diff --git a/scripting/globalbans.sp b/scripting/globalbans.sp index f996629..f40a7d1 100644 --- a/scripting/globalbans.sp +++ b/scripting/globalbans.sp @@ -62,7 +62,7 @@ public void OnClientAuthorized(int client, const char[] auth) { if(!StrEqual(auth, "BOT", true)) { static char query[256], ip[32]; GetClientIP(client, ip, sizeof(ip)); - Format(query, sizeof(query), "SELECT `reason`, `steamid`, `expired` FROM `bans` WHERE `steamid` LIKE 'STEAM_%:%:%s' OR ip = '?'", auth[10], ip); + g_db.Format(query, sizeof(query), "SELECT `reason`, `steamid`, `expired` FROM `bans` WHERE `steamid` LIKE 'STEAM_%%:%%:%s' OR ip = '%s'", auth[10], ip); g_db.Query(DB_OnConnectCheck, query, GetClientUserId(client), DBPrio_High); } } @@ -82,7 +82,7 @@ public Action OnBanIdentity(const char[] identity, int time, int flags, const ch }else{ Format(expiresDate, sizeof(expiresDate), "NULL"); } - Format(query, sizeof(query), "INSERT INTO bans" + g_db.Format(query, sizeof(query), "INSERT INTO bans" ..."(steamid, reason, expires, executor, ip_banned)" ..."VALUES ('%s', '%s', %s, '%s', 0)", identity, @@ -100,15 +100,19 @@ public Action OnBanIdentity(const char[] identity, int time, int flags, const ch public Action OnBanClient(int client, int time, int flags, const char[] reason, const char[] kick_message, const char[] command, any source) { char executor[32], identity[32], ip[32]; - if(source > 0 && source <= MaxClients) { - GetClientAuthId(source, AuthId_Steam2, executor, sizeof(executor)); + GetClientAuthId(client, AuthId_Steam2, identity, sizeof(identity)); + + DataPack pack; + if(source > 0 && source <= MaxClients && IsClientConnected(source) && GetClientAuthId(source, AuthId_Steam2, executor, sizeof(executor))) { + pack = new DataPack(); + pack.WriteString(identity); + pack.WriteCell(source); }else{ executor = "CONSOLE"; } if(GetUserAdmin(client) != INVALID_ADMIN_ID) return Plugin_Stop; - GetClientAuthId(client, AuthId_Steam2, identity, sizeof(identity)); GetClientIP(client, ip, sizeof(ip)); static char query[255]; @@ -118,7 +122,8 @@ public Action OnBanClient(int client, int time, int flags, const char[] reason, } else { Format(expiresDate, sizeof(expiresDate), "NULL"); } - Format(query, sizeof(query), "INSERT INTO bans" + + g_db.Format(query, sizeof(query), "INSERT INTO bans" ..."(steamid, ip, reason, expires, executor, ip_banned)" ..."VALUES ('%s', '%s', '%s', FROM_UNIXTIME(%s), '%s', 0)", identity, @@ -128,7 +133,7 @@ public Action OnBanClient(int client, int time, int flags, const char[] reason, executor ); - g_db.Query(DB_OnBanQuery, query); + g_db.Query(DB_OnBanQuery, query, pack); return Plugin_Continue; } @@ -154,19 +159,19 @@ public void DB_OnConnectCheck(Database db, DBResultSet results, const char[] err KickClient(client, "Could not authenticate at this time."); LogMessage("Could not connect to database to authorize user '%N' (#%d)", client, user); } - }else{ + } else { //No failure, check the data. - if(results.RowCount > 0 && client) { - results.FetchRow(); + if(client > 0 && results.FetchRow()) { //Is there a ban found? static char reason[128], steamid[64]; DBResult reasonResult; results.FetchString(1, steamid, sizeof(steamid)); - bool expired = results.FetchInt(2) == 1; - if(results.IsFieldNull(2)) { + bool expired = results.FetchInt(2) == 1; //Check if computed column 'expired' is true + if(results.IsFieldNull(2)) { //If expired null, delete i guess. lol DeleteBan(steamid); - }else{ + } else { results.FetchString(0, reason, sizeof(reason), reasonResult); if(!expired) { + LogMessage("%N is banned: %s", client, reason); if(hKickType.IntValue > 0) { if(reasonResult == DBVal_Data) KickClient(client, "You have been banned: %s", reason); @@ -179,7 +184,9 @@ public void DB_OnConnectCheck(Database db, DBResultSet results, const char[] err static char query[128]; g_db.Format(query, sizeof(query), "UPDATE bans SET times_tried=times_tried+1 WHERE steamid = '%s'", steamid); g_db.Query(DB_OnBanQuery, query); - }else{ + } else { + LogMessage("%N was previously banned: %s", client, reason); + // User was previously banned PrintChatToAdmins("%N has a previously expired ban of reason \"%s\"", client, reason); } } @@ -197,6 +204,19 @@ void DeleteBan(const char[] steamid) { public void DB_OnBanQuery(Database db, DBResultSet results, const char[] error, any data) { if(db == INVALID_HANDLE || results == null) { LogError("DB_OnBanQuery returned error: %s", error); + DataPack pack = data; + if(pack != null) { + pack.Reset(); + static char id[32]; + pack.ReadString(id, sizeof(id)); + int source = pack.ReadCell(); + + if(StrContains(error, "Duplicate entry") > 0) { + PrintToChat(source, "Could not ban \"%s\", as they were previously banned. Please edit the ban manually on the website (or yell at jackz).", id); + } else { + PrintToChat(source, "Could not ban \"%s\" due to an error: %s", id, error); + } + } } } diff --git a/scripting/include/feedthetrolls/commands.inc b/scripting/include/feedthetrolls/commands.inc index dbd3b11..c47a12f 100644 --- a/scripting/include/feedthetrolls/commands.inc +++ b/scripting/include/feedthetrolls/commands.inc @@ -250,6 +250,7 @@ public Action Command_ApplyUser(int client, int args) { ReplyToTargetError(client, target_count); return Plugin_Handled; } + if(args == 2) { static char key[32]; for(int i = 0; i < categories.Length; i++) { @@ -261,7 +262,10 @@ public Action Command_ApplyUser(int client, int args) { } } ReplyToCommand(client, "[FTT] Unknown category: '%s'", arg2); + } else if(args == 1) { + SetupCategoryMenu(client, GetClientUserId(target_list[0])); } + SilentMenuSelected[client] = false; SetupCategoryMenu(client, target_list[0]); } @@ -453,9 +457,8 @@ public Action Command_MarkPendingTroll(int client, int args) { menu.SetTitle("Choose a troll to mark"); static char userid[8], display[16]; for(int i = 1; i < MaxClients; i++) { - if(IsClientConnected(i) && IsClientInGame(i) && IsPlayerAlive(i) && GetClientTeam(i) == 2) { - AdminId admin = GetUserAdmin(i); - if(admin == INVALID_ADMIN_ID) { + if(IsClientConnected(i) && IsClientInGame(i) && IsPlayerAlive(i)) { + if(GetUserAdmin(i) == INVALID_ADMIN_ID) { Format(userid, sizeof(userid), "%d", GetClientUserId(i)); GetClientName(i, display, sizeof(display)); menu.AddItem(userid, display); @@ -487,9 +490,9 @@ public Action Command_MarkPendingTroll(int client, int args) { return Plugin_Handled; } int target = target_list[0]; - if(GetClientTeam(target) == 2) { + if (GetClientTeam(target) == 2) { ToggleMarkPlayer(client, target); - }else{ + } else { ReplyToCommand(client, "Player does not exist or is not a survivor."); } } diff --git a/scripting/include/feedthetrolls/menus.inc b/scripting/include/feedthetrolls/menus.inc index a3ffe42..0f3c290 100644 --- a/scripting/include/feedthetrolls/menus.inc +++ b/scripting/include/feedthetrolls/menus.inc @@ -322,8 +322,9 @@ public void StopItemGive(int client) { void SetupCategoryMenu(int client, int victimUserID) { Menu categoryMenu = new Menu(ChooseCategoryHandler); - static char category[64], id[8]; - Format(category, sizeof(category), "%N: Choose troll category", GetClientOfUserId(victimUserID)); + static char category[64], id[16]; + // Title with [ in name cause failure + Format(category, sizeof(category), "Choose troll category"); categoryMenu.SetTitle(category); Format(id, sizeof(id), "%d|-1", victimUserID); @@ -365,7 +366,7 @@ void ShowTrollCombosMenu(int client, int victimUserID) { void ShowTrollMenu(int client, bool isComboList) { Menu menu = isComboList ? new Menu(ChoosePlayerHandlerForCombos) : new Menu(ChoosePlayerHandler); menu.SetTitle("Choose a player to troll"); - static char userid[8], display[32]; + static char userid[8], display[64]; for(int i = 1; i <= MaxClients; i++) { if(IsClientConnected(i) && IsClientInGame(i) && IsPlayerAlive(i) && GetClientTeam(i) == 2) { IntToString(GetClientUserId(i), userid, sizeof(userid)); diff --git a/scripting/include/system2.inc b/scripting/include/system2.inc new file mode 100644 index 0000000..91d03e1 --- /dev/null +++ b/scripting/include/system2.inc @@ -0,0 +1,462 @@ +/** + * ----------------------------------------------------- + * File system2.inc + * Authors David Ordnung + * License GPLv3 + * Web http://dordnung.de + * ----------------------------------------------------- + * + * Copyright (C) 2013-2020 David Ordnung + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +#if defined _system2_included + #endinput +#endif + +#define _system2_included + +// Include request stuff +#include + + +/** + * Max length of a command when using formatted natives. + */ +#define CMD_MAX_LENGTH 2048 + + +/** + * A list of possible compression levels for the System2_Compress native. + */ +enum CompressLevel +{ + LEVEL_1, // Weekest + LEVEL_3, + LEVEL_5, + LEVEL_7, + LEVEL_9 // Strongest +} + + +/** + * A list of possible archive formats for the System2_Compress native. + */ +enum CompressArchive +{ + ARCHIVE_ZIP, + ARCHIVE_7Z, + ARCHIVE_GZIP, + ARCHIVE_BZIP2, + ARCHIVE_TAR +} + + +/** + * A list of possible operating systems for the System2_GetOS native. + */ +enum OS +{ + OS_UNKNOWN, // OS couldn't be determined + OS_WINDOWS, // Windows + OS_UNIX, // Linux / Unix + OS_MAC // MAC +} + + + +/** + * Called when finished with the System2_CopyFile native. + * + * @param success Whether copying was successful (will fail if couldn't open 'from' or 'to' file). + * @param from Path to file that was copied. + * @param to Path to the new copied file. + * @param data Data passed to the copy native. + * + * @noreturn + */ +typeset System2CopyCallback +{ + function void (bool success, const char[] from, const char[] to, any data); + function void (bool success, const char[] from, const char[] to); +}; + + +/** + * Called when finished with System2_ExecuteThreaded or System2_ExecuteFormattedThreaded native. + * The output will only be valid in the callback and will be destroyed afterwards. + * + * @param success Whether the execution was successful or not. + * This not means that the command itself was successful! + * Check the ExitStatus of the output for this. + * @param command The executed command. + * @param output Output of the execution. Is null if success is false. + * Can't be deleted, as it will be destroyed after the callback! + * @param data Data passed to the execution native. + * + * @noreturn + */ +typeset System2ExecuteCallback +{ + function void (bool success, const char[] command, System2ExecuteOutput output, any data); + function void (bool success, const char[] command, System2ExecuteOutput output); +}; + + + +/** + * Methodmap for the output of an execution. + */ +methodmap System2ExecuteOutput < Handle { + /** + * Retrieves the output of the command execution. + * + * @param output Buffer to store the output in. + * @param maxlength Maxlength of the output buffer. + * @param start Start byte to start reading from. + * You can use this to retrieve the output step by step. + * @param delimiter Delimiter until which the content should be retrieved. + * @param include Whether the delimiter should be included or not. + * + * @return Number of read bytes. + * @error Invalid Output. + */ + public native int GetOutput(char[] output, int maxlength, int start = 0, const char[] delimiter = "", bool include = true); + + property int Length { + /** + * Returns the length of the complete output. + * + * @return Length of the output. + * @error Invalid Output. + */ + public native get(); + } + + property int ExitStatus { + /** + * Returns the exit status of the execution. + * + * @return The exit status. + * @error Invalid Output. + */ + public native get(); + } +} + + + +/** + * Copies a file to another location. + * + * @param callback Callback function when finished with copying. + * @param from Path to the file to copy. + * @param to Path to the file to copy to (including filename). File will be replaced if it exists. + * @param data Additional data to pass to the callback. + * + * @noreturn + */ +native void System2_CopyFile(System2CopyCallback callback, const char[] from, const char[] to, any data = 0); + + +/** + * Checks whether 7-ZIP was found and is executable. + * + * @param execPath Buffer which will store the path to the 7-ZIP executable. + * Can be used for example when showing an error message. + * @param maxlength Maxlength of the buffer. + * @param force32Bit Whether to force using the 32 bit version of 7-ZIP, otherwise the appropriate version will be used. + * + * @return True if 7-ZIP executable could be found and is executable, otherwise false. + */ +native bool System2_Check7ZIP(char[] execPath, int maxlength, bool force32Bit = false); + +/** + * Compresses a file or folder to an archive. + * + * @param callback Callback function when finished with compressing. + * @param path Path to the file / folder to compress. + * @param archive Path to the archive file to compress to (including filename). + * @param archiveType Archive type to use. + * @param level Compress level to use. + * @param data Additional data to pass to the callback. + * @param force32Bit Whether to force using the 32 bit version of 7-ZIP, otherwise the appropriate version will be used. + * + * @return True if compress command could be fired, false when 7-ZIP executable couldn't be found or is not executable. + */ +native bool System2_Compress(System2ExecuteCallback callback, const char[] path, const char[] archive, CompressArchive archiveType = ARCHIVE_ZIP, CompressLevel level = LEVEL_9, any data = 0, bool force32Bit = false); + +/** + * Extracts a lot of archive types with 7zip. + * + * @param callback Callback function when finished with extracting. + * @param archive Path to the archive file to extract. + * @param extractDir Path to the directory to extract to. + * @param data Additional data to pass to the callback. + * @param force32Bit Whether to force using the 32 bit version of 7-ZIP, otherwise the appropriate version will be used. + * + * @return True if extract command could be fired, false when 7-ZIP executable couldn't be found or is not executable. + */ +native bool System2_Extract(System2ExecuteCallback callback, const char[] archive, const char[] extractDir, any data = 0, bool force32Bit = false); + + + +/** + * Executes a threaded system command. + * Hint: Append 2>&1 to your command to retrieve also output to stderr. + * + * @param callback Callback function when command was executed. + * @param command Command to execute. + * @param data Data to pass to the callback. + * + * @noreturn + */ +native void System2_ExecuteThreaded(System2ExecuteCallback callback, const char[] command, any data = 0); + +/** + * Executes a threaded system command with support for a formatted command. + * Note that the maxlength of the command is CMD_MAX_LENGTH, use System2_ExecuteThreaded if you need more. + * Hint: Append 2>&1 to your command to retrieve also output to stderr. + * + * @param callback Callback function when command was executed. + * @param data Data to pass to the callback. + * @param command Command string format. + * @param ... Command string arguments. + * + * @noreturn + */ +native void System2_ExecuteFormattedThreaded(System2ExecuteCallback callback, any data, const char[] command, any ...); + +/** + * Executes a non threaded system command. + * Hint: Append 2>&1 to your command to retrieve also output to stderr. + * + * @param output Buffer to store the output in. + * @param maxlength Maxlength of the output buffer. + * @param command Command to execute. + * + * @return True on success, otherwise false. + */ +native bool System2_Execute(char[] output, int maxlength, const char[] command); + +/** + * Executes a non threaded system command with support for a formatted command. + * Note that the maxlength of the command is CMD_MAX_LENGTH, use System2_Execute if you need more. + * Hint: Append 2>&1 to your command to retrieve also output to stderr. + * + * @param output Buffer to store the output in. + * @param maxlength Maxlength of the output buffer. + * @param command Command string format. + * @param ... Command string arguments. + * + * @return True on success, otherwise false. + */ +native bool System2_ExecuteFormatted(char[] output, int maxlength, const char[] command, any ...); + + + +/** + * Retrieves the absolute path to the gamedir of the current running game (e.g. /home/.../.../cstrike). + * You may need this when executing system commands. + * + * @param gamedir Buffer to store gamedir in. + * @param maxlength Maxlength of the buffer. + * + * @noreturn + */ +native void System2_GetGameDir(char[] gamedir, int maxlength); + +/** + * Returns the server's operating system. + * + * @return OS_UNKNOWN, OS_WINDOWS, OS_UNIX, OS_MAC. + */ +native OS System2_GetOS(); + + + +/** + * Retrieves the MD5 hex hash of a string. + * + * @param string String to retrieve the MD5 hash of. + * @param buffer Buffer to store MD5 hash in. + * @param maxlength Maxlength of the buffer. Should be greater or equal to 33 (32 MD5 + 1 terminator). + * + * @noreturn + */ +native void System2_GetStringMD5(const char[] str, char[] buffer, int maxlength); + +/** + * Retrieves the MD5 hex hash of a files content. + * + * @param file The path to the file. + * @param buffer Buffer to store MD5 hash in. + * @param maxlength Maxlength of the buffer. Should be greater or equal to 33 (32 MD5 + 1 terminator). + * + * @return True on success, false when file couldn't be opened. + */ +native bool System2_GetFileMD5(const char[] file, char[] buffer, int maxlength); + +/** + * Retrieves the CRC32 hex hash of a string. + * + * @param string The string to retrieve the CRC32 hash of. + * @param buffer Buffer to store CRC32 hash in. + * @param maxlength Maxlength of the buffer. Should be greater or equal to 9 (8 CRC32 + 1 terminator). + * + * @noreturn + */ +native void System2_GetStringCRC32(const char[] str, char[] buffer, int maxlength); + +/** + * Retrieves the CRC32 hex hash of a files content. + * + * @param file The path to the file. + * @param buffer Buffer to store CRC32 hash in. + * @param maxlength Maxlength of the buffer. Should be greater or equal to 9 (8 CRC32 + 1 terminator). + * + * @return True on success, false when file couldn't be opened. + */ +native bool System2_GetFileCRC32(const char[] file, char[] buffer, int maxlength); + + +// Include legacy stuff +#include + + +public Extension __ext_system2 = +{ + name = "System2", + file = "system2.ext", + + #if defined AUTOLOAD_EXTENSIONS + autoload = 1, + #else + autoload = 0, + #endif + + #if defined REQUIRE_EXTENSIONS + required = 1, + #else + required = 0, + #endif +}; + + +#if !defined REQUIRE_EXTENSIONS + public void __ext_system2_SetNTVOptional() + { + MarkNativeAsOptional("System2Request.SetURL"); + MarkNativeAsOptional("System2Request.GetURL"); + MarkNativeAsOptional("System2Request.SetPort"); + MarkNativeAsOptional("System2Request.GetPort"); + MarkNativeAsOptional("System2Request.SetOutputFile"); + MarkNativeAsOptional("System2Request.GetOutputFile"); + MarkNativeAsOptional("System2Request.SetVerifySSL"); + MarkNativeAsOptional("System2Request.GetVerifySSL"); + MarkNativeAsOptional("System2Request.SetProxy"); + MarkNativeAsOptional("System2Request.SetProxyAuthentication"); + MarkNativeAsOptional("System2Request.Timeout.get"); + MarkNativeAsOptional("System2Request.Timeout.set"); + MarkNativeAsOptional("System2Request.Any.get"); + MarkNativeAsOptional("System2Request.Any.set"); + + MarkNativeAsOptional("System2HTTPRequest.System2HTTPRequest"); + MarkNativeAsOptional("System2HTTPRequest.SetProgressCallback"); + MarkNativeAsOptional("System2HTTPRequest.SetData"); + MarkNativeAsOptional("System2HTTPRequest.GetData"); + MarkNativeAsOptional("System2HTTPRequest.SetHeader"); + MarkNativeAsOptional("System2HTTPRequest.GetHeader"); + MarkNativeAsOptional("System2HTTPRequest.GetHeaderName"); + MarkNativeAsOptional("System2HTTPRequest.SetUserAgent"); + MarkNativeAsOptional("System2HTTPRequest.SetBasicAuthentication"); + MarkNativeAsOptional("System2HTTPRequest.GET"); + MarkNativeAsOptional("System2HTTPRequest.POST"); + MarkNativeAsOptional("System2HTTPRequest.PUT"); + MarkNativeAsOptional("System2HTTPRequest.PATCH"); + MarkNativeAsOptional("System2HTTPRequest.DELETE"); + MarkNativeAsOptional("System2HTTPRequest.HEAD"); + MarkNativeAsOptional("System2HTTPRequest.FollowRedirects.get"); + MarkNativeAsOptional("System2HTTPRequest.FollowRedirects.set"); + MarkNativeAsOptional("System2HTTPRequest.Headers.get"); + + MarkNativeAsOptional("System2FTPRequest.System2FTPRequest"); + MarkNativeAsOptional("System2FTPRequest.SetProgressCallback"); + MarkNativeAsOptional("System2FTPRequest.SetAuthentication"); + MarkNativeAsOptional("System2FTPRequest.SetInputFile"); + MarkNativeAsOptional("System2FTPRequest.GetInputFile"); + MarkNativeAsOptional("System2FTPRequest.StartRequest"); + MarkNativeAsOptional("System2FTPRequest.AppendToFile.get"); + MarkNativeAsOptional("System2FTPRequest.AppendToFile.set"); + MarkNativeAsOptional("System2FTPRequest.CreateMissingDirs.get"); + MarkNativeAsOptional("System2FTPRequest.CreateMissingDirs.set"); + MarkNativeAsOptional("System2FTPRequest.ListFilenamesOnly.get"); + MarkNativeAsOptional("System2FTPRequest.ListFilenamesOnly.set"); + + MarkNativeAsOptional("System2Response.GetLastURL"); + MarkNativeAsOptional("System2Response.GetContent"); + MarkNativeAsOptional("System2Response.ContentLength.get"); + MarkNativeAsOptional("System2Response.StatusCode.get"); + MarkNativeAsOptional("System2Response.TotalTime.get"); + MarkNativeAsOptional("System2Response.DownloadSize.get"); + MarkNativeAsOptional("System2Response.UploadSize.get"); + MarkNativeAsOptional("System2Response.DownloadSpeed.get"); + MarkNativeAsOptional("System2Response.UploadSpeed.get"); + + MarkNativeAsOptional("System2HTTPResponse.GetContentType"); + MarkNativeAsOptional("System2HTTPResponse.GetHeader"); + MarkNativeAsOptional("System2HTTPResponse.GetHeaderName"); + MarkNativeAsOptional("System2HTTPResponse.GetHeadersCount"); + MarkNativeAsOptional("System2HTTPResponse.HTTPVersion.get"); + MarkNativeAsOptional("System2HTTPResponse.Headers.get"); + + MarkNativeAsOptional("System2_URLEncode"); + MarkNativeAsOptional("System2_URLDecode"); + + MarkNativeAsOptional("System2_CopyFile"); + + MarkNativeAsOptional("System2_Check7ZIP"); + MarkNativeAsOptional("System2_Compress"); + MarkNativeAsOptional("System2_Extract"); + + MarkNativeAsOptional("System2_ExecuteThreaded"); + MarkNativeAsOptional("System2_ExecuteFormattedThreaded"); + MarkNativeAsOptional("System2ExecuteOutput.GetOutput"); + MarkNativeAsOptional("System2ExecuteOutput.Length.get"); + MarkNativeAsOptional("System2ExecuteOutput.ExitStatus.get"); + + MarkNativeAsOptional("System2_Execute"); + MarkNativeAsOptional("System2_ExecuteFormatted"); + + MarkNativeAsOptional("System2_GetGameDir"); + MarkNativeAsOptional("System2_GetOS"); + + MarkNativeAsOptional("System2_GetStringMD5"); + MarkNativeAsOptional("System2_GetFileMD5"); + MarkNativeAsOptional("System2_GetStringCRC32"); + MarkNativeAsOptional("System2_GetFileCRC32"); + + // Deprecated v2 stuff + MarkNativeAsOptional("System2_GetPage"); + MarkNativeAsOptional("System2_DownloadFile"); + MarkNativeAsOptional("System2_DownloadFTPFile"); + MarkNativeAsOptional("System2_UploadFTPFile"); + MarkNativeAsOptional("System2_CompressFile"); + MarkNativeAsOptional("System2_ExtractArchive"); + MarkNativeAsOptional("System2_RunThreadCommand"); + MarkNativeAsOptional("System2_RunThreadCommandWithData"); + MarkNativeAsOptional("System2_RunCommand"); + } +#endif \ No newline at end of file diff --git a/scripting/l4d2_TKStopper.sp b/scripting/l4d2_TKStopper.sp index 103c3a7..7f33d44 100644 --- a/scripting/l4d2_TKStopper.sp +++ b/scripting/l4d2_TKStopper.sp @@ -11,15 +11,40 @@ #include #include -bool lateLoaded, isFinaleEnding; -bool isPlayerTroll[MAXPLAYERS+1], isImmune[MAXPLAYERS+1], isUnderAttack[MAXPLAYERS+1]; -int iJoinTime[MAXPLAYERS+1]; -int iIdleStartTime[MAXPLAYERS+1]; -int iLastFFTime[MAXPLAYERS+1]; -int iJumpAttempts[MAXPLAYERS+1]; +enum { + Immune_None, + Immune_TK = 1, + Immune_RFF = 2 +} -float playerTotalDamageFF[MAXPLAYERS+1]; -float autoFFScaleFactor[MAXPLAYERS+1]; +bool lateLoaded, isFinaleEnding; +// bool isPlayerTroll[MAXPLAYERS+1], isUnderAttack[MAXPLAYERS+1]; +// ImmunityFlag immunityFlags[MAXPLAYERS+1]; +// int iJoinTime[MAXPLAYERS+1]; +// int iIdleStartTime[MAXPLAYERS+1]; +// int iLastFFTime[MAXPLAYERS+1]; +// int iJumpAttempts[MAXPLAYERS+1]; + +// float playerTotalDamageFF[MAXPLAYERS+1]; +// float autoFFScaleFactor[MAXPLAYERS+1]; + +enum struct PlayerData { + int joinTime; + int idleStartTime; + int lastFFTime; + int jumpAttempts; + + float TKDamageBuffer; + float totalDamageFF; + float autoRFFScaleFactor; + + bool isTroll; + bool underAttack; + + int immunityFlags; +} + +PlayerData pData[MAXPLAYERS+1]; ConVar hForgivenessTime, hBanTime, hThreshold, hJoinTime, hTKAction, hSuicideAction, hSuicideLimit, hFFAutoScaleAmount, hFFAutoScaleForgivenessAmount, hFFAutoScaleMaxRatio, hFFAutoScaleIgnoreAdmins; @@ -43,17 +68,17 @@ public void OnPluginStart() { SetFailState("This plugin is for L4D/L4D2 only."); } - hForgivenessTime = CreateConVar("l4d2_tk_forgiveness_time", "15", "The minimum amount of time to pass (in seconds) where a player's previous accumulated FF is forgiven", FCVAR_NONE, true, 0.0); - hBanTime = CreateConVar("l4d2_tk_bantime", "120", "How long in minutes should a player be banned for? 0 for permanently", FCVAR_NONE, true, 0.0); + hForgivenessTime = CreateConVar("l4d2_tk_forgiveness_time", "15", "The minimum amount of time to pass (in seconds) where a player's previous accumulated teamkiller-detection FF is forgiven", FCVAR_NONE, true, 0.0); + hBanTime = CreateConVar("l4d2_tk_bantime", "60", "How long in minutes should a player be banned for? 0 for permanently", FCVAR_NONE, true, 0.0); hThreshold = CreateConVar("l4d2_tk_ban_ff_threshold", "75.0", "How much damage does a player need to do before being instantly banned", FCVAR_NONE, true, 0.0); - hJoinTime = CreateConVar("l4d2_tk_ban_join_time", "2", "Upto how many minutes should any new player be subjected to instant bans on any FF", FCVAR_NONE, true, 0.0); + hJoinTime = CreateConVar("l4d2_tk_ban_join_time", "2", "Upto how many minutes should any new player's ff be disabled? Used for jump attempts detection. also", FCVAR_NONE, true, 0.0); hTKAction = CreateConVar("l4d2_tk_action", "3", "How should the TK be punished?\n0 = No action (No message), 1 = Kick, 2 = Instant Ban, 3 = Ban on disconnect", FCVAR_NONE, true, 0.0, true, 3.0); hSuicideAction = CreateConVar("l4d2_suicide_action", "3", "How should a suicider be punished?\n0 = No action (No message), 1 = Kick, 2 = Instant Ban, 3 = Ban on disconnect", FCVAR_NONE, true, 0.0, true, 3.0); hSuicideLimit = CreateConVar("l4d2_suicide_limit", "1", "How many attempts does a new joined player have until action is taken for suiciding?", FCVAR_NONE, true, 0.0); // Reverse FF Auto Scale hFFAutoScaleAmount = CreateConVar("l4d2_tk_auto_ff_rate", "0.02", "The rate at which auto reverse-ff is scaled by.", FCVAR_NONE, true, 0.0); hFFAutoScaleMaxRatio = CreateConVar("l4d2_tk_auto_ff_max_ratio", "5.0", "The maximum amount that the reverse ff can go. 0.0 for unlimited", FCVAR_NONE, true, 0.0); - hFFAutoScaleForgivenessAmount = CreateConVar("l4d2_tk_auto_ff_forgive_rate", "0.03", "This amount times amount of minutes since last ff is removed from ff rate", FCVAR_NONE, true, 0.0); + hFFAutoScaleForgivenessAmount = CreateConVar("l4d2_tk_auto_ff_forgive_rate", "0.05", "This amount times amount of minutes since last ff is removed from ff rate", FCVAR_NONE, true, 0.0); hFFAutoScaleIgnoreAdmins = CreateConVar("l4d2_tk_auto_ff_ignore_admins", "1", "Should automatic reverse ff ignore admins? 0 = Admins are subjected\n1 = Admins are excempt", FCVAR_NONE, true, 0.0, true, 1.0); AutoExecConfig(true, "l4d2_tkstopper"); @@ -79,8 +104,9 @@ public void OnPluginStart() { HookEvent("bot_player_replace", Event_BotToPlayer); - RegAdminCmd("sm_ignore", Command_IgnorePlayer, ADMFLAG_KICK, "Makes a player immune for any anti trolling detection for a session"); + RegAdminCmd("sm_ignore", Command_IgnorePlayer, ADMFLAG_KICK, "Makes a player immune for any anti trolling detection or reverse-ff for a session"); RegAdminCmd("sm_tkinfo", Command_TKInfo, ADMFLAG_KICK, "Debug info for TKSTopper"); + RegAdminCmd("sm_review", Command_TKInfo, ADMFLAG_KICK, "Review FF info for a player"); if(lateLoaded) { for(int i = 1; i <= MaxClients; i++) { @@ -96,7 +122,7 @@ public void OnPluginStart() { public Action Event_ChargerCarry(Event event, const char[] name, bool dontBroadcast) { int victim = GetClientOfUserId(event.GetInt("victim")); if(victim) { - isUnderAttack[victim] = StrEqual(name, "charger_carry_start"); + pData[victim].underAttack = StrEqual(name, "charger_carry_start"); } return Plugin_Continue; } @@ -104,7 +130,7 @@ public Action Event_ChargerCarry(Event event, const char[] name, bool dontBroadc public Action Event_HunterPounce(Event event, const char[] name, bool dontBroadcast) { int victim = GetClientOfUserId(event.GetInt("victim")); if(victim) { - isUnderAttack[victim] = StrEqual(name, "lunge_pounce"); + pData[victim].underAttack = StrEqual(name, "lunge_pounce"); } return Plugin_Continue; } @@ -112,14 +138,14 @@ public Action Event_HunterPounce(Event event, const char[] name, bool dontBroadc public Action Event_SmokerChoke(Event event, const char[] name, bool dontBroadcast) { int victim = GetClientOfUserId(event.GetInt("victim")); if(victim) { - isUnderAttack[victim] = StrEqual(name, "choke_start"); + pData[victim].underAttack = StrEqual(name, "choke_start"); } return Plugin_Continue; } public Action Event_JockeyRide(Event event, const char[] name, bool dontBroadcast) { int victim = GetClientOfUserId(event.GetInt("victim")); if(victim) { - isUnderAttack[victim] = StrEqual(name, "jockey_ride"); + pData[victim].underAttack = StrEqual(name, "jockey_ride"); } return Plugin_Continue; } @@ -134,14 +160,14 @@ public Action Event_BotToPlayer(Event event, const char[] name, bool dontBroadca // If a player has been idle for over 600s (10 min), reset to them "just joined" // Purpose: Some trolls idle till end and then attack @ escape, or "gain trust" - if(GetTime() - iIdleStartTime[player] >= 600) { - iJoinTime[player] = GetTime(); + if(GetTime() - pData[player].idleStartTime >= 600) { + pData[player].joinTime = GetTime(); } return Plugin_Continue; } public Action Event_PlayerToBot(Event event, char[] name, bool dontBroadcast) { int player = GetClientOfUserId(event.GetInt("player")); - iIdleStartTime[player] = GetTime(); + pData[player].idleStartTime = GetTime(); return Plugin_Continue; } /////////////////////////////////////////////////////////////////////////////// @@ -150,8 +176,8 @@ public Action Event_PlayerToBot(Event event, char[] name, bool dontBroadcast) { public void Event_FinaleVehicleReady(Event event, const char[] name, bool dontBroadcast) { isFinaleEnding = true; for(int i = 1; i <= MaxClients; i++) { - if(isPlayerTroll[i] && IsClientConnected(i) && IsClientInGame(i)) { - PrintChatToAdmins("Note: %N is still marked as troll and will be banned after this game. Use /ignore to ignore them.", i); + if(pData[i].isTroll && IsClientConnected(i) && IsClientInGame(i)) { + PrintChatToAdmins("Note: %N is still marked as troll and will be banned after this game. Use \"/ignore tk\" to ignore them.", i); } } } @@ -161,38 +187,60 @@ public void OnMapEnd() { } public void OnClientPutInServer(int client) { - iJoinTime[client] = GetTime(); - iLastFFTime[client] = GetTime(); + pData[client].joinTime = pData[client].idleStartTime = GetTime(); SDKHook(client, SDKHook_OnTakeDamage, Event_OnTakeDamage); } +public void OnClientPostAdminCheck(int client) { + if(GetUserAdmin(client) != INVALID_ADMIN_ID) { + pData[client].immunityFlags = Immune_TK | Immune_RFF; + } +} + // Called on map changes, so only reset some variables: public void OnClientDisconnect(int client) { - playerTotalDamageFF[client] = 0.0; - isUnderAttack[client] = false; - iJumpAttempts[client] = 0; + pData[client].TKDamageBuffer = 0.0; + pData[client].jumpAttempts = 0; + pData[client].underAttack = false; } // Only clear things when they fully left on their own accord: public void Event_PlayerDisconnect(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); - if(client > 0 && isPlayerTroll[client]) { - BanClient(client, hBanTime.IntValue, BANFLAG_AUTO | BANFLAG_AUTHID, "Excessive FF", "Excessive Friendly Fire", "TKStopper"); + if(client > 0 && !IsFakeClient(client) && GetClientTeam(client) <= 2) { + if (pData[client].isTroll) { + BanClient(client, hBanTime.IntValue, BANFLAG_AUTO | BANFLAG_AUTHID, "Excessive FF", "Excessive Friendly Fire", "TKStopper"); + pData[client].isTroll = false; + pData[client].autoRFFScaleFactor = 0.0; + pData[client].totalDamageFF = 0.0; + } + + float minutesSinceiLastFFTime = (GetTime() - pData[client].lastFFTime) / 60.0; + float activeRate = pData[client].autoRFFScaleFactor - (minutesSinceiLastFFTime * hFFAutoScaleForgivenessAmount.FloatValue); + if(activeRate < 0.0) activeRate = 0.0; + PrintToConsoleAll("[TKStopper] FF Summary for %N:", client); + PrintToConsoleAll("\t\t%.2f TK-FF buffer (%.2f total ff) | %.3f (buf %f) rFF rate | lastff %.1f min ago | %d suicide jumps", + pData[client].TKDamageBuffer, + pData[client].totalDamageFF, + activeRate, + pData[client].autoRFFScaleFactor, + minutesSinceiLastFFTime, + pData[client].jumpAttempts + ); } - isPlayerTroll[client] = false; - autoFFScaleFactor[client] = 0.0; } public Action Event_OnTakeDamage(int victim, int& attacker, int& inflictor, float& damage, int& damagetype, int& weapon, float damageForce[3], float damagePosition[3]) { - if(damage > 0.0 && victim <= MaxClients && attacker <= MaxClients && attacker > 0 && victim > 0 && attacker != victim) { - if(GetClientTeam(victim) != GetClientTeam(attacker) || attacker == victim) return Plugin_Continue; + if(damage > 0.0 && victim <= MaxClients && attacker <= MaxClients && attacker > 0 && victim > 0) { + if(GetClientTeam(victim) != GetClientTeam(attacker) || attacker == victim) + return Plugin_Continue; else if(damagetype & DMG_BURN && IsFakeClient(attacker) && GetClientTeam(attacker) == 2) { // Ignore damage from fire caused by bots (players who left after causing fire) damage = 0.0; return Plugin_Changed; } // Otherwise if attacker was ignored or is a bot, stop here and let vanilla handle it - else if(isImmune[attacker] || IsFakeClient(attacker)) return Plugin_Continue; + else if(pData[attacker].immunityFlags & Immune_TK || IsFakeClient(attacker)) return Plugin_Continue; bool isAdmin = GetUserAdmin(attacker) != INVALID_ADMIN_ID; @@ -200,24 +248,23 @@ public Action Event_OnTakeDamage(int victim, int& attacker, int& inflictor, flo //if(IsFakeClient(victim) && !HasEntProp(attacker, Prop_Send, "m_humanSpectatorUserID") || GetEntProp(attacker, Prop_Send, "m_humanSpectatorUserID") == 0) return Plugin_Continue; // Stop all damage early if already marked as troll - if(isPlayerTroll[attacker]) { - SDKHooks_TakeDamage(attacker, attacker, attacker, autoFFScaleFactor[attacker] * damage); - + if(pData[attacker].isTroll) { + SDKHooks_TakeDamage(attacker, attacker, attacker, pData[attacker].autoRFFScaleFactor * damage); return Plugin_Stop; } // Allow vanilla-damage if being attacked by special (example, charger carry) - if(isUnderAttack[victim]) return Plugin_Continue; + if(pData[victim].underAttack) return Plugin_Continue; // Is damage not caused by fire or pipebombs? bool isDamageDirect = damagetype & (DMG_BLAST|DMG_BURN|DMG_BLAST_SURFACE) == 0; int time = GetTime(); // If is a fall within first 2 minutes, do appropiate action - if(!isAdmin && damagetype & DMG_FALL && attacker == victim && damage > 0.0 && time - iJoinTime[victim] <= hJoinTime.IntValue * 60) { - iJumpAttempts[victim]++; + if(!isAdmin && damagetype & DMG_FALL && attacker == victim && damage > 0.0 && time - pData[victim].joinTime <= hJoinTime.IntValue * 60) { + pData[victim].jumpAttempts++; float pos[3]; GetNearestPlayerPosition(victim, pos); PrintToConsoleAdmins("%N within join time (%d min), attempted to fall"); - if(iJumpAttempts[victim] > hSuicideLimit.IntValue) { + if(pData[victim].jumpAttempts > hSuicideLimit.IntValue) { if(hSuicideAction.IntValue == 1) { LogMessage("[NOTICE] Kicking %N for suicide attempts", victim, hBanTime.IntValue); NotifyAllAdmins("[Notice] Kicking %N for suicide attempts", victim, hBanTime.IntValue); @@ -228,60 +275,78 @@ public Action Event_OnTakeDamage(int victim, int& attacker, int& inflictor, flo BanClient(victim, hBanTime.IntValue, BANFLAG_AUTO | BANFLAG_AUTHID, "Suicide fall attempts", "Troll", "TKStopper"); } else if(hSuicideAction.IntValue == 3) { LogMessage("[NOTICE] %N will be banned for suicide attempts for %d minutes. ", victim, hBanTime.IntValue); - NotifyAllAdmins("[Notice] %N will be banned for suicide attempts for %d minutes. Use /ignore to make them immune.", victim, hBanTime.IntValue); - isPlayerTroll[victim] = true; + NotifyAllAdmins("[Notice] %N will be banned for suicide attempts for %d minutes. Use \"/ignore tk\" to make them immune.", victim, hBanTime.IntValue); + pData[victim].isTroll = true; } } } + if(attacker == victim) return Plugin_Continue; // Forgive player based on threshold, resetting accumlated damage - if(time - iLastFFTime[attacker] > hForgivenessTime.IntValue) { - playerTotalDamageFF[attacker] = 0.0; + if(time - pData[attacker].lastFFTime > hForgivenessTime.IntValue) { + pData[attacker].TKDamageBuffer = 0.0; } - playerTotalDamageFF[attacker] += damage; + pData[attacker].TKDamageBuffer += damage; + pData[attacker].totalDamageFF += damage; // Auto reverse ff logic - iLastFFTime[attacker] = time; - if(isDamageDirect && (!hFFAutoScaleIgnoreAdmins.BoolValue || !isAdmin)) { + int prevFFTime = pData[attacker].lastFFTime; + pData[attacker].lastFFTime = time; + // If not immune to RFF, damage is direct, _or admin shit_ + if(~pData[attacker].immunityFlags & Immune_RFF && isDamageDirect && (!hFFAutoScaleIgnoreAdmins.BoolValue || !isAdmin)) { // Decrement any forgiven ratio (computed on demand) - float minutesSinceiLastFFTime = (time - iLastFFTime[attacker]) / 60.0; - autoFFScaleFactor[attacker] -= minutesSinceiLastFFTime * hFFAutoScaleForgivenessAmount.FloatValue; - if(autoFFScaleFactor[attacker] < 0.0) { - autoFFScaleFactor[attacker] = 0.0; + float minutesSinceiLastFFTime = (time - pData[attacker].lastFFTime) / 60.0; + pData[attacker].autoRFFScaleFactor -= minutesSinceiLastFFTime * hFFAutoScaleForgivenessAmount.FloatValue; + if(pData[attacker].autoRFFScaleFactor < 0.0) { + pData[attacker].autoRFFScaleFactor = 0.0; } // Then calculate a new reverse ff ratio - autoFFScaleFactor[attacker] += hFFAutoScaleAmount.FloatValue * damage; - if(isPlayerTroll[attacker]) { - autoFFScaleFactor[attacker] *= 2; + pData[attacker].autoRFFScaleFactor += hFFAutoScaleAmount.FloatValue * damage; + if(pData[attacker].isTroll) { + pData[attacker].autoRFFScaleFactor *= 2; } - if(!isPlayerTroll[attacker] && hFFAutoScaleMaxRatio.FloatValue > 0.0 && autoFFScaleFactor[attacker] > hFFAutoScaleMaxRatio.FloatValue) { - autoFFScaleFactor[attacker] = hFFAutoScaleMaxRatio.FloatValue; + // Cap max damage only for non-trolls + if(!pData[attacker].isTroll && hFFAutoScaleMaxRatio.FloatValue > 0.0 && pData[attacker].autoRFFScaleFactor > hFFAutoScaleMaxRatio.FloatValue) { + pData[attacker].autoRFFScaleFactor = hFFAutoScaleMaxRatio.FloatValue; } } + pData[attacker].lastFFTime = time; // Check for excessive friendly fire damage in short timespan - if(!isAdmin && playerTotalDamageFF[attacker] > hThreshold.IntValue && !isFinaleEnding && isDamageDirect) { - LogAction(-1, attacker, "Excessive FF (%.2f HP) (%.2f RFF Rate)", playerTotalDamageFF[attacker], autoFFScaleFactor[attacker]); + // If not immune to TK, if over TK threshold, not when escaping, and direct damage + if(~pData[attacker].immunityFlags & Immune_TK && pData[attacker].TKDamageBuffer > hThreshold.IntValue && !isFinaleEnding && isDamageDirect) { + float diffJoinMin = (float(GetTime()) - float(pData[attacker].joinTime)) / 60.0; + float lastFFMin = (float(GetTime()) - float(prevFFTime)) / 60.0; + LogAction(-1, attacker, "Excessive FF (%.2f HP/%.2f total) (%.2f RFF Rate) (joined %.1fm ago) (%.1fmin last FF)", + pData[attacker].TKDamageBuffer, + pData[attacker].totalDamageFF, + pData[attacker].autoRFFScaleFactor, + diffJoinMin, + lastFFMin + ); if(hTKAction.IntValue == 1) { - LogMessage("[NOTICE] Kicking %N for excessive FF (%.2f HP) for %d minutes.", attacker, playerTotalDamageFF[attacker], hBanTime.IntValue); - NotifyAllAdmins("[Notice] Kicking %N for excessive FF (%.2f HP) for %d minutes.", attacker, playerTotalDamageFF[attacker], hBanTime.IntValue); + LogMessage("[TKStopper] Kicking %N for excessive FF (%.2f HP)", attacker, pData[attacker].TKDamageBuffer); + NotifyAllAdmins("[Notice] Kicking %N for excessive FF (%.2f HP)", attacker, pData[attacker].TKDamageBuffer); KickClient(attacker, "Excessive FF"); } else if(hTKAction.IntValue == 2) { - LogMessage("[NOTICE] Banning %N for excessive FF (%.2f HP) for %d minutes.", attacker, playerTotalDamageFF[attacker], hBanTime.IntValue); - NotifyAllAdmins("[Notice] Banning %N for excessive FF (%.2f HP) for %d minutes.", attacker, playerTotalDamageFF[attacker], hBanTime.IntValue); + LogMessage("[TKStopper] Banning %N for excessive FF (%.2f HP) for %d minutes.", attacker, pData[attacker].TKDamageBuffer, hBanTime.IntValue); + NotifyAllAdmins("[Notice] Banning %N for excessive FF (%.2f HP) for %d minutes.", attacker, pData[attacker].TKDamageBuffer, hBanTime.IntValue); BanClient(attacker, hBanTime.IntValue, BANFLAG_AUTO | BANFLAG_AUTHID, "Excessive FF", "Excessive Friendly Fire", "TKStopper"); } else if(hTKAction.IntValue == 3) { - LogMessage("[NOTICE] %N will be banned for FF on disconnect (%.2f HP) for %d minutes. ", attacker, playerTotalDamageFF[attacker], hBanTime.IntValue); - NotifyAllAdmins("[Notice] %N will be banned for FF on disconnect (%.2f HP) for %d minutes. Use /ignore to make them immune.", attacker, playerTotalDamageFF[attacker], hBanTime.IntValue); - isPlayerTroll[attacker] = true; + LogMessage("[TKStopper] %N will be banned for FF on disconnect (%.2f HP) for %d minutes. ", attacker, pData[attacker].TKDamageBuffer, hBanTime.IntValue); + NotifyAllAdmins("[Notice] %N will be banned for FF on disconnect (%.2f HP) for %d minutes. Use \"/ignore tk\" to make them immune.", attacker, pData[attacker].TKDamageBuffer, hBanTime.IntValue); + pData[attacker].isTroll = true; } damage = 0.0; return Plugin_Handled; } - + // Modify damages based on criteria - if(iJumpAttempts[victim] > 0 || L4D_IsInFirstCheckpoint(victim) || L4D_IsInLastCheckpoint(victim) || time - iJoinTime[attacker] <= hJoinTime.IntValue * 60) { + if(pData[victim].jumpAttempts > 0 + || L4D_IsInFirstCheckpoint(victim) || L4D_IsInLastCheckpoint(victim) + || time - pData[attacker].joinTime <= hJoinTime.IntValue * 60 + ) { /* If the amount of seconds since they joined is <= the minimum join time cvar (min) threshold or if the player is in a saferoom @@ -291,17 +356,19 @@ public Action Event_OnTakeDamage(int victim, int& attacker, int& inflictor, flo damage = 0.0; return Plugin_Handled; }else if(isFinaleEnding) { - // Keep admins immune if escape vehicle out - if(isAdmin) return Plugin_Continue; + // Keep admins immune if escape vehicle out, or if victim is a bot + if(isAdmin || IsFakeClient(victim)) return Plugin_Continue; SDKHooks_TakeDamage(attacker, attacker, attacker, damage * 2.0); damage = 0.0; return Plugin_Changed; - }else if(isDamageDirect) { // Ignore fire and propane damage, mistakes can happen + }else if(isDamageDirect && !isAdmin && pData[attacker].autoRFFScaleFactor > 0.3) { // Ignore fire and propane damage, mistakes can happen // Apply their reverse ff damage, and have victim take a decreasing amount - SDKHooks_TakeDamage(attacker, attacker, attacker, autoFFScaleFactor[attacker] * damage); - if(isPlayerTroll[attacker]) return Plugin_Stop; - if(autoFFScaleFactor[attacker] > 1.0) - damage /= autoFFScaleFactor[attacker]; + SDKHooks_TakeDamage(attacker, attacker, attacker, pData[attacker].autoRFFScaleFactor * damage); + if(pData[attacker].isTroll) return Plugin_Stop; + else if(pData[attacker].immunityFlags & Immune_RFF) return Plugin_Continue; + + if(pData[attacker].autoRFFScaleFactor > 1.0) + damage /= pData[attacker].autoRFFScaleFactor; else damage /= 2.0; return Plugin_Changed; @@ -314,25 +381,99 @@ public Action Event_OnTakeDamage(int victim, int& attacker, int& inflictor, flo public Action Command_TKInfo(int client, int args) { int time = GetTime(); - for(int i = 1; i <= MaxClients; i++) { - if(IsClientConnected(i) && IsClientInGame(i) && !IsFakeClient(i)) { - float minutesSinceiLastFFTime = (time - iLastFFTime[i]) / 60.0; - float activeRate = autoFFScaleFactor[i] - (minutesSinceiLastFFTime * hFFAutoScaleForgivenessAmount.FloatValue); - if(activeRate < 0.0) { - activeRate = 0.0; - } - ReplyToCommand(client, "%N: %f TK-FF buffer | %.3f (buf %f), reverse FF rate | last ff %.1f min ago | %d suicide jumps", i, playerTotalDamageFF[i], activeRate, autoFFScaleFactor[i], minutesSinceiLastFFTime, iJumpAttempts[i]); + if(args > 0) { + static char arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + static char target_name[MAX_TARGET_LENGTH]; + int target_list[1], target_count; + bool tn_is_ml; + + if ((target_count = ProcessTargetString( + arg1, + client, + target_list, + 1, + COMMAND_FILTER_NO_MULTI, + target_name, + sizeof(target_name), + tn_is_ml)) <= 0 + || target_count == 0) { + ReplyToTargetError(client, target_count); + return Plugin_Handled; + } + int target = target_list[0]; + ReplyToCommand(client, "FF Review for '%N':", target); + if(pData[target].isTroll) { + ReplyToCommand(client, "- will be banned on disconnect for TK -", target); + } + if(pData[target].immunityFlags == Immune_TK) { + ReplyToCommand(client, "Immunity: Teamkiller Detection", target); + } else if(pData[target].immunityFlags == Immune_RFF) { + ReplyToCommand(client, "Immunity: Auto reverse-ff", target); + } else if(view_as(pData[target].immunityFlags) > 0) { + ReplyToCommand(client, "Immunity: Teamkiller Detection, Auto reverse-ff", target); + } else { + ReplyToCommand(client, "Immunity: (none, use /ignore [immunity] to toggle)", target); + } + float minutesSinceiLastFFTime = (time - pData[target].lastFFTime) / 60.0; + float activeRate = pData[target].autoRFFScaleFactor - (minutesSinceiLastFFTime * hFFAutoScaleForgivenessAmount.FloatValue); + if(activeRate < 0.0) activeRate = 0.0; + + ReplyToCommand(client, "Total FF Damage: %.1f (%.1f min ago last ff)", pData[target].totalDamageFF, minutesSinceiLastFFTime); + if(~pData[target].immunityFlags & Immune_TK) + ReplyToCommand(client, "Recent FF (TKDetectBuff): %.1f", pData[target].TKDamageBuffer); + if(~pData[target].immunityFlags & Immune_RFF) + ReplyToCommand(client, "Auto Reverse-FF: %.1f return rate", activeRate); + ReplyToCommand(client, "Attempted suicide jumps: %d", pData[target].jumpAttempts); + } else { + for(int i = 1; i <= MaxClients; i++) { + if(IsClientConnected(i) && IsClientInGame(i) && !IsFakeClient(i)) { + float minutesSinceiLastFFTime = (time - pData[i].lastFFTime) / 60.0; + float activeRate = pData[i].autoRFFScaleFactor - (minutesSinceiLastFFTime * hFFAutoScaleForgivenessAmount.FloatValue); + if(activeRate < 0.0) { + activeRate = 0.0; + } + ReplyToCommand(client, "%20N: %.2f TK-FF buf (%.2f total) | %.3f (buf %f) rFF rate | lastff %.1f min ago | %d suicide jumps", + i, + pData[i].TKDamageBuffer, + pData[i].totalDamageFF, + activeRate, + pData[i].autoRFFScaleFactor, + minutesSinceiLastFFTime, + pData[i].jumpAttempts + ); + } } } return Plugin_Handled; } + public Action Command_IgnorePlayer(int client, int args) { - char arg1[32]; + char arg1[32], arg2[16]; GetCmdArg(1, arg1, sizeof(arg1)); + GetCmdArg(2, arg2, sizeof(arg2)); + + if(args < 2) { + ReplyToCommand(client, "Usage: sm_ignore "); + return Plugin_Handled; + } + + int flags = 0; + if(StrEqual(arg2, "tk") || StrEqual(arg2, "teamkill")) { + flags = Immune_TK; + } else if(StrEqual(arg2, "all") || StrEqual(arg2, "a")) { + flags = Immune_TK | Immune_RFF; + } else if(StrEqual(arg2, "reverseff") || StrEqual(arg2, "rff")) { + flags = Immune_RFF; + } else { + ReplyToCommand(client, "Usage: sm_ignore "); + return Plugin_Handled; + } char target_name[MAX_TARGET_LENGTH]; - int target_list[1], target_count; + int target_list[MAXPLAYERS+1], target_count; bool tn_is_ml; if ((target_count = ProcessTargetString( @@ -349,23 +490,38 @@ public Action Command_IgnorePlayer(int client, int args) { return Plugin_Handled; } - for(int i = 0; i < target_count; i++) { + for (int i = 0; i < target_count; i++) { int target = target_list[i]; - if(GetUserAdmin(target) != INVALID_ADMIN_ID) { + if (GetUserAdmin(target) != INVALID_ADMIN_ID) { ReplyToCommand(client, "%N is an admin and is already immune."); - }else{ - if(isImmune[target]) { + return Plugin_Handled; + } + + if (flags & Immune_TK) { + if (pData[target].immunityFlags & Immune_TK) { LogAction(client, target, "\"%L\" re-enabled teamkiller detection for \"%L\"", client, target); ShowActivity2(client, "[FTT] ", "%N has re-enabled teamkiller detection for %N", client, target); } else { LogAction(client, target, "\"%L\" ignored teamkiller detection for \"%L\"", client, target); ShowActivity2(client, "[FTT] ", "%N has ignored teamkiller detection for %N", client, target); } - isImmune[target] = !isImmune[target]; - } - isPlayerTroll[target] = false; - } + pData[target].immunityFlags ^= Immune_TK; + } + if (flags & Immune_RFF) { + if (pData[target].immunityFlags & Immune_RFF) { + LogAction(client, target, "\"%L\" re-enabled auto reverse friendly-fire for \"%L\"", client, target); + ShowActivity2(client, "[FTT] ", "%N has re-enabled auto reverse friendly-fire for %N", client, target); + } else { + LogAction(client, target, "\"%L\" disabled auto reverse friendly-fire for \"%L\"", client, target); + ShowActivity2(client, "[FTT] ", "%N has disabled auto reverse friendly-fire for %N", client, target); + pData[target].autoRFFScaleFactor = 0.0; + } + pData[target].immunityFlags ^= Immune_RFF; + } + + pData[target].isTroll = false; + } return Plugin_Handled; } @@ -390,8 +546,8 @@ stock bool GetNearestPlayerPosition(int client, float pos[3]) { return lowestID > 0; } +static char buffer[254]; stock void PrintChatToAdmins(const char[] format, any ...) { - char buffer[254]; VFormat(buffer, sizeof(buffer), format, 2); for(int i = 1; i <= MaxClients; i++) { if(IsClientConnected(i) && IsClientInGame(i)) { @@ -405,7 +561,6 @@ stock void PrintChatToAdmins(const char[] format, any ...) { } stock void PrintToConsoleAdmins(const char[] format, any ...) { - char buffer[254]; VFormat(buffer, sizeof(buffer), format, 2); for(int i = 1; i <= MaxClients; i++) { if(IsClientConnected(i) && IsClientInGame(i)) { diff --git a/scripting/l4d2_ai_minigun.sp b/scripting/l4d2_ai_minigun.sp index d017d8a..7788840 100644 --- a/scripting/l4d2_ai_minigun.sp +++ b/scripting/l4d2_ai_minigun.sp @@ -212,6 +212,7 @@ stock int SpawnSurvivor(const float vPos[3], const float vAng[3], const char[] m TeleportEntity(bot_client_id, vPos, NULL_VECTOR, NULL_VECTOR); SetEntityModel(bot_client_id, model); //set entity model to custom survivor model + CreateTimer(6.0, Timer_Move, bot_user_id); return bot_user_id; } void AvoidCharacter(int type, bool avoid) { diff --git a/scripting/l4d2_extraplayeritems.sp b/scripting/l4d2_extraplayeritems.sp index f606fd0..7584510 100644 --- a/scripting/l4d2_extraplayeritems.sp +++ b/scripting/l4d2_extraplayeritems.sp @@ -37,6 +37,7 @@ #include #include #include +#include #define L4D2_WEPUPGFLAG_NONE (0 << 0) #define L4D2_WEPUPGFLAG_INCENDIARY (1 << 0) @@ -48,6 +49,10 @@ #define TANK_CLASS_ID 8 +// configurable: +#define PLAYER_DROP_TIMEOUT_SECONDS 120000.0 + + /* 5+ Tank Improvement Every X amount of L4D2_OnChooseVictim() calls, if a player has done < Y amount since last call @@ -71,22 +76,59 @@ public Plugin myinfo = url = "" }; -static ConVar hExtraItemBasePercentage, hAddExtraKits, hMinPlayers, hUpdateMinPlayers, hMinPlayersSaferoomDoor, hSaferoomDoorWaitSeconds, hSaferoomDoorAutoOpen, hEPIHudState, hExtraFinaleTank, cvDropDisconnectTime; +static ConVar hExtraItemBasePercentage, hAddExtraKits, hMinPlayers, hUpdateMinPlayers, hMinPlayersSaferoomDoor, hSaferoomDoorWaitSeconds, hSaferoomDoorAutoOpen, hEPIHudState, hExtraFinaleTank, cvDropDisconnectTime, hSplitTankChance; static int extraKitsAmount, extraKitsStarted, abmExtraCount, firstSaferoomDoorEntity, playersLoadedIn, playerstoWaitFor; +static int currentChapter; static bool isCheckpointReached, isLateLoaded, firstGiven, isFailureRound; static ArrayList ammoPacks; static Handle updateHudTimer; static char gamemode[32]; +static bool allowTankSplit = true; + +enum State { + State_Empty, + State_PendingEmpty, + State_Active +} + enum struct PlayerData { bool itemGiven; //Is player being given an item (such that the next pickup event is ignored) bool isUnderAttack; //Is the player under attack (by any special) - bool active; + State state; +} + +enum struct PlayerInventory { + int timestamp; + bool isAlive; + + WeaponId itemID[6]; //int -> char? + bool lasers; + char meleeID[32]; + + int primaryHealth; + int tempHealth; + + char model[64]; + int survivorType; } PlayerData playerData[MAXPLAYERS+1]; +/* +TODO: +1. Save player inventory on: + a. Disconnect (saferoom disconnect too) + b. Periodically? +2. On new map join (OnClientPutInServer¿) check following item matches: + a. primary weapon + b. secondary weapon (excl melee) +If a || b != saved items, then their character was dropped/swapped +Restore from saved inventory +*/ + static StringMap weaponMaxClipSizes; +static StringMap pInv; static char HUD_SCRIPT[] = "ExtraPlayerHUD <- { Fields = { players = { slot = g_ModeScript.HUD_RIGHT_BOT, dataval = \"%s\", flags = g_ModeScript.HUD_FLAG_ALIGN_LEFT | g_ModeScript.HUD_FLAG_TEAM_SURVIVORS | g_ModeScript.HUD_FLAG_NOBG } } }; HUDSetLayout( ExtraPlayerHUD ); HUDPlace( g_ModeScript.HUD_RIGHT_BOT, 0.72, 0.78, 0.3, 0.3 ); g_ModeScript"; @@ -111,6 +153,7 @@ public void OnPluginStart() { } weaponMaxClipSizes = new StringMap(); + pInv = new StringMap(); ammoPacks = new ArrayList(2); // HookEvent("player_spawn", Event_PlayerSpawn); @@ -122,11 +165,12 @@ public void OnPluginStart() { HookEvent("round_end", Event_RoundEnd); HookEvent("map_transition", Event_MapTransition); HookEvent("game_start", Event_GameStart); + HookEvent("game_end", Event_GameStart); HookEvent("round_freeze_end", Event_RoundFreezeEnd); HookEvent("tank_spawn", Event_TankSpawn); //Special Event Tracking - HookEvent("player_disconnect", Event_PlayerDisconnect); + HookEvent("player_team", Event_PlayerTeam); HookEvent("charger_carry_start", Event_ChargerCarry); HookEvent("charger_carry_end", Event_ChargerCarry); @@ -150,8 +194,9 @@ public void OnPluginStart() { hSaferoomDoorWaitSeconds = CreateConVar("l4d2_extraitems_doorunlock_wait", "55", "How many seconds after to unlock saferoom door. 0 to disable", FCVAR_NONE, true, 0.0); hSaferoomDoorAutoOpen = CreateConVar("l4d2_extraitems_doorunlock_open", "0", "Controls when the door automatically opens after unlocked. Add bits together.\n0 = Never, 1 = When timer expires, 2 = When all players loaded in", FCVAR_NONE, true, 0.0); hEPIHudState = CreateConVar("l4d2_extraitems_hudstate", "1", "Controls when the hud displays.\n0 -> OFF, 1 = When 5+ players, 2 = ALWAYS", FCVAR_NONE, true, 0.0, true, 2.0); - hExtraFinaleTank = CreateConVar("l4d2_extraitems_extra_finale_tank", "1", "0 = Normal tank spawning, 1 = Two tanks spawn on second stage (half health)", FCVAR_NONE, true, 0.0, true, 1.0); - cvDropDisconnectTime = CreateConVar("l4d2_extraitems_disconnect_time", "120", "The amount of seconds after a player has actually disconnected, where their character slot will be void. 0 to disable", FCVAR_NONE, true, 0.0); + hExtraFinaleTank = CreateConVar("l4d2_extraitems_extra_tanks", "3", "Add bits together. 0 = Normal tank spawning, 1 = 50% tank split on non-finale (half health), 2 = Tank split (full health) on finale ", FCVAR_NONE, true, 0.0, true, 3.0); + hSplitTankChance = CreateConVar("l4d2_extraitems_splittank_chance", "0.5", "Add bits together. 0 = Normal tank spawning, 1 = 50% tank split on non-finale (half health), 2 = Tank split (full health) on finale ", FCVAR_NONE, true, 0.0, true, 1.0); + cvDropDisconnectTime = CreateConVar("l4d2_extraitems_disconnect_time", "120.0", "The amount of seconds after a player has actually disconnected, where their character slot will be void. 0 to disable", FCVAR_NONE, true, 0.0); hEPIHudState.AddChangeHook(Cvar_HudStateChange); @@ -196,6 +241,22 @@ public void OnPluginStart() { RegAdminCmd("sm_epi_items", Command_RunExtraItems, ADMFLAG_CHEATS); #endif + CreateTimer(10.0, Timer_ForceUpdateInventories, _, TIMER_REPEAT); +} + +public Action Timer_ForceUpdateInventories(Handle h) { + for(int i = 1; i <= MaxClients; i++) { + if(IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) == 2) { + GetPlayerInventory(i); + } + } + return Plugin_Continue; +} + +public void OnClientPutInServer(int client) { + if(!IsFakeClient(client) && GetClientTeam(client) == 2 && !StrEqual(gamemode, "hideandseek")) { + CreateTimer(0.2, Timer_CheckInventory, client); + } } /////////////////////////////////////////////////////////////////////////////// @@ -348,7 +409,7 @@ int extraTankHP; FinaleStage finaleStage; public Action L4D2_OnChangeFinaleStage(int &finaleType, const char[] arg) { - if(finaleType == FINALE_STARTED && abmExtraCount > 4 && hExtraFinaleTank.BoolValue) { + if(finaleType == FINALE_STARTED && abmExtraCount > 4) { finaleStage = Stage_FinaleActive; PrintToConsoleAll("[EPI] Finale started and over threshold"); } else if(finaleType == FINALE_TANK) { @@ -367,45 +428,42 @@ public void Event_TankSpawn(Event event, const char[] name, bool dontBroadcast) int user = event.GetInt("userid"); int tank = GetClientOfUserId(user); if(tank > 0 && IsFakeClient(tank) && abmExtraCount > 4 && hExtraFinaleTank.BoolValue) { - if(finaleStage == Stage_FinaleTank2) { + if(finaleStage == Stage_FinaleTank2 && allowTankSplit && hExtraFinaleTank.IntValue & 2) { PrintToConsoleAll("[EPI] Second tank spawned, setting health."); // Sets health in half, sets finaleStage to health CreateTimer(5.0, Timer_SpawnFinaleTank, user); } else if(finaleStage == Stage_FinaleDuplicatePending) { PrintToConsoleAll("[EPI] Third & final tank spawned"); RequestFrame(Frame_SetExtraTankHealth, user); - } else if(finaleStage == Stage_Inactive && GetSurvivorsCount() > 6) { - PrintToConsoleAll("[EPI] Creating a split tank"); + } else if(finaleStage == Stage_Inactive && allowTankSplit && hExtraFinaleTank.IntValue & 1 && GetSurvivorsCount() > 6) { finaleStage = Stage_TankSplit; - // Half their HP, assign half to self and for next tank - int hp = GetEntProp(tank, Prop_Send, "m_iHealth") / 2; - SetEntProp(tank, Prop_Send, "m_iHealth", hp); - extraTankHP = hp; - CreateTimer(11.0, Timer_SplitTank, user); + if(GetRandomFloat() <= hSplitTankChance.FloatValue) { + // Half their HP, assign half to self and for next tank + int hp = GetEntProp(tank, Prop_Send, "m_iHealth") / 2; + PrintToConsoleAll("[EPI] Creating a split tank (hp=%d)", hp); + extraTankHP = hp; + CreateTimer(0.2, Timer_SetHealth, user); + CreateTimer(11.0, Timer_SpawnSplitTank, user); + } // Then, summon the next tank } else if(finaleStage == Stage_TankSplit) { - + CreateTimer(0.2, Timer_SetHealth, user); } } } public Action Timer_SpawnFinaleTank(Handle t, int user) { - if(finaleStage == Stage_TankSplit) { + if(finaleStage == Stage_FinaleTank2) { ServerCommand("sm_forcespecial tank"); finaleStage = Stage_Inactive; } } -public Action Timer_SplitTank(Handle t, int user) { - int tank = GetClientOfUserId(user); - if(tank > 0 && finaleStage == Stage_Inactive) { - finaleStage = Stage_TankSplit; - // Half their HP, assign half to self and for next tank - int hp = GetEntProp(tank, Prop_Send, "m_iHealth") / 2; - SetEntProp(tank, Prop_Send, "m_iHealth", hp); - extraTankHP = hp; - // Then, summon the next tank - ServerCommand("sm_forcespecial tank"); - } else { - finaleStage = Stage_Inactive; +public Action Timer_SpawnSplitTank(Handle t, int user) { + ServerCommand("sm_forcespecial tank"); +} +public Action Timer_SetHealth(Handle h, int user) { + int client = GetClientOfUserId(user); + if(client > 0 ) { + SetEntProp(client, Prop_Send, "m_iHealth", extraTankHP); } } @@ -437,8 +495,12 @@ public Action Event_GameStart(Event event, const char[] name, bool dontBroadcast extraKitsStarted = 0; abmExtraCount = 4; hMinPlayers.IntValue = 4; + currentChapter = 0; + pInv.Clear(); + for(int i = 1; i <= MaxClients; i++) { + playerData[i].state = State_Empty; + } return Plugin_Continue; - } public Action Event_PlayerFirstSpawn(Event event, const char[] name, bool dontBroadcast) { @@ -447,7 +509,7 @@ public Action Event_PlayerFirstSpawn(Event event, const char[] name, bool dontBr CreateTimer(1.5, Timer_RemoveInvincibility, client); SDKHook(client, SDKHook_OnTakeDamage, OnInvincibleDamageTaken); if(!IsFakeClient(client)) { - playerData[client].active = true; + playerData[client].state = State_Active; if(L4D_IsFirstMapInScenario() && !firstGiven) { //Check if all clients are ready, and survivor count is > 4. if(AreAllClientsReady()) { @@ -470,11 +532,12 @@ public Action Event_PlayerFirstSpawn(Event event, const char[] name, bool dontBr // TODO: Check if Timer_UpdateMinPlayers is needed, or if this works: // Never decrease abmExtraCount int newCount = GetRealSurvivorsCount(); - if(newCount > abmExtraCount) { + if(newCount > abmExtraCount && abmExtraCount > 4) { abmExtraCount = newCount; + hMinPlayers.IntValue = abmExtraCount; } // If 5 survivors, then set them up, TP them. - if(abmExtraCount > 4) { + if(newCount > 4) { RequestFrame(Frame_SetupNewClient, client); } } @@ -516,28 +579,65 @@ public Action Event_PlayerSpawn(Event event, const char[] name, bool dontBroadca } -public void Event_PlayerDisconnect(Event event, const char[] name, bool dontBroadcast) { - int userid = event.GetInt("userid"); - int client = GetClientOfUserId(userid); - if(client > 0 && !IsFakeClient(client)) { - DataPack pack = new DataPack(); - pack.WriteCell(userid); - pack.WriteCell(client); - CreateDataTimer(cvDropDisconnectTime.FloatValue, Timer_DropSurvivor, pack); +public Action Timer_CheckInventory(Handle h, int client) { + if(IsClientConnected(client) && IsClientInGame(client) && GetClientTeam(client) == 2 && DoesInventoryDiffer(client)) { + PrintToConsoleAll("[EPI] Detected mismatch inventory for %N, restoring", client); + RestoreInventory(client); } } -public Action Timer_DropSurvivor(Handle h, DataPack pack) { - int userid = pack.ReadCell(); - int client = GetClientOfUserId(userid); - // Check if player is not connected, if not, drop their existing status - if(client == 0) { - client = pack.ReadCell(); - if(client == 0) //In the case that someone took their client index, don't inactivate them: - playerData[client].active = false; +public void OnClientDisconnect(int client) { + if(GetClientTeam(client) == 2 && !IsFakeClient(client)) + SaveInventory(client); +} + +public void Event_PlayerTeam(Event event, const char[] name, bool dontBroadcast) { + if(event.GetBool("disconnect")) { + int userid = event.GetInt("userid"); + int client = GetClientOfUserId(userid); + int team = event.GetInt("team"); + if(client > 0 && !event.GetBool("isbot") && team == 2) { + playerData[client].state = State_PendingEmpty; + /*DataPack pack; + CreateDataTimer(cvDropDisconnectTime.FloatValue, Timer_DropSurvivor, pack); + pack.WriteCell(userid); + pack.WriteCell(client);*/ + CreateTimer(cvDropDisconnectTime.FloatValue, Timer_DropSurvivor, client); + } } } +public Action Timer_DropSurvivor(Handle h, int client) { + if(playerData[client].state == State_PendingEmpty) { + playerData[client].state = State_Empty; + PrintToConsoleAll("[EPI] Dropping survivor %d. hMinPlayers-pre:%d", client, hMinPlayers.IntValue); + hMinPlayers.IntValue = --abmExtraCount; + if(hMinPlayers.IntValue < 4) { + hMinPlayers.IntValue = 4; + PrintToConsoleAll("[EPI!!] hMinPlayers dropped below 4. This is a bug, please report to jackz."); + PrintToServer("[EPI!!] hMinPlayers dropped below 4. This is a bug, please report to jackz."); + } + DropDroppedInventories(); + } +} + +/*public Action Timer_DropSurvivor(Handle h, DataPack pack) { + pack.Reset(); + int userid = pack.ReadCell(); + int client = pack.ReadCell(); + // If the userid occupying client index is diff (or 0) + if(GetClientOfUserId(userid) != client) { + // If player was not replaced + if(!IsClientConnected(client)) { + PrintToConsoleAll("Dropping disconnected player after inactivity. UID:%d, index:%d, new MinPlayers: %d", userid, client, hMinPlayers.IntValue-1); + //playerData[client].active = false; + abmExtraCount--; + hMinPlayers.IntValue--; + } + } + DropDroppedInventories(); +}*/ + ///////////////////////////////////////// /////// Events ///////////////////////////////////////// @@ -560,12 +660,42 @@ public Action L4D_OnIsTeamFull(int team, bool &full) { return Plugin_Continue; } +char TIER1_WEAPONS[5][] = { + "shotgun_chrome", + "pumpshotgun", + "smg", + "smg_silenced", + "smg_mp5" +}; + +char TIER2_WEAPONS[9][] = { + "autoshotgun", + "rifle_ak47", + "sniper_military", + "rifle_sg552", + "rifle_desert", + "sniper_scout", + "rifle", + "hunting_rifle", + "shotgun_spas" +}; + public void Frame_SetupNewClient(int client) { if(!DoesClientHaveKit(client)) { int item = GivePlayerItem(client, "weapon_first_aid_kit"); EquipPlayerWeapon(client, item); } + static char weapon[32]; + if(currentChapter == 1 || (currentChapter == 2 && GetRandomFloat() < 0.3)) { + Format(weapon, sizeof(weapon), "weapon_%s", TIER1_WEAPONS[GetRandomInt(0,4)]); + } else { + Format(weapon, sizeof(weapon), "weapon_%s", TIER2_WEAPONS[GetRandomInt(0,8)]); + } + + // Find a suitable weapon to spawn with + + // static float spawnPos[3]; // if(GetIdealPositionInSurvivorFlow(client, spawnPos)) // TeleportEntity(client, spawnPos, NULL_VECTOR, NULL_VECTOR); @@ -618,14 +748,28 @@ public void OnMapStart() { }else if(!L4D_IsFirstMapInScenario()) { //Re-set value incase it reset. //hMinPlayers.IntValue = abmExtraCount; + currentChapter++; }else if(L4D_IsMissionFinalMap()) { //Add extra kits for finales + static char curMap[64]; + GetCurrentMap(curMap, sizeof(curMap)); + + if(StrEqual(curMap, "c4m5_milltown_escape")) { + allowTankSplit = false; + } else { + allowTankSplit = true; + } + int extraKits = GetSurvivorsCount() - 4; if(extraKits > 0) { extraKitsAmount += extraKits; extraKitsStarted = extraKitsAmount; } + currentChapter++; + } else { + currentChapter++; } + if(!isLateLoaded) { isLateLoaded = false; @@ -690,7 +834,7 @@ public void EntityOutput_OnStartTouchSaferoom(const char[] output, int caller, i float averageTeamHP = GetAverageHP(); if(averageTeamHP <= 30.0) extraPlayers += (extraPlayers / 2); //if perm. health < 30, give an extra 4 on top of the extra else if(averageTeamHP <= 50.0) extraPlayers = (extraPlayers / 3); //if the team's average health is less than 50 (permament) then give another - //Chance to get 1-2 extra kits (might need to be nerfed or restricted to > 50 HP) + //Chance to get an extra kit (might need to be nerfed or restricted to > 50 HP) if(GetRandomFloat() < 0.3 && averageTeamHP <= 80.0) ++extraPlayers; @@ -701,6 +845,8 @@ public void EntityOutput_OnStartTouchSaferoom(const char[] output, int caller, i extraKitsAmount = extraPlayers; extraKitsStarted = extraKitsAmount; + + hMinPlayers.IntValue = abmExtraCount; PrintToConsoleAll("CHECKPOINT REACHED BY %N | EXTRA KITS: %d", client, extraPlayers); PrintToServer("Player entered saferoom. Providing %d extra kits", extraKitsAmount); } @@ -917,11 +1063,11 @@ public Action Timer_UpdateHud(Handle h) { } //TOOD: Move to bool instead of ent prop if(!IsPlayerAlive(i)) - Format(data, sizeof(data), "Dead"); + Format(data, sizeof(data), "xx"); else if(GetEntProp(i, Prop_Send, "m_bIsOnThirdStrike") == 1) - Format(data, sizeof(data), "+%d b&&w %s%s%s", health, items[i].throwable, items[i].usable, items[i].consumable); + Format(data, sizeof(data), "+%d b&w %s%s%s", health, items[i].throwable, items[i].usable, items[i].consumable); else if(GetEntProp(i, Prop_Send, "m_isIncapacitated") == 1) { - Format(data, sizeof(data), "+%d (down)", health); + Format(data, sizeof(data), "+%d --", health); }else{ Format(data, sizeof(data), "+%d %s%s%s", health, items[i].throwable, items[i].usable, items[i].consumable); } @@ -994,6 +1140,108 @@ public void PopulateItems() { ///////////////////////////////////// /// Stocks //////////////////////////////////// +// enum struct PlayerData { +// bool itemGiven; //Is player being given an item (such that the next pickup event is ignored) +// bool isUnderAttack; //Is the player under attack (by any special) +// bool active; + +// WeaponId itemID[6]; //int -> char? +// bool lasers; +// char meleeID[32]; + + +// int primaryHealth; +// int tempHealth; + +// char model[32]; +// } + +void DropDroppedInventories() { + StringMapSnapshot snapshot = pInv.Snapshot(); + static PlayerInventory inv; + static char buffer[32]; + int time = GetTime(); + for(int i = 0; i < snapshot.Length; i++) { + snapshot.GetKey(i, buffer, sizeof(buffer)); + pInv.GetArray(buffer, inv, sizeof(inv)); + if(time - inv.timestamp > PLAYER_DROP_TIMEOUT_SECONDS) { + pInv.Remove(buffer); + } + } +} +void SaveInventory(int client) { + + PlayerInventory inventory; + inventory.timestamp = GetTime(); + inventory.isAlive = IsPlayerAlive(client); + + inventory.primaryHealth = GetClientHealth(client); + GetClientModel(client, inventory.model, 64); + inventory.survivorType = GetEntProp(client, Prop_Send, "m_survivorCharacter"); + + int weapon; + static char buffer[32]; + for(int i = 5; i >= 0; i--) { + weapon = GetPlayerWeaponSlot(client, i); + inventory.itemID[i] = IdentifyWeapon(weapon); + } + if(inventory.itemID[0] != WEPID_MELEE && inventory.itemID[0] != WEPID_NONE) + inventory.lasers = GetEntProp(weapon, Prop_Send, "m_upgradeBitVec") == 4; + + GetClientAuthId(client, AuthId_Steam3, buffer, sizeof(buffer)); + pInv.SetArray(buffer, inventory, sizeof(inventory)); +} + +void RestoreInventory(int client) { + static char buffer[32]; + GetClientAuthId(client, AuthId_Steam3, buffer, sizeof(buffer)); + PlayerInventory inventory; + pInv.GetArray(buffer, inventory, sizeof(inventory)); + + PrintToConsoleAll("[debug:RINV] health=%d primaryID=%d secondID=%d throw=%d kit=%d pill=%d surv=%d", inventory.primaryHealth, inventory.itemID[0], inventory.itemID[1], inventory.itemID[2], inventory.itemID[3], inventory.itemID[4], inventory.itemID[5], inventory.survivorType); + + return; + SetEntityModel(client, inventory.model); + SetEntProp(client, Prop_Send, "m_survivorCharacter", inventory.survivorType); + + if(inventory.isAlive) { + SetEntProp(client, Prop_Send, "m_iHealth", inventory.primaryHealth); + + int weapon; + for(int i = 5; i >= 0; i--) { + WeaponId id = inventory.itemID[i]; + if(id != WEPID_NONE) { + if(id == WEPID_MELEE) + GetWeaponName(id, buffer, sizeof(buffer)); + else + GetMeleeWeaponName(id, buffer, sizeof(buffer)); + weapon = GiveClientWeapon(client, buffer); + } + } + if(inventory.lasers) { + SetEntProp(weapon, Prop_Send, "m_upgradeBitVec", 4); + } + } + + GetClientAuthId(client, AuthId_Steam3, buffer, sizeof(buffer)); + pInv.Remove(buffer); +} + +bool DoesInventoryDiffer(int client) { + static char buffer[32]; + GetClientAuthId(client, AuthId_Steam3, buffer, sizeof(buffer)); + PlayerInventory inventory; + if(!pInv.GetArray(buffer, inventory, sizeof(inventory)) || inventory.timestamp == 0) { + return false; + } + + WeaponId currentPrimary = IdentifyWeapon(GetPlayerWeaponSlot(client, 0)); + WeaponId currentSecondary = IdentifyWeapon(GetPlayerWeaponSlot(client, 1)); + WeaponId storedPrimary = inventory.itemID[0]; + WeaponId storedSecondary = inventory.itemID[1]; + + return currentPrimary != storedPrimary || currentSecondary != storedSecondary; +} stock int FindFirstSurvivor() { for(int i = 1; i <= MaxClients; i++) { @@ -1034,6 +1282,16 @@ stock int GetSurvivorsCount() { return count; } +stock int GetActiveCount() { + int count; + for(int i = 1; i <= MaxClients; i++) { + if(IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) == 2 && playerData[i].state == State_Active) { + count++; + } + } + return count; +} + stock int GetRealSurvivorsCount() { #if defined DEBUG_FORCE_PLAYERS return DEBUG_FORCE_PLAYERS; @@ -1167,19 +1425,33 @@ int FindCabinetIndex(int cabinetId) { } void GetPlayerInventory(int client) { - char item[16]; - GetClientWeaponName(client, 2, item, sizeof(item)); - items[client].throwable[0] = CharToUpper(item[7]); - items[client].throwable[1] = '\0'; + static char item[16]; + if(GetClientWeaponName(client, 2, item, sizeof(item))) { + items[client].throwable[0] = CharToUpper(item[7]); + if(items[client].throwable[0] == 'V') { + items[client].throwable[0] = 'B'; //Replace [V]omitjar with [B]ile + } + items[client].throwable[1] = '\0'; + } else { + items[client].throwable[0] = '\0'; + } if(GetClientWeaponName(client, 3, item, sizeof(item))) { items[client].usable[0] = CharToUpper(item[7]); items[client].usable[1] = '\0'; + if(items[client].throwable[0] == 'F') { + items[client].throwable[0] = '+'; //Replace [V]omitjar with [B]ile + } + } else { + items[client].usable[0] = '-'; + items[client].usable[1] = '\0'; } if(GetClientWeaponName(client, 4, item, sizeof(item))) { items[client].consumable[0] = CharToUpper(item[7]); items[client].consumable[1] = '\0'; + } else { + items[client].consumable[0] = '\0'; } } diff --git a/scripting/l4d2_feedthetrolls.sp b/scripting/l4d2_feedthetrolls.sp index 05df479..632ce5a 100644 --- a/scripting/l4d2_feedthetrolls.sp +++ b/scripting/l4d2_feedthetrolls.sp @@ -17,6 +17,7 @@ #include #include #include +#include public Plugin myinfo = @@ -28,8 +29,6 @@ public Plugin myinfo = url = "" }; -//TODO: Make bots target player. Possibly automatic . See https://i.jackz.me/2021/05/NVIDIA_Share_2021-05-05_19-36-51.png -//TODO: Friendly trolling VS punishment trolling //TODO: Trolls: Force take pills, Survivor Bot Magnet @@ -68,11 +67,11 @@ public void OnPluginStart() { hShoveFailChance = CreateConVar("sm_ftt_shove_fail_chance", "0.65", "The % chance that a shove fails", FCVAR_NONE, true, 0.0, true, 1.0); hBadThrowHitSelf = CreateConVar("sm_ftt_badthrow_fail_chance", "1", "The % chance that on a throw, they will instead hit themselves. 0 to disable", FCVAR_NONE, true, 0.0, true, 1.0); hBotReverseFFDefend = CreateConVar("sm_ftt_bot_defend", "1", "Should bots defend themselves?\n0 = OFF\n1 = Will retaliate against non-admins\n2 = Anyone", FCVAR_NONE, true, 0.0, true, 2.0); + hBotDefendChance = CreateConVar("sm_ftt_bot_defend_chance", "0.75", "% Chance bots will defend themselves.", FCVAR_NONE, true, 0.0, true, 1.0); hSbFriendlyFire = FindConVar("sb_friendlyfire"); + if(hBotReverseFFDefend.IntValue > 0) hSbFriendlyFire.BoolValue = true; - - hBotReverseFFDefend.AddChangeHook(Change_BotDefend); RegAdminCmd("sm_ftl", Command_ListTheTrolls, ADMFLAG_KICK, "Lists all the trolls currently ingame."); @@ -84,7 +83,7 @@ public void OnPluginStart() { RegAdminCmd("sm_mark", Command_MarkPendingTroll, ADMFLAG_KICK, "Marks a player as to be banned on disconnect"); RegAdminCmd("sm_ftp", Command_FeedTheCrescendoTroll, ADMFLAG_KICK, "Applies a manual punish on the last crescendo activator"); RegAdminCmd("sm_ftc", Command_ApplyComboTrolls, ADMFLAG_KICK, "Applies predefined combinations of trolls"); - RegAdminCmd("sm_witch_attack", Command_WitchAttack, ADMFLAG_CHEATS, "Makes all witches target a player"); + RegAdminCmd("sm_witch_attack", Command_WitchAttack, ADMFLAG_CHEATS, "Makes all witches target a player"); RegAdminCmd("sm_insta", Command_InstaSpecial, ADMFLAG_KICK, "Spawns a special that targets them, close to them."); RegAdminCmd("sm_instaface", Command_InstaSpecialFace, ADMFLAG_KICK, "Spawns a special that targets them, right in their face."); RegAdminCmd("sm_inface", Command_InstaSpecialFace, ADMFLAG_KICK, "Spawns a special that targets them, right in their face."); @@ -118,6 +117,7 @@ public void Change_ThrowInterval(ConVar convar, const char[] oldValue, const cha } } +// Turn on bot FF if bot defend enabled public void Change_BotDefend(ConVar convar, const char[] oldValue, const char[] newValue) { hSbFriendlyFire.IntValue = convar.IntValue != 0; } @@ -180,7 +180,7 @@ bool IsPlayerFarDistance(int client, float distance) { } } float difference = highestFlow - secondHighestFlow; - PrintToConsoleAll("Flow Check | Player=%N Flow=%f Delta=%f", farthestClient, highestFlow, difference); + PrintToConsoleAll("Flow Check | Player1=%N Flow1=%f Delta=%f", farthestClient, highestFlow, difference); PrintToConsoleAll("Flow Check | Player2=%N Flow2=%f", secondClient, secondHighestFlow); return client == farthestClient && difference > distance; } diff --git a/scripting/l4d2_tank_priority.sp b/scripting/l4d2_tank_priority.sp index 457d96e..0709464 100644 --- a/scripting/l4d2_tank_priority.sp +++ b/scripting/l4d2_tank_priority.sp @@ -57,7 +57,7 @@ public Action L4D2_OnChooseVictim(int attacker, int &curTarget) { float dist = GetVectorDistance(clientPos, tankPos); // Only add targets who are far enough away from tank if(dist > 3000.0) { - PrintToConsoleAll("Adding player %N to possible victim list. Dist=%f, Dmg=%d", i, dist, totalTankDamage[i]); + PrintToConsoleAll("[TankPriority/debug] Adding player %N to possible victim list. Dist=%f Dmg=%d", i, dist, totalTankDamage[i]); int index = clients.Push(i); clients.Set(index, dist, 1); } @@ -74,8 +74,9 @@ public Action L4D2_OnChooseVictim(int attacker, int &curTarget) { //TODO: Possibly clear totalTankDamage return Plugin_Changed; } + if(tankChosenVictim[attacker] > 0) { - if(IsClientConnected(tankChosenVictim[attacker]) && IsClientInGame(tankChosenVictim[attacker])) { + if(IsClientConnected(tankChosenVictim[attacker]) && IsClientInGame(tankChosenVictim[attacker]) && IsPlayerAlive(tankChosenVictim[attacker]) && !IsPlayerIncapacitated(tankChosenVictim[attacker])) { curTarget = tankChosenVictim[attacker]; return Plugin_Changed; } else {