diff --git a/README.md b/README.md index 3fb4971..b498d86 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ This really only affects wandering zombies, mobs and panic events, but it may wo ### l4d2_feedthetrolls This plugin allows you to enact certain troll modes on any player, some are subtle some are less so. Either way, it works great to deal with a rusher, an asshole or even your friends. -Troll Modes: (updated 1/2/2021) +Troll Modes: (updated 4/20/2021) 1. **SlowSpeed** (0.8 < 1.0 base) - Slows down a user 2. **HigherGravity** (1.3 > 1.0) - Adds more gravity to a user diff --git a/plugins/BumpMineGiver.smx b/plugins/BumpMineGiver.smx new file mode 100644 index 0000000..04a48d7 Binary files /dev/null and b/plugins/BumpMineGiver.smx differ diff --git a/plugins/L4D2PreventBotMovement.smx b/plugins/L4D2PreventBotMovement.smx new file mode 100644 index 0000000..2311438 Binary files /dev/null and b/plugins/L4D2PreventBotMovement.smx differ diff --git a/plugins/l4d2_drop_secondary.smx b/plugins/l4d2_drop_secondary.smx new file mode 100644 index 0000000..d67c828 Binary files /dev/null and b/plugins/l4d2_drop_secondary.smx differ diff --git a/plugins/l4d2_skill_detect.smx b/plugins/l4d2_skill_detect.smx new file mode 100644 index 0000000..c1eca08 Binary files /dev/null and b/plugins/l4d2_skill_detect.smx differ diff --git a/plugins/l4d2_target_test.smx b/plugins/l4d2_target_test.smx new file mode 100644 index 0000000..12792f7 Binary files /dev/null and b/plugins/l4d2_target_test.smx differ diff --git a/plugins/l4d_tank_hp_sprite.smx b/plugins/l4d_tank_hp_sprite.smx index 1766cc5..5461441 100644 Binary files a/plugins/l4d_tank_hp_sprite.smx and b/plugins/l4d_tank_hp_sprite.smx differ diff --git a/plugins/l4d_target_override.smx b/plugins/l4d_target_override.smx new file mode 100644 index 0000000..c3887a0 Binary files /dev/null and b/plugins/l4d_target_override.smx differ diff --git a/plugins/l4dunreservelobby.smx b/plugins/l4dunreservelobby.smx new file mode 100644 index 0000000..8e70f23 Binary files /dev/null and b/plugins/l4dunreservelobby.smx differ diff --git a/plugins/left4dhooks.smx b/plugins/left4dhooks.smx new file mode 100644 index 0000000..6ea2b60 Binary files /dev/null and b/plugins/left4dhooks.smx differ diff --git a/plugins/sceneprocessor.smx b/plugins/sceneprocessor.smx new file mode 100644 index 0000000..7d50143 Binary files /dev/null and b/plugins/sceneprocessor.smx differ diff --git a/scripting/AutoWarpBot.sp b/scripting/AutoWarpBot.sp index b428d1a..d79d17d 100644 --- a/scripting/AutoWarpBot.sp +++ b/scripting/AutoWarpBot.sp @@ -24,7 +24,7 @@ public Plugin myinfo = url = PLUGIN_URL }; -bool g_EnteredCheckpoint = false; +ConVar g_EnteredCheckpoint; public void OnPluginStart() { @@ -34,28 +34,31 @@ public void OnPluginStart() SetFailState("This plugin is for L4D/L4D2 only."); } HookEvent("player_entered_checkpoint", Event_PlayerEnteredCheckpoint); - HookEvent("round_start",Event_RoundStart); + HookEvent("player_left_start_area",Event_RoundStart); + g_EnteredCheckpoint = CreateConVar("awb_status", "0", "Get status of plugin", FCVAR_SPONLY | FCVAR_DONTRECORD, true, 0.0, true, 1.0); } public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast) { - g_EnteredCheckpoint = false; + if(g_EnteredCheckpoint.BoolValue) g_EnteredCheckpoint.BoolValue = false; } public void Event_PlayerEnteredCheckpoint(Event event, const char[] name, bool dontBroadcast) { - if(!g_EnteredCheckpoint) { + PrintToChatAll("boolvalue %d | client %d", g_EnteredCheckpoint.BoolValue, GetClientOfUserId(event.GetInt("userid"))); + if(!g_EnteredCheckpoint.BoolValue) { + int client = GetClientOfUserId(event.GetInt("userid")); bool playersLeft = false; for (int i = 1; i < MaxClients;i++) { - if (client == i)continue; - if (!IsClientConnected(i)) continue; + if (client == i) continue; + if (!IsClientInGame(i)) continue; if (GetClientTeam(i) != 2) continue; - + PrintToChatAll("playersLeft: %d . clientr %d", playersLeft, i); if(!IsFakeClient(i)) { playersLeft = true; break; } } if(!playersLeft) { - g_EnteredCheckpoint = true; + g_EnteredCheckpoint.BoolValue = true; CheatCommand(client,"warp_all_survivors_to_checkpoint","",""); } } diff --git a/scripting/BumpMineGiver.sp b/scripting/BumpMineGiver.sp new file mode 100644 index 0000000..3011921 --- /dev/null +++ b/scripting/BumpMineGiver.sp @@ -0,0 +1,114 @@ +#pragma semicolon 1 + +#define DEBUG + +#define PLUGIN_AUTHOR "" +#define PLUGIN_VERSION "0.00" + +#include +#include +#include +//#include + +#pragma newdecls required + +ConVar g_bmgTTeam, g_bmgEnabled, g_bmgCmdLimit; + +int g_bmgBumpsGiven[MAXPLAYERS+1]; + +public Plugin myinfo = +{ + name = "BumpMineGiver", + author = PLUGIN_AUTHOR, + description = "", + version = PLUGIN_VERSION, + url = "" +}; + +public void OnPluginStart() +{ + EngineVersion g_Game = GetEngineVersion(); + if(g_Game != Engine_CSGO && g_Game != Engine_CSS) + { + SetFailState("This plugin is for CSGO/CSS only."); + } + g_bmgEnabled = CreateConVar("bmg_enabled", "1", "Should BumpMineGiver be enabled?", FCVAR_NONE, true, 0.0, true, 1.0); + g_bmgTTeam = CreateConVar("bmg_restrict_team", "0", "Should BumpMineGiver be restricted to a team? 0 - All, 1 - Terrorists, 2 - CounterTerrorists", FCVAR_NONE, true, 0.0, true, 3.0); + g_bmgCmdLimit = CreateConVar("bmg_cmdlimit", "0", "Limit of amount of bumpmines to be given with !bmp. 0: Disabled, -1: Infinity", FCVAR_NONE); + HookEvent("round_start", Event_RoundStart); + HookEvent("player_spawn", Event_PlayerSpawn); + + RegConsoleCmd("sm_bmp", Command_GiveBMP, "Give yourself a bump mine"); + RegConsoleCmd("sm_bmg", Command_GiveBMP, "Give yourself a bump mine"); + RegAdminCmd("sm_givebmp", Command_GiveOthersBMP, ADMFLAG_CHEATS, "Give someone x amount of bump mines. Usage: sm_givebmp "); + AutoExecConfig(); +} + +public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast) { + for (int i = 0; i < sizeof(g_bmgBumpsGiven); i++) { + g_bmgBumpsGiven[i] = 0; + } +} + +public void Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast) { + if(g_bmgEnabled.BoolValue) { + int client = GetClientOfUserId(event.GetInt("userid")); + int wpn_id = GetPlayerWeaponSlot(client, 4); + int team = GetClientTeam(client); + if(g_bmgTTeam.IntValue > 0) { //1 or 2 + if (team != g_bmgTTeam.IntValue) return; + } + if(wpn_id == -1) { + GivePlayerItem(client, "weapon_bumpmine"); + } + } +} + +public Action Command_GiveBMP(int client, int args) { + if(g_bmgCmdLimit.IntValue == 0) { + ReplyToCommand(client, "You have hit the limit of bumpmines"); + return Plugin_Handled; + } + //limit is enabled, check + if(g_bmgCmdLimit.IntValue > 0) { + if(g_bmgBumpsGiven[client] > g_bmgCmdLimit.IntValue) { + ReplyToCommand(client, "You have hit the limit of bumpmines"); + return Plugin_Handled; + } + } + GivePlayerItem(client, "weapon_bumpmine"); + g_bmgBumpsGiven[client]++; + return Plugin_Handled; +} + +public Action Command_GiveOthersBMP(int client, int args) { + if(args < 1) { + ReplyToCommand(client, "Usage: sm_givebmp "); + }else{ + char arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + char target_name[MAX_TARGET_LENGTH]; + int target_list[MAXPLAYERS], target_count; + bool tn_is_ml; + if ((target_count = ProcessTargetString( + arg1, + client, + target_list, + MAXPLAYERS, + COMMAND_FILTER_ALIVE, /* Only allow alive players */ + target_name, + sizeof(target_name), + tn_is_ml)) <= 0) + { + /* This function replies to the admin with a failure message */ + ReplyToTargetError(client, target_count); + return Plugin_Handled; + } + for (int i = 0; i < target_count; i++) + { + GivePlayerItem(target_list[i], "weapon_bumpmine"); + LogAction(client, target_list[i], "\"%L\" gave \"%L\" a bumpmine", client, target_list[i]); + } + } + return Plugin_Handled; +} \ No newline at end of file diff --git a/scripting/L4D2PreventBotMovement.sp b/scripting/L4D2PreventBotMovement.sp new file mode 100644 index 0000000..822071e --- /dev/null +++ b/scripting/L4D2PreventBotMovement.sp @@ -0,0 +1,57 @@ +#pragma semicolon 1 +#pragma newdecls required + +//#define DEBUG + +#define PLUGIN_NAME "L4D2 Prevent Bot Movement" +#define PLUGIN_DESCRIPTION "Prevents bots from moving in the beginning of the round for a set period of time." +#define PLUGIN_AUTHOR "jackzmc" +#define PLUGIN_VERSION "1.0" +#define PLUGIN_URL "" + +#include +#include +//#include + +public Plugin myinfo = +{ + name = PLUGIN_NAME, + author = PLUGIN_AUTHOR, + description = PLUGIN_DESCRIPTION, + version = PLUGIN_VERSION, + url = PLUGIN_URL +}; + +static ConVar hSBStop, hStopTime; + +public void OnPluginStart() +{ + EngineVersion g_Game = GetEngineVersion(); + if(g_Game != Engine_Left4Dead && g_Game != Engine_Left4Dead2) + { + SetFailState("This plugin is for L4D/L4D2 only."); + } + HookEvent("round_start",Event_RoundStart); + + hStopTime = CreateConVar("sm_freeze_bot_time","20.0","How long should the bots be frozen for on beginning of round? 0 to disable",FCVAR_NONE,true,0.0); + hSBStop = FindConVar("sb_stop"); +} +public void OnMapStart() { + if(hStopTime.IntValue != 0) { + PrintToChatAll("round start"); + hSBStop.BoolValue = true; + CreateTimer(hStopTime.FloatValue, ResumeBots); + } +} + +public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast) { + if(hStopTime.IntValue != 0) { + PrintToChatAll("round start"); + hSBStop.BoolValue = true; + CreateTimer(hStopTime.FloatValue, ResumeBots); + } +} +public Action ResumeBots(Handle timer) { + PrintToChatAll("Resuming bots"); + hSBStop.BoolValue = false; +} diff --git a/scripting/csgo-misc.sp b/scripting/csgo-misc.sp new file mode 100644 index 0000000..f039622 --- /dev/null +++ b/scripting/csgo-misc.sp @@ -0,0 +1,60 @@ +#pragma semicolon 1 + +#define DEBUG + +#define PLUGIN_AUTHOR "" +#define PLUGIN_VERSION "0.00" + +#include +#include +#include +//#include + +#pragma newdecls required + +public Plugin myinfo = +{ + name = "", + author = PLUGIN_AUTHOR, + description = "", + version = PLUGIN_VERSION, + url = "" +}; + +public void OnPluginStart() +{ + EngineVersion g_Game = GetEngineVersion(); + if(g_Game != Engine_CSGO && g_Game != Engine_CSS) + { + SetFailState("This plugin is for CSGO/CSS only."); + } + RegConsoleCmd("sm_first", Command_FirstPov, "Go back to first person"); + RegConsoleCmd("sm_third", Command_ThirdPov, "Go to third person"); +} +public Action Command_FirstPov(int client, int args) { + if(client == 0) { + ReplyToCommand(client, "This command is for clients only"); + return Plugin_Handled; + } + CheatCommand(client, "firstperson", "", ""); + return Plugin_Handled; +} +public Action Command_ThirdPov(int client, int args) { + if(client == 0) { + ReplyToCommand(client, "This command is for clients only"); + return Plugin_Handled; + } + CheatCommand(client, "thirdperson", "", ""); + return Plugin_Handled; +} + +stock void CheatCommand(int client, char[] command, char[] argument1, char[] argument2) +{ + int userFlags = GetUserFlagBits(client); + SetUserFlagBits(client, ADMFLAG_ROOT); + int flags = GetCommandFlags(command); + SetCommandFlags(command, flags & ~FCVAR_CHEAT); + FakeClientCommand(client, "%s %s %s", command, argument1, argument2); + SetCommandFlags(command, flags); + SetUserFlagBits(client, userFlags); +} \ No newline at end of file diff --git a/scripting/l4d2_drop_secondary.sp b/scripting/l4d2_drop_secondary.sp new file mode 100644 index 0000000..7d56a65 --- /dev/null +++ b/scripting/l4d2_drop_secondary.sp @@ -0,0 +1,233 @@ +#pragma semicolon 1 +#pragma newdecls required //強制1.7以後的新語法 +#include +#include + +#define MODEL_V_FIREAXE "models/weapons/melee/v_fireaxe.mdl" +#define MODEL_V_FRYING_PAN "models/weapons/melee/v_frying_pan.mdl" +#define MODEL_V_MACHETE "models/weapons/melee/v_machete.mdl" +#define MODEL_V_BASEBALL_BAT "models/weapons/melee/v_bat.mdl" +#define MODEL_V_CROWBAR "models/weapons/melee/v_crowbar.mdl" +#define MODEL_V_CRICKET_BAT "models/weapons/melee/v_cricket_bat.mdl" +#define MODEL_V_TONFA "models/weapons/melee/v_tonfa.mdl" +#define MODEL_V_KATANA "models/weapons/melee/v_katana.mdl" +#define MODEL_V_ELECTRIC_GUITAR "models/weapons/melee/v_electric_guitar.mdl" +#define MODEL_V_GOLFCLUB "models/weapons/melee/v_golfclub.mdl" +#define MODEL_V_SHIELD "models/weapons/melee/v_riotshield.mdl" +#define MODEL_V_KNIFE "models/v_models/v_knife_t.mdl" +#define MODEL_V_SHOVEL "models/weapons/melee/v_shovel.mdl" +#define MODEL_V_PITCHFORK "models/weapons/melee/v_pitchfork.mdl" + +static g_PlayerSecondaryWeapons[MAXPLAYERS + 1]; + +public Plugin myinfo = +{ + name = "L4D2 Drop Secondary", + author = "Jahze, Visor, NoBody & HarryPotter", + version = "1.8", + description = "Survivor players will drop their secondary weapon when they die", + url = "https://github.com/Attano/Equilibrium" +}; + +public void OnPluginStart() +{ + HookEvent("round_start", OnRoundStart, EventHookMode_PostNoCopy); + HookEvent("player_use", OnPlayerUse, EventHookMode_Post); + HookEvent("player_bot_replace", player_bot_replace); + HookEvent("bot_player_replace", bot_player_replace); + HookEvent("player_death", OnPlayerDeath, EventHookMode_Pre); + HookEvent("weapon_drop", OnWeaponDrop); + HookEvent("item_pickup", OnItemPickUp); +} + +public void OnRoundStart(Event event, const char[] name, bool dontBroadcast) +{ + for (int i = 0; i <= MAXPLAYERS; i++) + { + g_PlayerSecondaryWeapons[i] = -1; + } +} + +public Action OnPlayerUse(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + + if(client == 0 || !IsClientInGame(client) || GetClientTeam(client) != 2) + { + return; + } + + int weapon = GetPlayerWeaponSlot(client, 1); + + g_PlayerSecondaryWeapons[client] = (weapon == -1 ? weapon : EntIndexToEntRef(weapon)); +} + +public Action bot_player_replace(Event event, const char[] name, bool dontBroadcast) +{ + int bot = GetClientOfUserId(event.GetInt("bot")); + int client = GetClientOfUserId(event.GetInt("player")); + + g_PlayerSecondaryWeapons[client] = g_PlayerSecondaryWeapons[bot]; + g_PlayerSecondaryWeapons[bot] = -1; +} + +public Action player_bot_replace(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("player")); + int bot = GetClientOfUserId(event.GetInt("bot")); + + g_PlayerSecondaryWeapons[bot] = g_PlayerSecondaryWeapons[client]; + g_PlayerSecondaryWeapons[client] = -1; +} + +public Action OnItemPickUp(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + if(client == 0 || !IsClientInGame(client) || GetClientTeam(client) != 2) + { + return; + } + int weapon = GetPlayerWeaponSlot(client, 1); + + g_PlayerSecondaryWeapons[client] = (weapon == -1 ? weapon : EntIndexToEntRef(weapon)); +} + +public Action OnWeaponDrop(Event event, const char[] name, bool dontBroadcast) +{ + CreateTimer(0.1, ColdDown, event.GetInt("userid")); +} + +public Action ColdDown(Handle timer, int client) +{ + client = GetClientOfUserId(client); + if(client && IsClientInGame(client) && GetClientTeam(client) == 2) + { + int weapon = GetPlayerWeaponSlot(client, 1); + + g_PlayerSecondaryWeapons[client] = (weapon == -1 ? weapon : EntIndexToEntRef(weapon)); + } +} + +public Action OnPlayerDeath(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + + if(client == 0 || !IsClientInGame(client) || GetClientTeam(client) != 2) + { + return; + } + + int weapon = EntRefToEntIndex(g_PlayerSecondaryWeapons[client]); + + if(weapon == INVALID_ENT_REFERENCE) + { + return; + } + + char sWeapon[32]; + int clip; + GetEdictClassname(weapon, sWeapon, 32); + + int index = CreateEntityByName(sWeapon); + float origin[3]; + float ang[3]; + if (strcmp(sWeapon, "weapon_melee") == 0) + { + char melee[150]; + GetEntPropString(weapon , Prop_Data, "m_ModelName", melee, sizeof(melee)); + if (strcmp(melee, MODEL_V_FIREAXE) == 0) + { + DispatchKeyValue(index, "melee_script_name", "fireaxe"); + } + else if (strcmp(melee, MODEL_V_FRYING_PAN) == 0) + { + DispatchKeyValue(index, "melee_script_name", "frying_pan"); + } + else if (strcmp(melee, MODEL_V_MACHETE) == 0) + { + DispatchKeyValue(index, "melee_script_name", "machete"); + } + else if (strcmp(melee, MODEL_V_BASEBALL_BAT) == 0) + { + DispatchKeyValue(index, "melee_script_name", "baseball_bat"); + } + else if (strcmp(melee, MODEL_V_CROWBAR) == 0) + { + DispatchKeyValue(index, "melee_script_name", "crowbar"); + } + else if (strcmp(melee, MODEL_V_CRICKET_BAT) == 0) + { + DispatchKeyValue(index, "melee_script_name", "cricket_bat"); + } + else if (strcmp(melee, MODEL_V_TONFA) == 0) + { + DispatchKeyValue(index, "melee_script_name", "tonfa"); + } + else if (strcmp(melee, MODEL_V_KATANA) == 0) + { + DispatchKeyValue(index, "melee_script_name", "katana"); + } + else if (strcmp(melee, MODEL_V_ELECTRIC_GUITAR) == 0) + { + DispatchKeyValue(index, "melee_script_name", "electric_guitar"); + } + else if (strcmp(melee, MODEL_V_GOLFCLUB) == 0) + { + DispatchKeyValue(index, "melee_script_name", "golfclub"); + } + else if (strcmp(melee, MODEL_V_SHIELD) == 0) + { + DispatchKeyValue(index, "melee_script_name", "riotshield"); + } + else if (strcmp(melee, MODEL_V_KNIFE) == 0) + { + DispatchKeyValue(index, "melee_script_name", "knife"); + } + else if (strcmp(melee, MODEL_V_SHOVEL) == 0) + { + DispatchKeyValue(index, "melee_script_name", "shovel"); + } + else if (strcmp(melee, MODEL_V_PITCHFORK) == 0) + { + DispatchKeyValue(index, "melee_script_name", "pitchfork"); + } + else return; + } + else if (strcmp(sWeapon, "weapon_chainsaw") == 0) + { + clip = GetEntProp(weapon, Prop_Send, "m_iClip1"); + } + else if (strcmp(sWeapon, "weapon_pistol") == 0 && (GetEntProp(weapon, Prop_Send, "m_isDualWielding") > 0)) + { + int indexC = CreateEntityByName(sWeapon); + GetClientEyePosition(client,origin); + GetClientEyeAngles(client, ang); + GetAngleVectors(ang, ang, NULL_VECTOR,NULL_VECTOR); + NormalizeVector(ang,ang); + ScaleVector(ang, 90.0); + + DispatchSpawn(indexC); + TeleportEntity(indexC, origin, NULL_VECTOR, ang); + } + else if (strcmp(sWeapon, "weapon_pistol_magnum") == 0) + { + clip = GetEntProp(weapon, Prop_Send, "m_iClip1"); + } + + RemovePlayerItem(client, weapon); + RemoveEntity(weapon); + + GetClientEyePosition(client,origin); + GetClientEyeAngles(client, ang); + GetAngleVectors(ang, ang, NULL_VECTOR,NULL_VECTOR); + NormalizeVector(ang,ang); + ScaleVector(ang, 90.0); + + DispatchSpawn(index); + TeleportEntity(index, origin, NULL_VECTOR, ang); + + if (strcmp(sWeapon, "weapon_chainsaw") == 0 || strcmp(sWeapon, "weapon_pistol") == 0 || strcmp(sWeapon, "weapon_pistol_magnum") == 0) + { + SetEntProp(index, Prop_Send, "m_iClip1", clip); + } +} diff --git a/scripting/l4d2_skill_detect.sp b/scripting/l4d2_skill_detect.sp new file mode 100644 index 0000000..4cc88d2 --- /dev/null +++ b/scripting/l4d2_skill_detect.sp @@ -0,0 +1,3125 @@ +/** + * L4D2_skill_detect + * + * Plugin to detect and forward reports about 'skill'-actions, + * such as skeets, crowns, levels, dp's. + * Works in campaign and versus modes. + * + * m_isAttemptingToPounce can only be trusted for + * AI hunters -- for human hunters this gets cleared + * instantly on taking killing damage + * + * Shotgun skeets and teamskeets are only counted if the + * added up damage to pounce_interrupt is done by shotguns + * only. 'Skeeting' chipped hunters shouldn't count, IMO. + * + * This performs global forward calls to: + * OnSkeet( survivor, hunter ) + * OnSkeetMelee( survivor, hunter ) + * OnSkeetGL( survivor, hunter ) + * OnSkeetSniper( survivor, hunter ) + * OnSkeetHurt( survivor, hunter, damage, isOverkill ) + * OnSkeetMeleeHurt( survivor, hunter, damage, isOverkill ) + * OnSkeetSniperHurt( survivor, hunter, damage, isOverkill ) + * OnHunterDeadstop( survivor, hunter ) + * OnBoomerPop( survivor, boomer, shoveCount, Float:timeAlive ) + * OnChargerLevel( survivor, charger ) + * OnChargerLevelHurt( survivor, charger, damage ) + * OnWitchCrown( survivor, damage ) + * OnWitchCrownHurt( survivor, damage, chipdamage ) + * OnTongueCut( survivor, smoker ) + * OnSmokerSelfClear( survivor, smoker, withShove ) + * OnTankRockSkeeted( survivor, tank ) + * OnTankRockEaten( tank, survivor ) + * OnHunterHighPounce( hunter, victim, actualDamage, Float:calculatedDamage, Float:height, bool:bReportedHigh, bool:bPlayerIncapped ) + * OnJockeyHighPounce( jockey, victim, Float:height, bool:bReportedHigh ) + * OnDeathCharge( charger, victim, Float: height, Float: distance, wasCarried ) + * OnSpecialShoved( survivor, infected, zombieClass ) + * OnSpecialClear( clearer, pinner, pinvictim, zombieClass, Float:timeA, Float:timeB, withShove ) + * OnBoomerVomitLanded( boomer, amount ) + * OnBunnyHopStreak( survivor, streak, Float:maxVelocity ) + * OnCarAlarmTriggered( survivor, infected, reason ) + * + * OnDeathChargeAssist( assister, charger, victim ) [ not done yet ] + * OnBHop( player, isInfected, speed, streak ) [ not done yet ] + * + * Where survivor == -2 if it was a team effort, -1 or 0 if unknown or invalid client. + * damage is the amount of damage done (that didn't add up to skeeting damage), + * and isOverkill indicates whether the shot would've been a skeet if the hunter + * had not been chipped. + * + * @author Tabun + * @libraryname skill_detect + */ + +#pragma semicolon 1 + +#include +#include +#include +#include + +#define PLUGIN_VERSION "1.0" + +#define IS_VALID_CLIENT(%1) (%1 > 0 && %1 <= MaxClients) +#define IS_SURVIVOR(%1) (GetClientTeam(%1) == 2) +#define IS_INFECTED(%1) (GetClientTeam(%1) == 3) +#define IS_VALID_INGAME(%1) (IS_VALID_CLIENT(%1) && IsClientInGame(%1)) +#define IS_VALID_SURVIVOR(%1) (IS_VALID_INGAME(%1) && IS_SURVIVOR(%1)) +#define IS_VALID_INFECTED(%1) (IS_VALID_INGAME(%1) && IS_INFECTED(%1)) +#define IS_SURVIVOR_ALIVE(%1) (IS_VALID_SURVIVOR(%1) && IsPlayerAlive(%1)) +#define IS_INFECTED_ALIVE(%1) (IS_VALID_INFECTED(%1) && IsPlayerAlive(%1)) +#define QUOTES(%1) (%1) + +#define SHOTGUN_BLAST_TIME 0.1 +#define POUNCE_CHECK_TIME 0.1 +#define HOP_CHECK_TIME 0.1 +#define HOPEND_CHECK_TIME 0.1 // after streak end (potentially) detected, to check for realz? +#define SHOVE_TIME 0.05 +#define MAX_CHARGE_TIME 12.0 // maximum time to pass before charge checking ends +#define CHARGE_CHECK_TIME 0.25 // check interval for survivors flying from impacts +#define CHARGE_END_CHECK 2.5 // after client hits ground after getting impact-charged: when to check whether it was a death +#define CHARGE_END_RECHECK 3.0 // safeguard wait to recheck on someone getting incapped out of bounds +#define VOMIT_DURATION_TIME 2.25 // how long the boomer vomit stream lasts -- when to check for boom count +#define ROCK_CHECK_TIME 0.34 // how long to wait after rock entity is destroyed before checking for skeet/eat (high to avoid lag issues) +#define CARALARM_MIN_TIME 0.11 // maximum time after touch/shot => alarm to connect the two events (test this for LAG) + +#define WITCH_CHECK_TIME 0.1 // time to wait before checking for witch crown after shoots fired +#define WITCH_DELETE_TIME 0.15 // time to wait before deleting entry from witch trie after entity is destroyed + +#define MIN_DC_TRIGGER_DMG 300 // minimum amount a 'trigger' / drown must do before counted as a death action +#define MIN_DC_FALL_DMG 175 // minimum amount of fall damage counts as death-falling for a deathcharge +#define WEIRD_FLOW_THRESH 900.0 // -9999 seems to be break flow.. but meh +#define MIN_FLOWDROPHEIGHT 350.0 // minimum height a survivor has to have dropped before a WEIRD_FLOW value is treated as a DC spot +#define MIN_DC_RECHECK_DMG 100 // minimum damage from map to have taken on first check, to warrant recheck + +#define HOP_ACCEL_THRESH 0.01 // bhop speed increase must be higher than this for it to count as part of a hop streak + +#define ZC_SMOKER 1 +#define ZC_BOOMER 2 +#define ZC_HUNTER 3 +#define ZC_JOCKEY 5 +#define ZC_CHARGER 6 +#define ZC_TANK 8 +#define HITGROUP_HEAD 1 + +#define DMG_CRUSH (1 << 0) // crushed by falling or moving object. +#define DMG_BULLET (1 << 1) // shot +#define DMG_SLASH (1 << 2) // cut, clawed, stabbed +#define DMG_CLUB (1 << 7) // crowbar, punch, headbutt +#define DMG_BUCKSHOT (1 << 29) // not quite a bullet. Little, rounder, different. + +#define DMGARRAYEXT 7 // MAXPLAYERS+# -- extra indices in witch_dmg_array + 1 + +#define CUT_SHOVED 1 // smoker got shoved +#define CUT_SHOVEDSURV 2 // survivor got shoved +#define CUT_KILL 3 // reason for tongue break (release_type) +#define CUT_SLASH 4 // this is used for others shoving a survivor free too, don't trust .. it involves tongue damage? + +#define VICFLG_CARRIED (1 << 0) // was the one that the charger carried (not impacted) +#define VICFLG_FALL (1 << 1) // flags stored per charge victim, to check for deathchargeroony -- fallen +#define VICFLG_DROWN (1 << 2) // drowned +#define VICFLG_HURTLOTS (1 << 3) // whether the victim was hurt by 400 dmg+ at once +#define VICFLG_TRIGGER (1 << 4) // killed by trigger_hurt +#define VICFLG_AIRDEATH (1 << 5) // died before they hit the ground (impact check) +#define VICFLG_KILLEDBYOTHER (1 << 6) // if the survivor was killed by an SI other than the charger +#define VICFLG_WEIRDFLOW (1 << 7) // when survivors get out of the map and such +#define VICFLG_WEIRDFLOWDONE (1 << 8) // checked, don't recheck for this + +#define REP_SKEET (1 << 0) +#define REP_HURTSKEET (1 << 1) +#define REP_LEVEL (1 << 2) +#define REP_HURTLEVEL (1 << 3) +#define REP_CROWN (1 << 4) +#define REP_DRAWCROWN (1 << 5) +#define REP_TONGUECUT (1 << 6) +#define REP_SELFCLEAR (1 << 7) +#define REP_SELFCLEARSHOVE (1 << 8) +#define REP_ROCKSKEET (1 << 9) +#define REP_DEADSTOP (1 << 10) +#define REP_POP (1 << 11) +#define REP_SHOVE (1 << 12) +#define REP_HUNTERDP (1 << 13) +#define REP_JOCKEYDP (1 << 14) +#define REP_DEATHCHARGE (1 << 15) +#define REP_DC_ASSIST (1 << 16) +#define REP_INSTACLEAR (1 << 17) // 131072 +#define REP_BHOPSTREAK (1 << 18) // 262144 +#define REP_CARALARM (1 << 19) // 524288 + +#define REP_DEFAULT "581685" // (REP_SKEET | REP_LEVEL | REP_CROWN | REP_DRAWCROWN | REP_HUNTERDP | REP_JOCKEYDP | REP_DEATHCHARGE | REP_CARALARM) + // 1 4 16 32 8192 16384 32768 65536 (122933 with ASSIST, 57397 without); 131072 for instaclears + 524288 for car alarm + + +// trie values: weapon type +enum strWeaponType +{ + WPTYPE_SNIPER, + WPTYPE_MAGNUM, + WPTYPE_GL +}; + +// trie values: OnEntityCreated classname +enum strOEC +{ + OEC_WITCH, + OEC_TANKROCK, + OEC_TRIGGER, + OEC_CARALARM, + OEC_CARGLASS +}; + +// trie values: special abilities +enum strAbility +{ + ABL_HUNTERLUNGE, + ABL_ROCKTHROW +}; + +enum +{ + rckDamage, + rckTank, + rckSkeeter, + strRockData +}; + +// witch array entries (maxplayers+index) +enum +{ + WTCH_NONE, + WTCH_HEALTH, + WTCH_GOTSLASH, + WTCH_STARTLED, + WTCH_CROWNER, + WTCH_CROWNSHOT, + WTCH_CROWNTYPE, + strWitchArray +}; + +enum +{ + CALARM_UNKNOWN, + CALARM_HIT, + CALARM_TOUCHED, + CALARM_EXPLOSION, + CALARM_BOOMER, + enAlarmReasons +}; + +new const String: g_csSIClassName[][] = +{ + "", + "smoker", + "boomer", + "hunter", + "spitter", + "jockey", + "charger", + "witch", + "tank" +}; + +new bool: g_bLateLoad = false; + +new Handle: g_hForwardSkeet = INVALID_HANDLE; +new Handle: g_hForwardSkeetHurt = INVALID_HANDLE; +new Handle: g_hForwardSkeetMelee = INVALID_HANDLE; +new Handle: g_hForwardSkeetMeleeHurt = INVALID_HANDLE; +new Handle: g_hForwardSkeetSniper = INVALID_HANDLE; +new Handle: g_hForwardSkeetSniperHurt = INVALID_HANDLE; +new Handle: g_hForwardSkeetGL = INVALID_HANDLE; +new Handle: g_hForwardHunterDeadstop = INVALID_HANDLE; +new Handle: g_hForwardSIShove = INVALID_HANDLE; +new Handle: g_hForwardBoomerPop = INVALID_HANDLE; +new Handle: g_hForwardLevel = INVALID_HANDLE; +new Handle: g_hForwardLevelHurt = INVALID_HANDLE; +new Handle: g_hForwardCrown = INVALID_HANDLE; +new Handle: g_hForwardDrawCrown = INVALID_HANDLE; +new Handle: g_hForwardTongueCut = INVALID_HANDLE; +new Handle: g_hForwardSmokerSelfClear = INVALID_HANDLE; +new Handle: g_hForwardRockSkeeted = INVALID_HANDLE; +new Handle: g_hForwardRockEaten = INVALID_HANDLE; +new Handle: g_hForwardHunterDP = INVALID_HANDLE; +new Handle: g_hForwardJockeyDP = INVALID_HANDLE; +new Handle: g_hForwardDeathCharge = INVALID_HANDLE; +new Handle: g_hForwardClear = INVALID_HANDLE; +new Handle: g_hForwardVomitLanded = INVALID_HANDLE; +new Handle: g_hForwardBHopStreak = INVALID_HANDLE; +new Handle: g_hForwardAlarmTriggered = INVALID_HANDLE; + +new Handle: g_hTrieWeapons = INVALID_HANDLE; // weapon check +new Handle: g_hTrieEntityCreated = INVALID_HANDLE; // getting classname of entity created +new Handle: g_hTrieAbility = INVALID_HANDLE; // ability check +new Handle: g_hWitchTrie = INVALID_HANDLE; // witch tracking (Crox) +new Handle: g_hRockTrie = INVALID_HANDLE; // tank rock tracking +new Handle: g_hCarTrie = INVALID_HANDLE; // car alarm tracking + +// all SI / pinners +new Float: g_fSpawnTime [MAXPLAYERS + 1]; // time the SI spawned up +new Float: g_fPinTime [MAXPLAYERS + 1][2]; // time the SI pinned a target: 0 = start of pin (tongue pull, charger carry); 1 = carry end / tongue reigned in +new g_iSpecialVictim [MAXPLAYERS + 1]; // current victim (set in traceattack, so we can check on death) + +// hunters: skeets/pounces +new g_iHunterShotDmgTeam [MAXPLAYERS + 1]; // counting shotgun blast damage for hunter, counting entire survivor team's damage +new g_iHunterShotDmg [MAXPLAYERS + 1][MAXPLAYERS + 1]; // counting shotgun blast damage for hunter / skeeter combo +new Float: g_fHunterShotStart [MAXPLAYERS + 1][MAXPLAYERS + 1]; // when the last shotgun blast on hunter started (if at any time) by an attacker +new Float: g_fHunterTracePouncing [MAXPLAYERS + 1]; // time when the hunter was still pouncing (in traceattack) -- used to detect pouncing status +new Float: g_fHunterLastShot [MAXPLAYERS + 1]; // when the last shotgun damage was done (by anyone) on a hunter +new g_iHunterLastHealth [MAXPLAYERS + 1]; // last time hunter took any damage, how much health did it have left? +new g_iHunterOverkill [MAXPLAYERS + 1]; // how much more damage a hunter would've taken if it wasn't already dead +new bool: g_bHunterKilledPouncing [MAXPLAYERS + 1]; // whether the hunter was killed when actually pouncing +new g_iPounceDamage [MAXPLAYERS + 1]; // how much damage on last 'highpounce' done +new Float: g_fPouncePosition [MAXPLAYERS + 1][3]; // position that a hunter (jockey?) pounced from (or charger started his carry) + +// deadstops +new Float: g_fVictimLastShove [MAXPLAYERS + 1][MAXPLAYERS + 1]; // when was the player shoved last by attacker? (to prevent doubles) + +// levels / charges +new g_iChargerHealth [MAXPLAYERS + 1]; // how much health the charger had the last time it was seen taking damage +new Float: g_fChargeTime [MAXPLAYERS + 1]; // time the charger's charge last started, or if victim, when impact started +new g_iChargeVictim [MAXPLAYERS + 1]; // who got charged +new Float: g_fChargeVictimPos [MAXPLAYERS + 1][3]; // location of each survivor when it got hit by the charger +new g_iVictimCharger [MAXPLAYERS + 1]; // for a victim, by whom they got charge(impacted) +new g_iVictimFlags [MAXPLAYERS + 1]; // flags stored per charge victim: VICFLAGS_ +new g_iVictimMapDmg [MAXPLAYERS + 1]; // for a victim, how much the cumulative map damage is so far (trigger hurt / drowning) + +// pops +new bool: g_bBoomerHitSomebody [MAXPLAYERS + 1]; // false if boomer didn't puke/exploded on anybody +new g_iBoomerGotShoved [MAXPLAYERS + 1]; // count boomer was shoved at any point +new g_iBoomerVomitHits [MAXPLAYERS + 1]; // how many booms in one vomit so far + +// crowns +new Float: g_fWitchShotStart [MAXPLAYERS + 1]; // when the last shotgun blast from a survivor started (on any witch) + +// smoker clears +new bool: g_bSmokerClearCheck [MAXPLAYERS + 1]; // [smoker] smoker dies and this is set, it's a self-clear if g_iSmokerVictim is the killer +new g_iSmokerVictim [MAXPLAYERS + 1]; // [smoker] the one that's being pulled +new g_iSmokerVictimDamage [MAXPLAYERS + 1]; // [smoker] amount of damage done to a smoker by the one he pulled +new bool: g_bSmokerShoved [MAXPLAYERS + 1]; // [smoker] set if the victim of a pull manages to shove the smoker + +// rocks +new g_iTankRock [MAXPLAYERS + 1]; // rock entity per tank +new g_iRocksBeingThrown [10]; // 10 tanks max simultanously throwing rocks should be ok (this stores the tank client) +new g_iRocksBeingThrownCount = 0; // so we can do a push/pop type check for who is throwing a created rock + +// hops +new bool: g_bIsHopping [MAXPLAYERS + 1]; // currently in a hop streak +new bool: g_bHopCheck [MAXPLAYERS + 1]; // flag to check whether a hopstreak has ended (if on ground for too long.. ends) +new g_iHops [MAXPLAYERS + 1]; // amount of hops in streak +new Float: g_fLastHop [MAXPLAYERS + 1][3]; // velocity vector of last jump +new Float: g_fHopTopVelocity [MAXPLAYERS + 1]; // maximum velocity in hopping streak + +// alarms +new Float: g_fLastCarAlarm = 0.0; // time when last car alarm went off +new g_iLastCarAlarmReason [MAXPLAYERS + 1]; // what this survivor did to set the last alarm off +new g_iLastCarAlarmBoomer; // if a boomer triggered an alarm, remember it + +// cvars +new Handle: g_hCvarReport = INVALID_HANDLE; // cvar whether to report at all +new Handle: g_hCvarReportFlags = INVALID_HANDLE; // cvar what to report + +new Handle: g_hCvarAllowMelee = INVALID_HANDLE; // cvar whether to count melee skeets +new Handle: g_hCvarAllowSniper = INVALID_HANDLE; // cvar whether to count sniper headshot skeets +new Handle: g_hCvarAllowGLSkeet = INVALID_HANDLE; // cvar whether to count direct hit GL skeets +new Handle: g_hCvarDrawCrownThresh = INVALID_HANDLE; // cvar damage in final shot for drawcrown-req. +new Handle: g_hCvarSelfClearThresh = INVALID_HANDLE; // cvar damage while self-clearing from smokers +new Handle: g_hCvarHunterDPThresh = INVALID_HANDLE; // cvar damage for hunter highpounce +new Handle: g_hCvarJockeyDPThresh = INVALID_HANDLE; // cvar distance for jockey highpounce +new Handle: g_hCvarHideFakeDamage = INVALID_HANDLE; // cvar damage while self-clearing from smokers +new Handle: g_hCvarDeathChargeHeight = INVALID_HANDLE; // cvar how high a charger must have come in order for a DC to count +new Handle: g_hCvarInstaTime = INVALID_HANDLE; // cvar clear within this time or lower for instaclear +new Handle: g_hCvarBHopMinStreak = INVALID_HANDLE; // cvar this many hops in a row+ = streak +new Handle: g_hCvarBHopMinInitSpeed = INVALID_HANDLE; // cvar lower than this and the first jump won't be seen as the start of a streak +new Handle: g_hCvarBHopContSpeed = INVALID_HANDLE; // cvar + +new Handle: g_hCvarPounceInterrupt = INVALID_HANDLE; // z_pounce_damage_interrupt +new g_iPounceInterrupt = 150; +new Handle: g_hCvarChargerHealth = INVALID_HANDLE; // z_charger_health +new Handle: g_hCvarWitchHealth = INVALID_HANDLE; // z_witch_health +new Handle: g_hCvarMaxPounceDistance = INVALID_HANDLE; // z_pounce_damage_range_max +new Handle: g_hCvarMinPounceDistance = INVALID_HANDLE; // z_pounce_damage_range_min +new Handle: g_hCvarMaxPounceDamage = INVALID_HANDLE; // z_hunter_max_pounce_bonus_damage; + + +/* + Reports: + -------- + Damage shown is damage done in the last shot/slash. So for crowns, this means + that the 'damage' value is one shotgun blast + + + Quirks: + ------- + Does not report people cutting smoker tongues that target players other + than themselves. Could be done, but would require (too much) tracking. + + Actual damage done, on Hunter DPs, is low when the survivor gets incapped + by (a fraction of) the total pounce damage. + + + Fake Damage + ----------- + Hiding of fake damage has the following consequences: + - Drawcrowns are less likely to be registered: if a witch takes too + much chip before the crowning shot, the final shot will be considered + as doing too little damage for a crown (even if it would have been a crown + had the witch had more health). + - Charger levels are harder to get on chipped chargers. Any charger that + has taken (600 - 390 =) 210 damage or more cannot be leveled (even if + the melee swing would've killed the charger (1559 damage) if it'd have + had full health). + I strongly recommend leaving fakedamage visible: it will offer more feedback on + the survivor's action and reward survivors doing (what would be) full crowns and + levels on chipped targets. + + + To Do + ----- + + - fix: tank rock owner is not reliable for the RockEaten forward + - fix: tank rock skeets still unreliable detection (often triggers a 'skeet' when actually landed on someone) + + - fix: apparently some HR4 cars generate car alarm messages when shot, even when no alarm goes off + (combination with car equalize plugin?) + - see below: the single hook might also fix this.. -- if not, hook for sound + - do a hookoutput on prop_car_alarm's and use that to track the actual alarm + going off (might help in the case 2 alarms go off exactly at the same time?) + - fix: double prints on car alarms (sometimes? epi + m60) + + - fix: sometimes instaclear reports double for single clear (0.16s / 0.19s) epi saw this, was for hunter + - fix: deadstops and m2s don't always register .. no idea why.. + - fix: sometimes a (first?) round doesn't work for skeet detection.. no hurt/full skeets are reported or counted + + - make forwards fire for every potential action, + - include the relevant values, so other plugins can decide for themselves what to consider it + + - test chargers getting dislodged with boomer pops? + + - add commonhop check + - add deathcharge assist check + - smoker + - jockey + + - add deathcharge coordinates for some areas + - DT4 next to saferoom + - DA1 near the lower roof, on sidewalk next to fence (no hurttrigger there) + - DA2 next to crane roof to the right of window + DA2 charge down into start area, after everyone's jumped the fence + + - count rock hits even if they do no damage [epi request] + - sir + - make separate teamskeet forward, with (for now, up to) 4 skeeters + the damage each did + - xan + - add detection/display of unsuccesful witch crowns (witch death + info) + + detect... + - ? add jockey deadstops (and change forward to reflect type) + - ? speedcrown detection? + - ? spit-on-cap detection + + --- + done: + - applied sanity bounds to calculated damage for hunter dps + - removed tank's name from rock skeet print + - 300+ speed hops are considered hops even if no increase +*/ + +public Plugin:myinfo = +{ + name = "Skill Detection (skeets, crowns, levels)", + author = "Tabun", + description = "Detects and reports skeets, crowns, levels, highpounces, etc.", + version = PLUGIN_VERSION, + url = "https://github.com/Tabbernaut/L4D2-Plugins" +} + +public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) +{ + RegPluginLibrary("skill_detect"); + + g_hForwardSkeet = CreateGlobalForward("OnSkeet", ET_Ignore, Param_Cell, Param_Cell ); + g_hForwardSkeetHurt = CreateGlobalForward("OnSkeetHurt", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell ); + g_hForwardSkeetMelee = CreateGlobalForward("OnSkeetMelee", ET_Ignore, Param_Cell, Param_Cell ); + g_hForwardSkeetMeleeHurt = CreateGlobalForward("OnSkeetMeleeHurt", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell ); + g_hForwardSkeetSniper = CreateGlobalForward("OnSkeetSniper", ET_Ignore, Param_Cell, Param_Cell ); + g_hForwardSkeetSniperHurt = CreateGlobalForward("OnSkeetSniperHurt", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell ); + g_hForwardSkeetGL = CreateGlobalForward("OnSkeetGL", ET_Ignore, Param_Cell, Param_Cell ); + g_hForwardSIShove = CreateGlobalForward("OnSpecialShoved", ET_Ignore, Param_Cell, Param_Cell, Param_Cell ); + g_hForwardHunterDeadstop = CreateGlobalForward("OnHunterDeadstop", ET_Ignore, Param_Cell, Param_Cell ); + g_hForwardBoomerPop = CreateGlobalForward("OnBoomerPop", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Float ); + g_hForwardLevel = CreateGlobalForward("OnChargerLevel", ET_Ignore, Param_Cell, Param_Cell ); + g_hForwardLevelHurt = CreateGlobalForward("OnChargerLevelHurt", ET_Ignore, Param_Cell, Param_Cell, Param_Cell ); + g_hForwardCrown = CreateGlobalForward("OnWitchCrown", ET_Ignore, Param_Cell, Param_Cell ); + g_hForwardDrawCrown = CreateGlobalForward("OnWitchDrawCrown", ET_Ignore, Param_Cell, Param_Cell, Param_Cell ); + g_hForwardTongueCut = CreateGlobalForward("OnTongueCut", ET_Ignore, Param_Cell, Param_Cell ); + g_hForwardSmokerSelfClear = CreateGlobalForward("OnSmokerSelfClear", ET_Ignore, Param_Cell, Param_Cell, Param_Cell ); + g_hForwardRockSkeeted = CreateGlobalForward("OnTankRockSkeeted", ET_Ignore, Param_Cell, Param_Cell ); + g_hForwardRockEaten = CreateGlobalForward("OnTankRockEaten", ET_Ignore, Param_Cell, Param_Cell ); + g_hForwardHunterDP = CreateGlobalForward("OnHunterHighPounce", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Float, Param_Float, Param_Cell, Param_Cell ); + g_hForwardJockeyDP = CreateGlobalForward("OnJockeyHighPounce", ET_Ignore, Param_Cell, Param_Cell, Param_Float, Param_Cell ); + g_hForwardDeathCharge = CreateGlobalForward("OnDeathCharge", ET_Ignore, Param_Cell, Param_Cell, Param_Float, Param_Float, Param_Cell ); + g_hForwardClear = CreateGlobalForward("OnSpecialClear", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Float, Param_Float, Param_Cell ); + g_hForwardVomitLanded = CreateGlobalForward("OnBoomerVomitLanded", ET_Ignore, Param_Cell, Param_Cell ); + g_hForwardBHopStreak = CreateGlobalForward("OnBunnyHopStreak", ET_Ignore, Param_Cell, Param_Cell, Param_Float ); + g_hForwardAlarmTriggered = CreateGlobalForward("OnCarAlarmTriggered", ET_Ignore, Param_Cell, Param_Cell, Param_Cell ); + g_bLateLoad = late; + + return APLRes_Success; +} + +public OnPluginStart() +{ + // hooks + HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy); + HookEvent("scavenge_round_start", Event_RoundStart, EventHookMode_PostNoCopy); + HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy); + + HookEvent("player_spawn", Event_PlayerSpawn, EventHookMode_Post); + HookEvent("player_hurt", Event_PlayerHurt, EventHookMode_Pre); + HookEvent("player_death", Event_PlayerDeath, EventHookMode_Pre); + HookEvent("ability_use", Event_AbilityUse, EventHookMode_Post); + HookEvent("lunge_pounce", Event_LungePounce, EventHookMode_Post); + HookEvent("player_shoved", Event_PlayerShoved, EventHookMode_Post); + HookEvent("player_jump", Event_PlayerJumped, EventHookMode_Post); + HookEvent("player_jump_apex", Event_PlayerJumpApex, EventHookMode_Post); + + HookEvent("player_now_it", Event_PlayerBoomed, EventHookMode_Post); + HookEvent("boomer_exploded", Event_BoomerExploded, EventHookMode_Post); + + //HookEvent("infected_hurt", Event_InfectedHurt, EventHookMode_Post); + HookEvent("witch_spawn", Event_WitchSpawned, EventHookMode_Post); + HookEvent("witch_killed", Event_WitchKilled, EventHookMode_Post); + HookEvent("witch_harasser_set", Event_WitchHarasserSet, EventHookMode_Post); + + HookEvent("tongue_grab", Event_TongueGrab, EventHookMode_Post); + HookEvent("tongue_pull_stopped", Event_TonguePullStopped, EventHookMode_Post); + HookEvent("choke_start", Event_ChokeStart, EventHookMode_Post); + HookEvent("choke_stopped", Event_ChokeStop, EventHookMode_Post); + HookEvent("jockey_ride", Event_JockeyRide, EventHookMode_Post); + HookEvent("charger_carry_start", Event_ChargeCarryStart, EventHookMode_Post); + HookEvent("charger_carry_end", Event_ChargeCarryEnd, EventHookMode_Post); + HookEvent("charger_impact", Event_ChargeImpact, EventHookMode_Post); + HookEvent("charger_pummel_start", Event_ChargePummelStart, EventHookMode_Post); + + HookEvent("player_incapacitated_start", Event_IncapStart, EventHookMode_Post); + HookEvent("triggered_car_alarm", Event_CarAlarmGoesOff, EventHookMode_Post); + + + // version cvar + CreateConVar( "sm_skill_detect_version", PLUGIN_VERSION, "Skill detect plugin version.", FCVAR_NONE|FCVAR_NOTIFY|FCVAR_REPLICATED|FCVAR_DONTRECORD ); + + // cvars: config + + g_hCvarReport = CreateConVar( "sm_skill_report_enable" , "0", "Whether to report in chat (see sm_skill_report_flags).", FCVAR_NONE, true, 0.0, true, 1.0 ); + g_hCvarReportFlags = CreateConVar( "sm_skill_report_flags" , REP_DEFAULT, "What to report skeets in chat (bitflags: 1,2:skeets/hurt; 4,8:level/chip; 16,32:crown/draw; 64,128:cut/selfclear, ... ).", FCVAR_NONE, true, 0.0 ); + + g_hCvarAllowMelee = CreateConVar( "sm_skill_skeet_allowmelee", "1", "Whether to count/forward melee skeets.", FCVAR_NONE, true, 0.0, true, 1.0 ); + g_hCvarAllowSniper = CreateConVar( "sm_skill_skeet_allowsniper", "1", "Whether to count/forward sniper/magnum headshots as skeets.", FCVAR_NONE, true, 0.0, true, 1.0 ); + g_hCvarAllowGLSkeet = CreateConVar( "sm_skill_skeet_allowgl", "1", "Whether to count/forward direct GL hits as skeets.", FCVAR_NONE, true, 0.0, true, 1.0 ); + g_hCvarDrawCrownThresh = CreateConVar( "sm_skill_drawcrown_damage", "500", "How much damage a survivor must at least do in the final shot for it to count as a drawcrown.", FCVAR_NONE, true, 0.0, false ); + g_hCvarSelfClearThresh = CreateConVar( "sm_skill_selfclear_damage", "200", "How much damage a survivor must at least do to a smoker for him to count as self-clearing.", FCVAR_NONE, true, 0.0, false ); + g_hCvarHunterDPThresh = CreateConVar( "sm_skill_hunterdp_height", "400", "Minimum height of hunter pounce for it to count as a DP.", FCVAR_NONE, true, 0.0, false ); + g_hCvarJockeyDPThresh = CreateConVar( "sm_skill_jockeydp_height", "300", "How much height distance a jockey must make for his 'DP' to count as a reportable highpounce.", FCVAR_NONE, true, 0.0, false ); + g_hCvarHideFakeDamage = CreateConVar( "sm_skill_hidefakedamage", "0", "If set, any damage done that exceeds the health of a victim is hidden in reports.", FCVAR_NONE, true, 0.0, true, 1.0 ); + g_hCvarDeathChargeHeight = CreateConVar("sm_skill_deathcharge_height","400", "How much height distance a charger must take its victim for a deathcharge to be reported.", FCVAR_NONE, true, 0.0, false ); + g_hCvarInstaTime = CreateConVar( "sm_skill_instaclear_time", "0.75", "A clear within this time (in seconds) counts as an insta-clear.", FCVAR_NONE, true, 0.0, false ); + g_hCvarBHopMinStreak = CreateConVar( "sm_skill_bhopstreak", "3", "The lowest bunnyhop streak that will be reported.", FCVAR_NONE, true, 0.0, false ); + g_hCvarBHopMinInitSpeed = CreateConVar( "sm_skill_bhopinitspeed", "150", "The minimal speed of the first jump of a bunnyhopstreak (0 to allow 'hops' from standstill).", FCVAR_NONE, true, 0.0, false ); + g_hCvarBHopContSpeed = CreateConVar( "sm_skill_bhopkeepspeed", "300", "The minimal speed at which hops are considered succesful even if not speed increase is made.", FCVAR_NONE, true, 0.0, false ); + + // cvars: built in + g_hCvarPounceInterrupt = FindConVar("z_pounce_damage_interrupt"); + HookConVarChange(g_hCvarPounceInterrupt, CvarChange_PounceInterrupt); + g_iPounceInterrupt = GetConVarInt(g_hCvarPounceInterrupt); + + g_hCvarChargerHealth = FindConVar("z_charger_health"); + g_hCvarWitchHealth = FindConVar("z_witch_health"); + + g_hCvarMaxPounceDistance = FindConVar("z_pounce_damage_range_max"); + g_hCvarMinPounceDistance = FindConVar("z_pounce_damage_range_min"); + g_hCvarMaxPounceDamage = FindConVar("z_hunter_max_pounce_bonus_damage"); + if ( g_hCvarMaxPounceDistance == INVALID_HANDLE ) { g_hCvarMaxPounceDistance = CreateConVar( "z_pounce_damage_range_max", "1000.0", "Not available on this server, added by l4d2_skill_detect.", FCVAR_NONE, true, 0.0, false ); } + if ( g_hCvarMinPounceDistance == INVALID_HANDLE ) { g_hCvarMinPounceDistance = CreateConVar( "z_pounce_damage_range_min", "300.0", "Not available on this server, added by l4d2_skill_detect.", FCVAR_NONE, true, 0.0, false ); } + if ( g_hCvarMaxPounceDamage == INVALID_HANDLE ) { g_hCvarMaxPounceDamage = CreateConVar( "z_hunter_max_pounce_bonus_damage", "49", "Not available on this server, added by l4d2_skill_detect.", FCVAR_NONE, true, 0.0, false ); } + + + // tries + g_hTrieWeapons = CreateTrie(); + SetTrieValue(g_hTrieWeapons, "hunting_rifle", WPTYPE_SNIPER); + SetTrieValue(g_hTrieWeapons, "sniper_military", WPTYPE_SNIPER); + SetTrieValue(g_hTrieWeapons, "sniper_awp", WPTYPE_SNIPER); + SetTrieValue(g_hTrieWeapons, "sniper_scout", WPTYPE_SNIPER); + SetTrieValue(g_hTrieWeapons, "pistol_magnum", WPTYPE_MAGNUM); + SetTrieValue(g_hTrieWeapons, "grenade_launcher_projectile", WPTYPE_GL); + + g_hTrieEntityCreated = CreateTrie(); + SetTrieValue(g_hTrieEntityCreated, "tank_rock", OEC_TANKROCK); + SetTrieValue(g_hTrieEntityCreated, "witch", OEC_WITCH); + SetTrieValue(g_hTrieEntityCreated, "trigger_hurt", OEC_TRIGGER); + SetTrieValue(g_hTrieEntityCreated, "prop_car_alarm", OEC_CARALARM); + SetTrieValue(g_hTrieEntityCreated, "prop_car_glass", OEC_CARGLASS); + + g_hTrieAbility = CreateTrie(); + SetTrieValue(g_hTrieAbility, "ability_lunge", ABL_HUNTERLUNGE); + SetTrieValue(g_hTrieAbility, "ability_throw", ABL_ROCKTHROW); + + g_hWitchTrie = CreateTrie(); + g_hRockTrie = CreateTrie(); + g_hCarTrie = CreateTrie(); + + if ( g_bLateLoad ) + { + for ( new client = 1; client <= MaxClients; client++ ) + { + if ( IS_VALID_INGAME(client) ) + { + SDKHook( client, SDKHook_OnTakeDamage, OnTakeDamageByWitch ); + } + } + } + + AutoExecConfig(true, "l4d2_skill_detect"); +} + +public CvarChange_PounceInterrupt( Handle:convar, const String:oldValue[], const String:newValue[] ) +{ + g_iPounceInterrupt = GetConVarInt(convar); +} + +public OnClientPostAdminCheck(client) +{ + SDKHook(client, SDKHook_OnTakeDamage, OnTakeDamageByWitch); +} + +public OnClientDisconnect(client) +{ + SDKUnhook(client, SDKHook_OnTakeDamage, OnTakeDamageByWitch); +} + + + +/* + Tracking + -------- +*/ + +public Action: Event_RoundStart( Handle:event, const String:name[], bool:dontBroadcast ) +{ + g_iRocksBeingThrownCount = 0; + + for ( new i = 1; i <= MaxClients; i++ ) + { + g_bIsHopping[i] = false; + + for ( new j = 1; j <= MaxClients; j++ ) + { + g_fVictimLastShove[i][j] = 0.0; + } + } +} + +public Action: Event_RoundEnd( Handle:event, const String:name[], bool:dontBroadcast ) +{ + // clean trie, new cars will be created + ClearTrie(g_hCarTrie); +} + +public Action: Event_PlayerHurt( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + new zClass; + + new damage = GetEventInt(event, "dmg_health"); + new damagetype = GetEventInt(event, "type"); + + if ( IS_VALID_INFECTED(victim) ) + { + zClass = GetEntProp(victim, Prop_Send, "m_zombieClass"); + new health = GetEventInt(event, "health"); + new hitgroup = GetEventInt(event, "hitgroup"); + + if ( damage < 1 ) { return Plugin_Continue; } + + switch ( zClass ) + { + case ZC_HUNTER: + { + // if it's not a survivor doing the work, only get the remaining health + if ( !IS_VALID_SURVIVOR(attacker) ) + { + g_iHunterLastHealth[victim] = health; + return Plugin_Continue; + } + + // if the damage done is greater than the health we know the hunter to have remaining, reduce the damage done + if ( g_iHunterLastHealth[victim] > 0 && damage > g_iHunterLastHealth[victim] ) + { + damage = g_iHunterLastHealth[victim]; + g_iHunterOverkill[victim] = g_iHunterLastHealth[victim] - damage; + g_iHunterLastHealth[victim] = 0; + } + + /* + handle old shotgun blast: too long ago? not the same blast + */ + if ( g_iHunterShotDmg[victim][attacker] > 0 && (GetGameTime() - g_fHunterShotStart[victim][attacker]) > SHOTGUN_BLAST_TIME ) + { + g_fHunterShotStart[victim][attacker] = 0.0; + } + + /* + m_isAttemptingToPounce is set to 0 here if the hunter is actually skeeted + so the g_fHunterTracePouncing[victim] value indicates when the hunter was last seen pouncing in traceattack + (should be DIRECTLY before this event for every shot). + */ + new bool: isPouncing = bool:( + GetEntProp(victim, Prop_Send, "m_isAttemptingToPounce") || + g_fHunterTracePouncing[victim] != 0.0 && ( GetGameTime() - g_fHunterTracePouncing[victim] ) < 0.001 + ); + + if ( isPouncing ) + { + if ( damagetype & DMG_BUCKSHOT ) + { + // first pellet hit? + if ( g_fHunterShotStart[victim][attacker] == 0.0 ) + { + // new shotgun blast + g_fHunterShotStart[victim][attacker] = GetGameTime(); + g_fHunterLastShot[victim] = g_fHunterShotStart[victim][attacker]; + } + g_iHunterShotDmg[victim][attacker] += damage; + g_iHunterShotDmgTeam[victim] += damage; + + if ( health == 0 ) { + g_bHunterKilledPouncing[victim] = true; + } + } + else if ( damagetype & (DMG_BLAST | DMG_PLASMA) && health == 0 ) + { + // direct GL hit? + /* + direct hit is DMG_BLAST | DMG_PLASMA + indirect hit is DMG_AIRBOAT + */ + + decl String: weaponB[32]; + new strWeaponType: weaponTypeB; + GetEventString(event, "weapon", weaponB, sizeof(weaponB)); + + if ( GetTrieValue(g_hTrieWeapons, weaponB, weaponTypeB) && weaponTypeB == WPTYPE_GL ) + { + if ( GetConVarBool(g_hCvarAllowGLSkeet) ) { + HandleSkeet( attacker, victim, false, false, true ); + } + } + } + else if ( damagetype & DMG_BULLET && + health == 0 && + hitgroup == HITGROUP_HEAD + ) { + // headshot with bullet based weapon (only single shots) -- only snipers + decl String: weaponA[32]; + new strWeaponType: weaponTypeA; + GetEventString(event, "weapon", weaponA, sizeof(weaponA)); + + if ( GetTrieValue(g_hTrieWeapons, weaponA, weaponTypeA) && + ( weaponTypeA == WPTYPE_SNIPER || + weaponTypeA == WPTYPE_MAGNUM ) + ) { + if ( damage >= g_iPounceInterrupt ) + { + g_iHunterShotDmgTeam[victim] = 0; + if ( GetConVarBool(g_hCvarAllowSniper) ) { + HandleSkeet( attacker, victim, false, true ); + } + ResetHunter(victim); + } + else + { + // hurt skeet + if ( GetConVarBool(g_hCvarAllowSniper) ) { + HandleNonSkeet( attacker, victim, damage, ( g_iHunterOverkill[victim] + g_iHunterShotDmgTeam[victim] > g_iPounceInterrupt ), false, true ); + } + ResetHunter(victim); + } + } + + // already handled hurt skeet above + //g_bHunterKilledPouncing[victim] = true; + } + else if ( damagetype & DMG_SLASH || damagetype & DMG_CLUB ) + { + // melee skeet + if ( damage >= g_iPounceInterrupt ) + { + g_iHunterShotDmgTeam[victim] = 0; + if ( GetConVarBool(g_hCvarAllowMelee) ) { + HandleSkeet( attacker, victim, true ); + } + ResetHunter(victim); + //g_bHunterKilledPouncing[victim] = true; + } + else if ( health == 0 ) + { + // hurt skeet (always overkill) + if ( GetConVarBool(g_hCvarAllowMelee) ) { + HandleNonSkeet( attacker, victim, damage, true, true, false ); + } + ResetHunter(victim); + } + } + } + else if ( health == 0 ) + { + // make sure we don't mistake non-pouncing hunters as 'not skeeted'-warnable + g_bHunterKilledPouncing[victim] = false; + } + + // store last health seen for next damage event + g_iHunterLastHealth[victim] = health; + } + + case ZC_CHARGER: + { + if ( IS_VALID_SURVIVOR(attacker) ) + { + // check for levels + if ( health == 0 && ( damagetype & DMG_CLUB || damagetype & DMG_SLASH ) ) + { + new iChargeHealth = GetConVarInt(g_hCvarChargerHealth); + new abilityEnt = GetEntPropEnt( victim, Prop_Send, "m_customAbility" ); + if ( IsValidEntity(abilityEnt) && GetEntProp(abilityEnt, Prop_Send, "m_isCharging") ) + { + // fix fake damage? + if ( GetConVarBool(g_hCvarHideFakeDamage) ) + { + damage = iChargeHealth - g_iChargerHealth[victim]; + } + + // charger was killed, was it a full level? + if ( damage > (iChargeHealth * 0.65) ) { + HandleLevel( attacker, victim ); + } + else { + HandleLevelHurt( attacker, victim, damage ); + } + } + } + } + + // store health for next damage it takes + if ( health > 0 ) + { + g_iChargerHealth[victim] = health; + } + } + + case ZC_SMOKER: + { + if ( !IS_VALID_SURVIVOR(attacker) ) { return Plugin_Continue; } + + g_iSmokerVictimDamage[victim] += damage; + } + + } + } + else if ( IS_VALID_INFECTED(attacker) ) + { + zClass = GetEntProp(attacker, Prop_Send, "m_zombieClass"); + + switch ( zClass ) + { + case ZC_HUNTER: + { + // a hunter pounce landing is DMG_CRUSH + if ( damagetype & DMG_CRUSH ) { + g_iPounceDamage[attacker] = damage; + } + } + + case ZC_TANK: + { + new String: weapon[10]; + GetEventString(event, "weapon", weapon, sizeof(weapon)); + + if ( StrEqual(weapon, "tank_rock") ) + { + // find rock entity through tank + if ( g_iTankRock[attacker] ) + { + // remember that the rock wasn't shot + decl String:rock_key[10]; + FormatEx(rock_key, sizeof(rock_key), "%x", g_iTankRock[attacker]); + new rock_array[3]; + rock_array[rckDamage] = -1; + SetTrieArray(g_hRockTrie, rock_key, rock_array, sizeof(rock_array), true); + } + + if ( IS_VALID_SURVIVOR(victim) ) + { + HandleRockEaten( attacker, victim ); + } + } + + return Plugin_Continue; + } + } + } + + // check for deathcharge flags + if ( IS_VALID_SURVIVOR(victim) ) + { + // debug + if ( damagetype & DMG_DROWN || damagetype & DMG_FALL ) { + g_iVictimMapDmg[victim] += damage; + } + + if ( damagetype & DMG_DROWN && damage >= MIN_DC_TRIGGER_DMG ) + { + g_iVictimFlags[victim] = g_iVictimFlags[victim] | VICFLG_HURTLOTS; + } + else if ( damagetype & DMG_FALL && damage >= MIN_DC_FALL_DMG ) + { + g_iVictimFlags[victim] = g_iVictimFlags[victim] | VICFLG_HURTLOTS; + } + } + + return Plugin_Continue; +} + +public Action: Event_PlayerSpawn( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if ( !IS_VALID_INFECTED(client) ) { return Plugin_Continue; } + + new zClass = GetEntProp(client, Prop_Send, "m_zombieClass"); + + g_fSpawnTime[client] = GetGameTime(); + g_fPinTime[client][0] = 0.0; + g_fPinTime[client][1] = 0.0; + + switch ( zClass ) + { + case ZC_BOOMER: + { + g_bBoomerHitSomebody[client] = false; + g_iBoomerGotShoved[client] = 0; + } + case ZC_SMOKER: + { + g_bSmokerClearCheck[client] = false; + g_iSmokerVictim[client] = 0; + g_iSmokerVictimDamage[client] = 0; + } + case ZC_HUNTER: + { + SDKHook(client, SDKHook_TraceAttack, TraceAttack_Hunter); + + g_fPouncePosition[client][0] = 0.0; + g_fPouncePosition[client][1] = 0.0; + g_fPouncePosition[client][2] = 0.0; + } + case ZC_JOCKEY: + { + SDKHook(client, SDKHook_TraceAttack, TraceAttack_Jockey); + + g_fPouncePosition[client][0] = 0.0; + g_fPouncePosition[client][1] = 0.0; + g_fPouncePosition[client][2] = 0.0; + } + case ZC_CHARGER: + { + SDKHook(client, SDKHook_TraceAttack, TraceAttack_Charger); + + g_iChargerHealth[client] = GetConVarInt(g_hCvarChargerHealth); + } + } + + return Plugin_Continue; +} + +// player about to get incapped +public Action: Event_IncapStart( Handle:event, const String:name[], bool:dontBroadcast ) +{ + // test for deathcharges + + new client = GetClientOfUserId( GetEventInt(event, "userid") ); + //new attacker = GetClientOfUserId( GetEventInt(event, "attacker") ); + new attackent = GetEventInt(event, "attackerentid"); + new dmgtype = GetEventInt(event, "type"); + + new String: classname[24]; + new strOEC: classnameOEC; + if ( IsValidEntity(attackent) ) { + GetEdictClassname(attackent, classname, sizeof(classname)); + if ( GetTrieValue(g_hTrieEntityCreated, classname, classnameOEC)) { + g_iVictimFlags[client] = g_iVictimFlags[client] | VICFLG_TRIGGER; + } + } + + new Float: flow = GetSurvivorDistance(client); + + //PrintDebug( 3, "Incap Pre on [%N]: attk: %i / %i (%s) - dmgtype: %i - flow: %.1f", client, attacker, attackent, classname, dmgtype, flow ); + + // drown is damage type + if ( dmgtype & DMG_DROWN ) + { + g_iVictimFlags[client] = g_iVictimFlags[client] | VICFLG_DROWN; + } + if ( flow < WEIRD_FLOW_THRESH ) + { + g_iVictimFlags[client] = g_iVictimFlags[client] | VICFLG_WEIRDFLOW; + } +} + +// trace attacks on hunters +public Action: TraceAttack_Hunter (victim, &attacker, &inflictor, &Float:damage, &damagetype, &ammotype, hitbox, hitgroup) +{ + // track pinning + g_iSpecialVictim[victim] = GetEntPropEnt(victim, Prop_Send, "m_pounceVictim"); + + if ( !IS_VALID_SURVIVOR(attacker) || !IsValidEdict(inflictor) ) { return; } + + // track flight + if ( GetEntProp(victim, Prop_Send, "m_isAttemptingToPounce") ) + { + g_fHunterTracePouncing[victim] = GetGameTime(); + } + else + { + g_fHunterTracePouncing[victim] = 0.0; + } +} +public Action: TraceAttack_Charger (victim, &attacker, &inflictor, &Float:damage, &damagetype, &ammotype, hitbox, hitgroup) +{ + // track pinning + new victimA = GetEntPropEnt(victim, Prop_Send, "m_carryVictim"); + if ( victimA != -1 ) { + g_iSpecialVictim[victim] = victimA; + } else { + g_iSpecialVictim[victim] = GetEntPropEnt(victim, Prop_Send, "m_pummelVictim"); + } + +} +public Action: TraceAttack_Jockey (victim, &attacker, &inflictor, &Float:damage, &damagetype, &ammotype, hitbox, hitgroup) +{ + // track pinning + g_iSpecialVictim[victim] = GetEntPropEnt(victim, Prop_Send, "m_jockeyVictim"); +} + +public Action: Event_PlayerDeath( Handle:hEvent, const String:name[], bool:dontBroadcast ) +{ + new victim = GetClientOfUserId( GetEventInt(hEvent, "userid") ); + new attacker = GetClientOfUserId( GetEventInt(hEvent, "attacker") ); + + if ( IS_VALID_INFECTED(victim) ) + { + new zClass = GetEntProp(victim, Prop_Send, "m_zombieClass"); + + switch ( zClass ) + { + case ZC_HUNTER: + { + if ( !IS_VALID_SURVIVOR(attacker) ) { return Plugin_Continue; } + + if ( g_iHunterShotDmgTeam[victim] > 0 && g_bHunterKilledPouncing[victim] ) + { + // skeet? + if ( g_iHunterShotDmgTeam[victim] > g_iHunterShotDmg[victim][attacker] && + g_iHunterShotDmgTeam[victim] >= g_iPounceInterrupt + ) { + // team skeet + HandleSkeet( -2, victim ); + } + else if ( g_iHunterShotDmg[victim][attacker] >= g_iPounceInterrupt ) + { + // single player skeet + HandleSkeet( attacker, victim ); + } + else if ( g_iHunterOverkill[victim] > 0 ) + { + // overkill? might've been a skeet, if it wasn't on a hurt hunter (only for shotguns) + HandleNonSkeet( attacker, victim, g_iHunterShotDmgTeam[victim], ( g_iHunterOverkill[victim] + g_iHunterShotDmgTeam[victim] > g_iPounceInterrupt ) ); + } + else + { + // not a skeet at all + HandleNonSkeet( attacker, victim, g_iHunterShotDmg[victim][attacker] ); + } + } + else { + // check whether it was a clear + if ( g_iSpecialVictim[victim] > 0 ) + { + HandleClear( attacker, victim, g_iSpecialVictim[victim], + ZC_HUNTER, + ( GetGameTime() - g_fPinTime[victim][0]), + -1.0 + ); + } + } + + ResetHunter(victim); + } + + case ZC_SMOKER: + { + if ( !IS_VALID_SURVIVOR(attacker) ) { return Plugin_Continue; } + + if ( g_bSmokerClearCheck[victim] && + g_iSmokerVictim[victim] == attacker && + g_iSmokerVictimDamage[victim] >= GetConVarInt(g_hCvarSelfClearThresh) + ) { + HandleSmokerSelfClear( attacker, victim ); + } + else + { + g_bSmokerClearCheck[victim] = false; + g_iSmokerVictim[victim] = 0; + } + } + + case ZC_JOCKEY: + { + // check whether it was a clear + if ( g_iSpecialVictim[victim] > 0 ) + { + HandleClear( attacker, victim, g_iSpecialVictim[victim], + ZC_JOCKEY, + ( GetGameTime() - g_fPinTime[victim][0]), + -1.0 + ); + } + } + + case ZC_CHARGER: + { + // is it someone carrying a survivor (that might be DC'd)? + // switch charge victim to 'impact' check (reset checktime) + if ( IS_VALID_INGAME(g_iChargeVictim[victim]) ) { + g_fChargeTime[ g_iChargeVictim[victim] ] = GetGameTime(); + } + + // check whether it was a clear + if ( g_iSpecialVictim[victim] > 0 ) + { + HandleClear( attacker, victim, g_iSpecialVictim[victim], + ZC_CHARGER, + (g_fPinTime[victim][1] > 0.0) ? ( GetGameTime() - g_fPinTime[victim][1]) : -1.0, + ( GetGameTime() - g_fPinTime[victim][0]) + ); + } + } + } + } + else if ( IS_VALID_SURVIVOR(victim) ) + { + // check for deathcharges + //new atkent = GetEventInt(hEvent, "attackerentid"); + new dmgtype = GetEventInt(hEvent, "type"); + + //PrintDebug( 3, "Died [%N]: attk: %i / %i - dmgtype: %i", victim, attacker, atkent, dmgtype ); + + if ( dmgtype & DMG_FALL) + { + g_iVictimFlags[victim] = g_iVictimFlags[victim] | VICFLG_FALL; + } + else if ( IS_VALID_INFECTED(attacker) && attacker != g_iVictimCharger[victim] ) + { + // if something other than the charger killed them, remember (not a DC) + g_iVictimFlags[victim] = g_iVictimFlags[victim] | VICFLG_KILLEDBYOTHER; + } + } + + return Plugin_Continue; +} + +public Action: Event_PlayerShoved( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new victim = GetClientOfUserId(GetEventInt(event, "userid")); + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + + //PrintDebug(1, "Shove from %i on %i", attacker, victim); + + if ( !IS_VALID_SURVIVOR(attacker) || !IS_VALID_INFECTED(victim) ) { return Plugin_Continue; } + + new zClass = GetEntProp(victim, Prop_Send, "m_zombieClass"); + + //PrintDebug(1, " --> Shove from %N on %N (class: %i) -- (last shove time: %.2f / %.2f)", attacker, victim, zClass, g_fVictimLastShove[victim][attacker], ( GetGameTime() - g_fVictimLastShove[victim][attacker] ) ); + + // track on boomers + if ( zClass == ZC_BOOMER ) + { + g_iBoomerGotShoved[victim]++; + } + else { + // check for clears + switch ( zClass ) + { + case ZC_HUNTER: { + if ( GetEntPropEnt(victim, Prop_Send, "m_pounceVictim") > 0 ) + { + HandleClear( attacker, victim, GetEntPropEnt(victim, Prop_Send, "m_pounceVictim"), + ZC_HUNTER, + ( GetGameTime() - g_fPinTime[victim][0]), + -1.0, + true + ); + } + } + case ZC_JOCKEY: { + if ( GetEntPropEnt(victim, Prop_Send, "m_jockeyVictim") > 0 ) + { + HandleClear( attacker, victim, GetEntPropEnt(victim, Prop_Send, "m_jockeyVictim"), + ZC_JOCKEY, + ( GetGameTime() - g_fPinTime[victim][0]), + -1.0, + true + ); + } + } + } + } + + if ( g_fVictimLastShove[victim][attacker] == 0.0 || ( GetGameTime() - g_fVictimLastShove[victim][attacker] ) >= SHOVE_TIME ) + { + if ( GetEntProp(victim, Prop_Send, "m_isAttemptingToPounce") ) + { + HandleDeadstop( attacker, victim ); + } + + HandleShove( attacker, victim, zClass ); + + g_fVictimLastShove[victim][attacker] = GetGameTime(); + } + + // check for shove on smoker by pull victim + if ( g_iSmokerVictim[victim] == attacker ) + { + g_bSmokerShoved[victim] = true; + } + + //PrintDebug(0, "shove by %i on %i", attacker, victim ); + return Plugin_Continue; +} + +public Action: Event_LungePounce( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new client = GetClientOfUserId( GetEventInt(event, "userid") ); + new victim = GetClientOfUserId( GetEventInt(event, "victim") ); + + g_fPinTime[client][0] = GetGameTime(); + + // clear hunter-hit stats (not skeeted) + ResetHunter(client); + + // check if it was a DP + // ignore if no real pounce start pos + if ( g_fPouncePosition[client][0] == 0.0 + && g_fPouncePosition[client][1] == 0.0 + && g_fPouncePosition[client][2] == 0.0 + ) { + return Plugin_Continue; + } + + new Float: endPos[3]; + GetClientAbsOrigin( client, endPos ); + new Float: fHeight = g_fPouncePosition[client][2] - endPos[2]; + + // from pounceannounce: + // distance supplied isn't the actual 2d vector distance needed for damage calculation. See more about it at + // http://forums.alliedmods.net/showthread.php?t=93207 + + new Float: fMin = GetConVarFloat(g_hCvarMinPounceDistance); + new Float: fMax = GetConVarFloat(g_hCvarMaxPounceDistance); + new Float: fMaxDmg = GetConVarFloat(g_hCvarMaxPounceDamage); + + // calculate 2d distance between previous position and pounce position + new distance = RoundToNearest( GetVectorDistance(g_fPouncePosition[client], endPos) ); + + // get damage using hunter damage formula + // check if this is accurate, seems to differ from actual damage done! + new Float: fDamage = ( ( (float(distance) - fMin) / (fMax - fMin) ) * fMaxDmg ) + 1.0; + + // apply bounds + if (fDamage < 0.0) { + fDamage = 0.0; + } else if (fDamage > fMaxDmg + 1.0) { + fDamage = fMaxDmg + 1.0; + } + + new Handle: pack = CreateDataPack(); + WritePackCell( pack, client ); + WritePackCell( pack, victim ); + WritePackFloat( pack, fDamage ); + WritePackFloat( pack, fHeight ); + CreateTimer( 0.05, Timer_HunterDP, pack ); + + return Plugin_Continue; +} + +public Action: Timer_HunterDP( Handle:timer, Handle:pack ) +{ + ResetPack( pack ); + new client = ReadPackCell( pack ); + new victim = ReadPackCell( pack ); + new Float: fDamage = ReadPackFloat( pack ); + new Float: fHeight = ReadPackFloat( pack ); + CloseHandle( pack ); + + HandleHunterDP( client, victim, g_iPounceDamage[client], fDamage, fHeight ); +} + +public Action: Event_PlayerJumped( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new client = GetClientOfUserId( GetEventInt(event, "userid") ); + + if ( IS_VALID_INFECTED(client) ) + { + new zClass = GetEntProp(client, Prop_Send, "m_zombieClass"); + if ( zClass != ZC_JOCKEY ) { return Plugin_Continue; } + + // where did jockey jump from? + GetClientAbsOrigin( client, g_fPouncePosition[client] ); + } + else if ( IS_VALID_SURVIVOR(client) ) + { + // could be the start or part of a hopping streak + + new Float: fPos[3], Float: fVel[3]; + GetClientAbsOrigin( client, fPos ); + GetEntPropVector(client, Prop_Data, "m_vecVelocity", fVel ); + fVel[2] = 0.0; // safeguard + + new Float: fLengthNew, Float: fLengthOld; + fLengthNew = GetVectorLength(fVel); + + + g_bHopCheck[client] = false; + + if ( !g_bIsHopping[client] ) + { + if ( fLengthNew >= GetConVarFloat(g_hCvarBHopMinInitSpeed) ) + { + // starting potential hop streak + g_fHopTopVelocity[client] = fLengthNew; + g_bIsHopping[client] = true; + g_iHops[client] = 0; + } + } + else + { + // check for hopping streak + fLengthOld = GetVectorLength(g_fLastHop[client]); + + // if they picked up speed, count it as a hop, otherwise, we're done hopping + if ( fLengthNew - fLengthOld > HOP_ACCEL_THRESH || fLengthNew >= GetConVarFloat(g_hCvarBHopContSpeed) ) + { + g_iHops[client]++; + + // this should always be the case... + if ( fLengthNew > g_fHopTopVelocity[client] ) + { + g_fHopTopVelocity[client] = fLengthNew; + } + + //PrintToChat( client, "bunnyhop %i: speed: %.1f / increase: %.1f", g_iHops[client], fLengthNew, fLengthNew - fLengthOld ); + } + else + { + g_bIsHopping[client] = false; + + if ( g_iHops[client] ) + { + HandleBHopStreak( client, g_iHops[client], g_fHopTopVelocity[client] ); + g_iHops[client] = 0; + } + } + } + + g_fLastHop[client][0] = fVel[0]; + g_fLastHop[client][1] = fVel[1]; + g_fLastHop[client][2] = fVel[2]; + + if ( g_iHops[client] != 0 ) + { + // check when the player returns to the ground + CreateTimer( HOP_CHECK_TIME, Timer_CheckHop, client, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE ); + } + } + + return Plugin_Continue; +} + +public Action: Timer_CheckHop (Handle:timer, any:client) +{ + // player back to ground = end of hop (streak)? + + if ( !IS_VALID_INGAME(client) || !IsPlayerAlive(client) ) + { + // streak stopped by dying / teamswitch / disconnect? + return Plugin_Stop; + } + else if ( GetEntityFlags(client) & FL_ONGROUND ) + { + new Float: fVel[3]; + GetEntPropVector(client, Prop_Data, "m_vecVelocity", fVel ); + fVel[2] = 0.0; // safeguard + + //PrintToChatAll("grounded %i: vel length: %.1f", client, GetVectorLength(fVel) ); + + g_bHopCheck[client] = true; + + CreateTimer( HOPEND_CHECK_TIME, Timer_CheckHopStreak, client, TIMER_FLAG_NO_MAPCHANGE ); + + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action: Timer_CheckHopStreak (Handle:timer, any:client) +{ + if ( !IS_VALID_INGAME(client) || !IsPlayerAlive(client) ) { return Plugin_Continue; } + + // check if we have any sort of hop streak, and report + if ( g_bHopCheck[client] && g_iHops[client] ) + { + HandleBHopStreak( client, g_iHops[client], g_fHopTopVelocity[client] ); + g_bIsHopping[client] = false; + g_iHops[client] = 0; + g_fHopTopVelocity[client] = 0.0; + } + + g_bHopCheck[client] = false; + + return Plugin_Continue; +} + + +public Action: Event_PlayerJumpApex( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new client = GetClientOfUserId( GetEventInt(event, "userid") ); + + if ( g_bIsHopping[client] ) + { + new Float: fVel[3]; + GetEntPropVector(client, Prop_Data, "m_vecVelocity", fVel ); + fVel[2] = 0.0; + new Float: fLength = GetVectorLength(fVel); + + if ( fLength > g_fHopTopVelocity[client] ) + { + g_fHopTopVelocity[client] = fLength; + } + } +} + + +public Action: Event_JockeyRide( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new client = GetClientOfUserId( GetEventInt(event, "userid") ); + new victim = GetClientOfUserId( GetEventInt(event, "victim") ); + + if ( !IS_VALID_INFECTED(client) || !IS_VALID_SURVIVOR(victim) ) { return Plugin_Continue; } + + g_fPinTime[client][0] = GetGameTime(); + + // minimum distance travelled? + // ignore if no real pounce start pos + if ( g_fPouncePosition[client][0] == 0.0 && g_fPouncePosition[client][1] == 0.0 && g_fPouncePosition[client][2] == 0.0 ) { return Plugin_Continue; } + + new Float: endPos[3]; + GetClientAbsOrigin( client, endPos ); + new Float: fHeight = g_fPouncePosition[client][2] - endPos[2]; + + //PrintToChatAll("jockey height: %.3f", fHeight); + + // (high) pounce + HandleJockeyDP( client, victim, fHeight ); + + return Plugin_Continue; +} + +public Action: Event_AbilityUse( Handle:event, const String:name[], bool:dontBroadcast ) +{ + // track hunters pouncing + new client = GetClientOfUserId( GetEventInt(event, "userid") ); + new String: abilityName[64]; + GetEventString( event, "ability", abilityName, sizeof(abilityName) ); + + if ( !IS_VALID_INGAME(client) ) { return Plugin_Continue; } + + new strAbility: ability; + if ( !GetTrieValue(g_hTrieAbility, abilityName, ability) ) { return Plugin_Continue; } + + switch ( ability ) + { + case ABL_HUNTERLUNGE: + { + // hunter started a pounce + ResetHunter(client); + GetClientAbsOrigin( client, g_fPouncePosition[client] ); + } + + case ABL_ROCKTHROW: + { + // tank throws rock + g_iRocksBeingThrown[g_iRocksBeingThrownCount] = client; + + // safeguard + if ( g_iRocksBeingThrownCount < 9 ) { g_iRocksBeingThrownCount++; } + } + } + + return Plugin_Continue; +} + +// charger carrying +public Action: Event_ChargeCarryStart( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new client = GetClientOfUserId( GetEventInt(event, "userid") ); + new victim = GetClientOfUserId( GetEventInt(event, "victim") ); + if ( !IS_VALID_INFECTED(client) ) { return; } + + PrintDebug(0, "Charge carry start: %i - %i -- time: %.2f", client, victim, GetGameTime() ); + + g_fChargeTime[client] = GetGameTime(); + g_fPinTime[client][0] = g_fChargeTime[client]; + g_fPinTime[client][1] = 0.0; + + if ( !IS_VALID_SURVIVOR(victim) ) { return; } + + g_iChargeVictim[client] = victim; // store who we're carrying (as long as this is set, it's not considered an impact charge flight) + g_iVictimCharger[victim] = client; // store who's charging whom + g_iVictimFlags[victim] = VICFLG_CARRIED; // reset flags for checking later - we know only this now + g_fChargeTime[victim] = g_fChargeTime[client]; + g_iVictimMapDmg[victim] = 0; + + GetClientAbsOrigin( victim, g_fChargeVictimPos[victim] ); + + //CreateTimer( CHARGE_CHECK_TIME, Timer_ChargeCheck, client, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE ); + CreateTimer( CHARGE_CHECK_TIME, Timer_ChargeCheck, victim, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE ); +} + +public Action: Event_ChargeImpact( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new client = GetClientOfUserId( GetEventInt(event, "userid") ); + new victim = GetClientOfUserId( GetEventInt(event, "victim") ); + if ( !IS_VALID_INFECTED(client) || !IS_VALID_SURVIVOR(victim) ) { return; } + + // remember how many people the charger bumped into, and who, and where they were + GetClientAbsOrigin( victim, g_fChargeVictimPos[victim] ); + + g_iVictimCharger[victim] = client; // store who we've bumped up + g_iVictimFlags[victim] = 0; // reset flags for checking later + g_fChargeTime[victim] = GetGameTime(); // store time per victim, for impacts + g_iVictimMapDmg[victim] = 0; + + CreateTimer( CHARGE_CHECK_TIME, Timer_ChargeCheck, victim, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE ); +} + +public Action: Event_ChargePummelStart( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new client = GetClientOfUserId( GetEventInt(event, "userid") ); + + if ( !IS_VALID_INFECTED(client) ) { return; } + + g_fPinTime[client][1] = GetGameTime(); +} + + +public Action: Event_ChargeCarryEnd( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new client = GetClientOfUserId( GetEventInt(event, "userid") ); + if ( client < 1 || client > MaxClients ) { return; } + + g_fPinTime[client][1] = GetGameTime(); + + // delay so we can check whether charger died 'mid carry' + CreateTimer( 0.1, Timer_ChargeCarryEnd, client, TIMER_FLAG_NO_MAPCHANGE ); +} + +public Action: Timer_ChargeCarryEnd( Handle:timer, any:client ) +{ + // set charge time to 0 to avoid deathcharge timer continuing + g_iChargeVictim[client] = 0; // unset this so the repeated timer knows to stop for an ongroundcheck +} + +public Action: Timer_ChargeCheck( Handle:timer, any:client ) +{ + // if something went wrong with the survivor or it was too long ago, forget about it + if ( !IS_VALID_SURVIVOR(client) || !g_iVictimCharger[client] || g_fChargeTime[client] == 0.0 || ( GetGameTime() - g_fChargeTime[client]) > MAX_CHARGE_TIME ) + { + return Plugin_Stop; + } + + // we're done checking if either the victim reached the ground, or died + if ( !IsPlayerAlive(client) ) + { + // player died (this was .. probably.. a death charge) + g_iVictimFlags[client] = g_iVictimFlags[client] | VICFLG_AIRDEATH; + + // check conditions now + CreateTimer( 0.0, Timer_DeathChargeCheck, client, TIMER_FLAG_NO_MAPCHANGE ); + + return Plugin_Stop; + } + else if ( GetEntityFlags(client) & FL_ONGROUND && g_iChargeVictim[ g_iVictimCharger[client] ] != client ) + { + // survivor reached the ground and didn't die (yet) + // the client-check condition checks whether the survivor is still being carried by the charger + // (in which case it doesn't matter that they're on the ground) + + // check conditions with small delay (to see if they still die soon) + CreateTimer( CHARGE_END_CHECK, Timer_DeathChargeCheck, client, TIMER_FLAG_NO_MAPCHANGE ); + + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action: Timer_DeathChargeCheck( Handle:timer, any:client ) +{ + if ( !IS_VALID_INGAME(client) ) { return; } + + // check conditions.. if flags match up, it's a DC + PrintDebug( 3, "Checking charge victim: %i - %i - flags: %i (alive? %i)", g_iVictimCharger[client], client, g_iVictimFlags[client], IsPlayerAlive(client) ); + + new flags = g_iVictimFlags[client]; + + if ( !IsPlayerAlive(client) ) + { + new Float: pos[3]; + GetClientAbsOrigin( client, pos ); + new Float: fHeight = g_fChargeVictimPos[client][2] - pos[2]; + + /* + it's a deathcharge when: + the survivor is dead AND + they drowned/fell AND took enough damage or died in mid-air + AND not killed by someone else + OR is in an unreachable spot AND dropped at least X height + OR took plenty of map damage + + old.. need? + fHeight > GetConVarFloat(g_hCvarDeathChargeHeight) + */ + if ( ( ( flags & VICFLG_DROWN || flags & VICFLG_FALL ) && + ( flags & VICFLG_HURTLOTS || flags & VICFLG_AIRDEATH ) || + ( flags & VICFLG_WEIRDFLOW && fHeight >= MIN_FLOWDROPHEIGHT ) || + g_iVictimMapDmg[client] >= MIN_DC_TRIGGER_DMG + ) && + !( flags & VICFLG_KILLEDBYOTHER ) + ) { + HandleDeathCharge( g_iVictimCharger[client], client, fHeight, GetVectorDistance(g_fChargeVictimPos[client], pos, false), bool:(flags & VICFLG_CARRIED) ); + } + } + else if ( ( flags & VICFLG_WEIRDFLOW || g_iVictimMapDmg[client] >= MIN_DC_RECHECK_DMG ) && + !(flags & VICFLG_WEIRDFLOWDONE) + ) { + // could be incapped and dying more slowly + // flag only gets set on preincap, so don't need to check for incap + g_iVictimFlags[client] = g_iVictimFlags[client] | VICFLG_WEIRDFLOWDONE; + + CreateTimer( CHARGE_END_RECHECK, Timer_DeathChargeCheck, client, TIMER_FLAG_NO_MAPCHANGE ); + } +} + +stock ResetHunter(client) +{ + g_iHunterShotDmgTeam[client] = 0; + + for ( new i=1; i <= MaxClients; i++ ) + { + g_iHunterShotDmg[client][i] = 0; + g_fHunterShotStart[client][i] = 0.0; + } + g_iHunterOverkill[client] = 0; +} + + +// entity creation +public OnEntityCreated ( entity, const String:classname[] ) +{ + if ( entity < 1 || !IsValidEntity(entity) || !IsValidEdict(entity) ) { return; } + + // track infected / witches, so damage on them counts as hits + + new strOEC: classnameOEC; + if (!GetTrieValue(g_hTrieEntityCreated, classname, classnameOEC)) { return; } + + switch ( classnameOEC ) + { + case OEC_TANKROCK: + { + decl String:rock_key[10]; + FormatEx(rock_key, sizeof(rock_key), "%x", entity); + new rock_array[3]; + + // store which tank is throwing what rock + new tank = ShiftTankThrower(); + + if ( IS_VALID_INGAME(tank) ) + { + g_iTankRock[tank] = entity; + rock_array[rckTank] = tank; + } + SetTrieArray(g_hRockTrie, rock_key, rock_array, sizeof(rock_array), true); + + SDKHook(entity, SDKHook_TraceAttack, TraceAttack_Rock); + SDKHook(entity, SDKHook_Touch, OnTouch_Rock); + } + + + case OEC_CARALARM: + { + decl String:car_key[10]; + FormatEx(car_key, sizeof(car_key), "%x", entity); + + SDKHook(entity, SDKHook_OnTakeDamage, OnTakeDamage_Car); + SDKHook(entity, SDKHook_Touch, OnTouch_Car); + + SDKHook(entity, SDKHook_Spawn, OnEntitySpawned_CarAlarm); + } + + case OEC_CARGLASS: + { + SDKHook(entity, SDKHook_OnTakeDamage, OnTakeDamage_CarGlass); + SDKHook(entity, SDKHook_Touch, OnTouch_CarGlass); + + //SetTrieValue(g_hCarTrie, car_key, ); + SDKHook(entity, SDKHook_Spawn, OnEntitySpawned_CarAlarmGlass); + } + } +} + +public OnEntitySpawned_CarAlarm ( entity ) +{ + if ( !IsValidEntity(entity) ) { return; } + + decl String:car_key[10]; + FormatEx(car_key, sizeof(car_key), "%x", entity); + + decl String:target[48]; + GetEntPropString(entity, Prop_Data, "m_iName", target, sizeof(target)); + + SetTrieValue( g_hCarTrie, target, entity ); + SetTrieValue( g_hCarTrie, car_key, 0 ); // who shot the car? + + HookSingleEntityOutput( entity, "OnCarAlarmStart", Hook_CarAlarmStart ); +} + +public OnEntitySpawned_CarAlarmGlass ( entity ) +{ + if ( !IsValidEntity(entity) ) { return; } + + // glass is parented to a car, link the two through the trie + // find parent and save both + decl String:car_key[10]; + FormatEx(car_key, sizeof(car_key), "%x", entity); + + decl String:parent[48]; + GetEntPropString(entity, Prop_Data, "m_iParent", parent, sizeof(parent)); + new parentEntity; + + // find targetname in trie + if ( GetTrieValue(g_hCarTrie, parent, parentEntity ) ) + { + // if valid entity, save the parent entity + if ( IsValidEntity(parentEntity) ) + { + SetTrieValue( g_hCarTrie, car_key, parentEntity ); + + decl String:car_key_p[10]; + FormatEx(car_key_p, sizeof(car_key_p), "%x_A", parentEntity); + new testEntity; + + if ( GetTrieValue(g_hCarTrie, car_key_p, testEntity) ) + { + // second glass + FormatEx(car_key_p, sizeof(car_key_p), "%x_B", parentEntity); + } + + SetTrieValue( g_hCarTrie, car_key_p, entity ); + } + } +} + +// entity destruction +public OnEntityDestroyed ( entity ) +{ + decl String:witch_key[10]; + FormatEx(witch_key, sizeof(witch_key), "%x", entity); + + decl rock_array[3]; + if ( GetTrieArray(g_hRockTrie, witch_key, rock_array, sizeof(rock_array)) ) + { + // tank rock + CreateTimer( ROCK_CHECK_TIME, Timer_CheckRockSkeet, entity ); + SDKUnhook(entity, SDKHook_TraceAttack, TraceAttack_Rock); + return; + } + + decl witch_array[MAXPLAYERS+DMGARRAYEXT]; + if ( GetTrieArray(g_hWitchTrie, witch_key, witch_array, sizeof(witch_array)) ) + { + // witch + // delayed deletion, to avoid potential problems with crowns not detecting + CreateTimer( WITCH_DELETE_TIME, Timer_WitchKeyDelete, entity ); + SDKUnhook(entity, SDKHook_OnTakeDamagePost, OnTakeDamagePost_Witch); + return; + } +} + +public Action: Timer_WitchKeyDelete (Handle:timer, any:witch) +{ + decl String:witch_key[10]; + FormatEx(witch_key, sizeof(witch_key), "%x", witch); + RemoveFromTrie(g_hWitchTrie, witch_key); +} + + +public Action: Timer_CheckRockSkeet (Handle:timer, any:rock) +{ + decl rock_array[3]; + decl String: rock_key[10]; + FormatEx(rock_key, sizeof(rock_key), "%x", rock); + if (!GetTrieArray(g_hRockTrie, rock_key, rock_array, sizeof(rock_array)) ) { return Plugin_Continue; } + + RemoveFromTrie(g_hRockTrie, rock_key); + + // if rock didn't hit anyone / didn't touch anything, it was shot + if ( rock_array[rckDamage] > 0 ) + { + HandleRockSkeeted( rock_array[rckSkeeter], rock_array[rckTank] ); + } + + return Plugin_Continue; +} + +// boomer got somebody +public Action: Event_PlayerBoomed (Handle:event, const String:name[], bool:dontBroadcast) +{ + new attacker = GetClientOfUserId( GetEventInt(event, "attacker") ); + new bool: byBoom = GetEventBool(event, "by_boomer"); + + if ( byBoom && IS_VALID_INFECTED(attacker) ) + { + g_bBoomerHitSomebody[attacker] = true; + + // check if it was vomit spray + new bool: byExplosion = GetEventBool(event, "exploded"); + if ( !byExplosion ) + { + // count amount of booms + if ( !g_iBoomerVomitHits[attacker] ) { + // check for boom count later + CreateTimer( VOMIT_DURATION_TIME, Timer_BoomVomitCheck, attacker, TIMER_FLAG_NO_MAPCHANGE ); + } + g_iBoomerVomitHits[attacker]++; + } + } +} +// check how many booms landed +public Action: Timer_BoomVomitCheck ( Handle:timer, any:client ) +{ + HandleVomitLanded( client, g_iBoomerVomitHits[client] ); + g_iBoomerVomitHits[client] = 0; +} + +// boomers that didn't bile anyone +public Action: Event_BoomerExploded (Handle:event, const String:name[], bool:dontBroadcast) +{ + new client = GetClientOfUserId( GetEventInt(event, "userid") ); + new bool: biled = GetEventBool(event, "splashedbile"); + if ( !biled && !g_bBoomerHitSomebody[client] ) + { + new attacker = GetClientOfUserId( GetEventInt(event, "attacker") ); + if ( IS_VALID_SURVIVOR(attacker) ) + { + HandlePop( attacker, client, g_iBoomerGotShoved[client], (GetGameTime() - g_fSpawnTime[client]) ); + } + } +} + +// crown tracking +public Action: Event_WitchSpawned ( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new witch = GetEventInt(event, "witchid"); + + SDKHook(witch, SDKHook_OnTakeDamagePost, OnTakeDamagePost_Witch); + + new witch_dmg_array[MAXPLAYERS+DMGARRAYEXT]; + decl String:witch_key[10]; + FormatEx(witch_key, sizeof(witch_key), "%x", witch); + witch_dmg_array[MAXPLAYERS+WTCH_HEALTH] = GetConVarInt(g_hCvarWitchHealth); + SetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT, false); +} + +public Action: Event_WitchKilled ( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new witch = GetEventInt(event, "witchid"); + new attacker = GetClientOfUserId( GetEventInt(event, "userid") ); + SDKUnhook(witch, SDKHook_OnTakeDamagePost, OnTakeDamagePost_Witch); + + if ( !IS_VALID_SURVIVOR(attacker) ) { return Plugin_Continue; } + + new bool: bOneShot = GetEventBool(event, "oneshot"); + + // is it a crown / drawcrown? + new Handle: pack = CreateDataPack(); + WritePackCell( pack, attacker ); + WritePackCell( pack, witch ); + WritePackCell( pack, (bOneShot) ? 1 : 0 ); + CreateTimer( WITCH_CHECK_TIME, Timer_CheckWitchCrown, pack ); + + return Plugin_Continue; +} +public Action: Event_WitchHarasserSet ( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new witch = GetEventInt(event, "witchid"); + + decl String:witch_key[10]; + FormatEx(witch_key, sizeof(witch_key), "%x", witch); + decl witch_dmg_array[MAXPLAYERS+DMGARRAYEXT]; + + if ( !GetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT) ) + { + for ( new i = 0; i <= MAXPLAYERS; i++ ) + { + witch_dmg_array[i] = 0; + } + witch_dmg_array[MAXPLAYERS+WTCH_HEALTH] = GetConVarInt(g_hCvarWitchHealth); + witch_dmg_array[MAXPLAYERS+WTCH_STARTLED] = 1; // harasser set + SetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT, false); + } + else + { + witch_dmg_array[MAXPLAYERS+WTCH_STARTLED] = 1; // harasser set + SetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT, true); + } +} + +public Action: OnTakeDamageByWitch ( victim, &attacker, &inflictor, &Float:damage, &damagetype ) +{ + // if a survivor is hit by a witch, note it in the witch damage array (maxplayers+2 = 1) + if ( IS_VALID_SURVIVOR(victim) && damage > 0.0 ) + { + + // not a crown if witch hit anyone for > 0 damage + if ( IsWitch(attacker) ) + { + decl String:witch_key[10]; + FormatEx(witch_key, sizeof(witch_key), "%x", attacker); + decl witch_dmg_array[MAXPLAYERS+DMGARRAYEXT]; + + if ( !GetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT) ) + { + for ( new i = 0; i <= MAXPLAYERS; i++ ) + { + witch_dmg_array[i] = 0; + } + witch_dmg_array[MAXPLAYERS+WTCH_HEALTH] = GetConVarInt(g_hCvarWitchHealth); + witch_dmg_array[MAXPLAYERS+WTCH_GOTSLASH] = 1; // failed + SetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT, false); + } + else + { + witch_dmg_array[MAXPLAYERS+WTCH_GOTSLASH] = 1; // failed + SetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT, true); + } + } + } +} + +public OnTakeDamagePost_Witch ( victim, attacker, inflictor, Float:damage, damagetype ) +{ + // only called for witches, so no check required + + decl String:witch_key[10]; + FormatEx(witch_key, sizeof(witch_key), "%x", victim); + decl witch_dmg_array[MAXPLAYERS+DMGARRAYEXT]; + + if ( !GetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT) ) + { + for ( new i = 0; i <= MAXPLAYERS; i++ ) + { + witch_dmg_array[i] = 0; + } + witch_dmg_array[MAXPLAYERS+WTCH_HEALTH] = GetConVarInt(g_hCvarWitchHealth); + SetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT, false); + } + + // store damage done to witch + if ( IS_VALID_SURVIVOR(attacker) ) + { + witch_dmg_array[attacker] += RoundToFloor(damage); + witch_dmg_array[MAXPLAYERS+WTCH_HEALTH] -= RoundToFloor(damage); + + // remember last shot + if ( g_fWitchShotStart[attacker] == 0.0 || (GetGameTime() - g_fWitchShotStart[attacker]) > SHOTGUN_BLAST_TIME ) + { + // reset last shot damage count and attacker + g_fWitchShotStart[attacker] = GetGameTime(); + witch_dmg_array[MAXPLAYERS+WTCH_CROWNER] = attacker; + witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT] = 0; + witch_dmg_array[MAXPLAYERS+WTCH_CROWNTYPE] = ( damagetype & DMG_BUCKSHOT ) ? 1 : 0; // only allow shotguns + } + + // continued blast, add up + witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT] += RoundToFloor(damage); + + SetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT, true); + } + else + { + // store all chip from other sources than survivor in [0] + witch_dmg_array[0] += RoundToFloor(damage); + //witch_dmg_array[MAXPLAYERS+1] -= RoundToFloor(damage); + SetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT, true); + } +} + +public Action: Timer_CheckWitchCrown(Handle:timer, Handle:pack) +{ + ResetPack( pack ); + new attacker = ReadPackCell( pack ); + new witch = ReadPackCell( pack ); + new bool:bOneShot = bool:ReadPackCell( pack ); + CloseHandle( pack ); + + CheckWitchCrown( witch, attacker, bOneShot ); +} + +stock CheckWitchCrown ( witch, attacker, bool: bOneShot = false ) +{ + decl String:witch_key[10]; + FormatEx(witch_key, sizeof(witch_key), "%x", witch); + decl witch_dmg_array[MAXPLAYERS+DMGARRAYEXT]; + if ( !GetTrieArray(g_hWitchTrie, witch_key, witch_dmg_array, MAXPLAYERS+DMGARRAYEXT) ) { + PrintDebug(0, "Witch Crown Check: Error: Trie entry missing (entity: %i, oneshot: %i)", witch, bOneShot); + return; + } + + new chipDamage = 0; + new iWitchHealth = GetConVarInt(g_hCvarWitchHealth); + + /* + the attacker is the last one that did damage to witch + if their damage is full damage on an unharrassed witch, it's a full crown + if their damage is full or > drawcrown_threshhold, it's a drawcrown + */ + + // not a crown at all if anyone was hit, or if the killing damage wasn't a shotgun blast + + // safeguard: if it was a 'oneshot' witch kill, must've been a shotgun + // this is not enough: sometimes a shotgun crown happens that is not even reported as a oneshot... + // seems like the cause is that the witch post ontakedamage is not called in time? + if ( bOneShot ) + { + witch_dmg_array[MAXPLAYERS+WTCH_CROWNTYPE] = 1; + } + + if ( witch_dmg_array[MAXPLAYERS+WTCH_GOTSLASH] || !witch_dmg_array[MAXPLAYERS+WTCH_CROWNTYPE] ) + { + PrintDebug(0, "Witch Crown Check: Failed: bungled: %i / crowntype: %i (entity: %i)", + witch_dmg_array[MAXPLAYERS+WTCH_GOTSLASH], + witch_dmg_array[MAXPLAYERS+WTCH_CROWNTYPE], + witch + ); + PrintDebug(1, "Witch Crown Check: Further details: attacker: %N, attacker dmg: %i, teamless dmg: %i", + attacker, + witch_dmg_array[attacker], + witch_dmg_array[0] + ); + return; + } + + PrintDebug(0, "Witch Crown Check: crown shot: %i, harrassed: %i (full health: %i / drawthresh: %i / oneshot %i)", + witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT], + witch_dmg_array[MAXPLAYERS+WTCH_STARTLED], + iWitchHealth, + GetConVarInt(g_hCvarDrawCrownThresh), + bOneShot + ); + + // full crown? unharrassed + if ( !witch_dmg_array[MAXPLAYERS+WTCH_STARTLED] && ( bOneShot || witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT] >= iWitchHealth ) ) + { + // make sure that we don't count any type of chip + if ( GetConVarBool(g_hCvarHideFakeDamage) ) + { + chipDamage = 0; + for ( new i = 0; i <= MAXPLAYERS; i++ ) + { + if ( i == attacker ) { continue; } + chipDamage += witch_dmg_array[i]; + } + witch_dmg_array[attacker] = iWitchHealth - chipDamage; + } + HandleCrown( attacker, witch_dmg_array[attacker] ); + } + else if ( witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT] >= GetConVarInt(g_hCvarDrawCrownThresh) ) + { + // draw crown: harassed + over X damage done by one survivor -- in ONE shot + + for ( new i = 0; i <= MAXPLAYERS; i++ ) + { + if ( i == attacker ) { + // count any damage done before final shot as chip + chipDamage += witch_dmg_array[i] - witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT]; + } else { + chipDamage += witch_dmg_array[i]; + } + } + + // make sure that we don't count any type of chip + if ( GetConVarBool(g_hCvarHideFakeDamage) ) + { + // unlikely to happen, but if the chip was A LOT + if ( chipDamage >= iWitchHealth ) { + chipDamage = iWitchHealth - 1; + witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT] = 1; + } + else { + witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT] = iWitchHealth - chipDamage; + } + // re-check whether it qualifies as a drawcrown: + if ( witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT] < GetConVarInt(g_hCvarDrawCrownThresh) ) { return; } + } + + // plus, set final shot as 'damage', and the rest as chip + HandleDrawCrown( attacker, witch_dmg_array[MAXPLAYERS+WTCH_CROWNSHOT], chipDamage ); + } + + // remove trie + +} + +// tank rock +public Action: TraceAttack_Rock (victim, &attacker, &inflictor, &Float:damage, &damagetype, &ammotype, hitbox, hitgroup) +{ + if ( IS_VALID_SURVIVOR(attacker) ) + { + /* + can't really use this for precise detection, though it does + report the last shot -- the damage report is without distance falloff + */ + decl String:rock_key[10]; + decl rock_array[3]; + FormatEx(rock_key, sizeof(rock_key), "%x", victim); + GetTrieArray(g_hRockTrie, rock_key, rock_array, sizeof(rock_array)); + rock_array[rckDamage] += RoundToFloor(damage); + rock_array[rckSkeeter] = attacker; + SetTrieArray(g_hRockTrie, rock_key, rock_array, sizeof(rock_array), true); + } +} + +public OnTouch_Rock ( entity ) +{ + // remember that the rock wasn't shot + decl String:rock_key[10]; + FormatEx(rock_key, sizeof(rock_key), "%x", entity); + new rock_array[3]; + rock_array[rckDamage] = -1; + SetTrieArray(g_hRockTrie, rock_key, rock_array, sizeof(rock_array), true); + + SDKUnhook(entity, SDKHook_Touch, OnTouch_Rock); +} + +// smoker tongue cutting & self clears +public Action: Event_TonguePullStopped (Handle:event, const String:name[], bool:dontBroadcast) +{ + new attacker = GetClientOfUserId( GetEventInt(event, "userid") ); + new victim = GetClientOfUserId( GetEventInt(event, "victim") ); + new smoker = GetClientOfUserId( GetEventInt(event, "smoker") ); + new reason = GetEventInt(event, "release_type"); + + if ( !IS_VALID_SURVIVOR(attacker) || !IS_VALID_INFECTED(smoker) ) { return Plugin_Continue; } + + // clear check - if the smoker itself was not shoved, handle the clear + HandleClear( attacker, smoker, victim, + ZC_SMOKER, + (g_fPinTime[smoker][1] > 0.0) ? ( GetGameTime() - g_fPinTime[smoker][1]) : -1.0, + ( GetGameTime() - g_fPinTime[smoker][0]), + bool:( reason != CUT_SLASH && reason != CUT_KILL ) + ); + + if ( attacker != victim ) { return Plugin_Continue; } + + if ( reason == CUT_KILL ) + { + g_bSmokerClearCheck[smoker] = true; + } + else if ( g_bSmokerShoved[smoker] ) + { + HandleSmokerSelfClear( attacker, smoker, true ); + } + else if ( reason == CUT_SLASH ) // note: can't trust this to actually BE a slash.. + { + // check weapon + decl String:weapon[32]; + GetClientWeapon( attacker, weapon, 32 ); + + // this doesn't count the chainsaw, but that's no-skill anyway + if ( StrEqual(weapon, "weapon_melee", false) ) + { + HandleTongueCut( attacker, smoker ); + } + } + + return Plugin_Continue; +} + +public Action: Event_TongueGrab (Handle:event, const String:name[], bool:dontBroadcast) +{ + new attacker = GetClientOfUserId( GetEventInt(event, "userid") ); + new victim = GetClientOfUserId( GetEventInt(event, "victim") ); + + if ( IS_VALID_INFECTED(attacker) && IS_VALID_SURVIVOR(victim) ) + { + // new pull, clean damage + g_bSmokerClearCheck[attacker] = false; + g_bSmokerShoved[attacker] = false; + g_iSmokerVictim[attacker] = victim; + g_iSmokerVictimDamage[attacker] = 0; + g_fPinTime[attacker][0] = GetGameTime(); + g_fPinTime[attacker][1] = 0.0; + } + + return Plugin_Continue; +} + +public Action: Event_ChokeStart (Handle:event, const String:name[], bool:dontBroadcast) +{ + new attacker = GetClientOfUserId( GetEventInt(event, "userid") ); + + if ( g_fPinTime[attacker][0] == 0.0 ) { g_fPinTime[attacker][0] = GetGameTime(); } + g_fPinTime[attacker][1] = GetGameTime(); +} + +public Action: Event_ChokeStop (Handle:event, const String:name[], bool:dontBroadcast) +{ + new attacker = GetClientOfUserId( GetEventInt(event, "userid") ); + new victim = GetClientOfUserId( GetEventInt(event, "victim") ); + new smoker = GetClientOfUserId( GetEventInt(event, "smoker") ); + new reason = GetEventInt(event, "release_type"); + + if ( !IS_VALID_SURVIVOR(attacker) || !IS_VALID_INFECTED(smoker) ) { return; } + + // if the smoker itself was not shoved, handle the clear + HandleClear( attacker, smoker, victim, + ZC_SMOKER, + (g_fPinTime[smoker][1] > 0.0) ? ( GetGameTime() - g_fPinTime[smoker][1]) : -1.0, + ( GetGameTime() - g_fPinTime[smoker][0]), + bool:( reason != CUT_SLASH && reason != CUT_KILL ) + ); +} + +// car alarm handling +public Hook_CarAlarmStart ( const String:output[], caller, activator, Float:delay ) +{ + //decl String:car_key[10]; + //FormatEx(car_key, sizeof(car_key), "%x", entity); + + PrintDebug( 0, "calarm trigger: caller %i / activator %i / delay: %.2f", caller, activator, delay ); +} +public Action: Event_CarAlarmGoesOff( Handle:event, const String:name[], bool:dontBroadcast ) +{ + g_fLastCarAlarm = GetGameTime(); +} + +public Action: OnTakeDamage_Car ( victim, &attacker, &inflictor, &Float:damage, &damagetype ) +{ + if ( !IS_VALID_SURVIVOR(attacker) ) { return Plugin_Continue; } + + /* + boomer popped on alarmed car = + DMG_BLAST_SURFACE| DMG_BLAST + and inflictor is the boomer + + melee slash/club = + DMG_SLOWBURN|DMG_PREVENT_PHYSICS_FORCE + DMG_CLUB or DMG_SLASH + shove is without DMG_SLOWBURN + */ + + CreateTimer( 0.01, Timer_CheckAlarm, victim, TIMER_FLAG_NO_MAPCHANGE ); + + decl String:car_key[10]; + FormatEx(car_key, sizeof(car_key), "%x", victim); + SetTrieValue(g_hCarTrie, car_key, attacker); + + if ( damagetype & DMG_BLAST ) + { + if ( IS_VALID_INFECTED(inflictor) && GetEntProp(inflictor, Prop_Send, "m_zombieClass") == ZC_BOOMER ) { + g_iLastCarAlarmReason[attacker] = CALARM_BOOMER; + g_iLastCarAlarmBoomer = inflictor; + } else { + g_iLastCarAlarmReason[attacker] = CALARM_EXPLOSION; + } + } + else if ( damage == 0.0 && ( damagetype & DMG_CLUB || damagetype & DMG_SLASH ) && !( damagetype & DMG_SLOWBURN) ) + { + g_iLastCarAlarmReason[attacker] = CALARM_TOUCHED; + } + else + { + g_iLastCarAlarmReason[attacker] = CALARM_HIT; + } + + return Plugin_Continue; +} + +public OnTouch_Car ( entity, client ) +{ + if ( !IS_VALID_SURVIVOR(client) ) { return; } + + CreateTimer( 0.01, Timer_CheckAlarm, entity, TIMER_FLAG_NO_MAPCHANGE ); + + decl String:car_key[10]; + FormatEx(car_key, sizeof(car_key), "%x", entity); + SetTrieValue(g_hCarTrie, car_key, client); + + g_iLastCarAlarmReason[client] = CALARM_TOUCHED; + + return; +} + +public Action: OnTakeDamage_CarGlass ( victim, &attacker, &inflictor, &Float:damage, &damagetype ) +{ + // check for either: boomer pop or survivor + if ( !IS_VALID_SURVIVOR(attacker) ) { return Plugin_Continue; } + + decl String:car_key[10]; + FormatEx(car_key, sizeof(car_key), "%x", victim); + new parentEntity; + + if ( GetTrieValue(g_hCarTrie, car_key, parentEntity) ) + { + CreateTimer( 0.01, Timer_CheckAlarm, parentEntity, TIMER_FLAG_NO_MAPCHANGE ); + + FormatEx(car_key, sizeof(car_key), "%x", parentEntity); + SetTrieValue(g_hCarTrie, car_key, attacker); + + if ( damagetype & DMG_BLAST ) + { + if ( IS_VALID_INFECTED(inflictor) && GetEntProp(inflictor, Prop_Send, "m_zombieClass") == ZC_BOOMER ) { + g_iLastCarAlarmReason[attacker] = CALARM_BOOMER; + g_iLastCarAlarmBoomer = inflictor; + } else { + g_iLastCarAlarmReason[attacker] = CALARM_EXPLOSION; + } + } + else if ( damage == 0.0 && ( damagetype & DMG_CLUB || damagetype & DMG_SLASH ) && !( damagetype & DMG_SLOWBURN) ) + { + g_iLastCarAlarmReason[attacker] = CALARM_TOUCHED; + } + else + { + g_iLastCarAlarmReason[attacker] = CALARM_HIT; + } + } + + return Plugin_Continue; +} + +public OnTouch_CarGlass ( entity, client ) +{ + if ( !IS_VALID_SURVIVOR(client) ) { return; } + + decl String:car_key[10]; + FormatEx(car_key, sizeof(car_key), "%x", entity); + new parentEntity; + + if ( GetTrieValue(g_hCarTrie, car_key, parentEntity) ) + { + CreateTimer( 0.01, Timer_CheckAlarm, parentEntity, TIMER_FLAG_NO_MAPCHANGE ); + + FormatEx(car_key, sizeof(car_key), "%x", parentEntity); + SetTrieValue(g_hCarTrie, car_key, client); + + g_iLastCarAlarmReason[client] = CALARM_TOUCHED; + } + + return; +} + +public Action: Timer_CheckAlarm (Handle:timer, any:entity) +{ + //PrintToChatAll( "checking alarm: time: %.3f", GetGameTime() - g_fLastCarAlarm ); + + if ( (GetGameTime() - g_fLastCarAlarm) < CARALARM_MIN_TIME ) + { + // got a match, drop stuff from trie and handle triggering + decl String:car_key[10]; + new testEntity; + new survivor = -1; + + // remove car glass + FormatEx(car_key, sizeof(car_key), "%x_A", entity); + if ( GetTrieValue(g_hCarTrie, car_key, testEntity) ) + { + RemoveFromTrie(g_hCarTrie, car_key); + SDKUnhook(testEntity, SDKHook_OnTakeDamage, OnTakeDamage_CarGlass); + SDKUnhook(testEntity, SDKHook_Touch, OnTouch_CarGlass); + } + FormatEx(car_key, sizeof(car_key), "%x_B", entity); + if ( GetTrieValue(g_hCarTrie, car_key, testEntity) ) + { + RemoveFromTrie(g_hCarTrie, car_key); + SDKUnhook(testEntity, SDKHook_OnTakeDamage, OnTakeDamage_CarGlass); + SDKUnhook(testEntity, SDKHook_Touch, OnTouch_CarGlass); + } + + // remove car + FormatEx(car_key, sizeof(car_key), "%x", entity); + if ( GetTrieValue(g_hCarTrie, car_key, survivor) ) + { + RemoveFromTrie(g_hCarTrie, car_key); + SDKUnhook(entity, SDKHook_OnTakeDamage, OnTakeDamage_Car); + SDKUnhook(entity, SDKHook_Touch, OnTouch_Car); + } + + // check for infected assistance + new infected = 0; + if ( IS_VALID_SURVIVOR(survivor) ) + { + if ( g_iLastCarAlarmReason[survivor] == CALARM_BOOMER ) + { + infected = g_iLastCarAlarmBoomer; + } + else if ( IS_VALID_INFECTED(GetEntPropEnt(survivor, Prop_Send, "m_carryAttacker")) ) + { + infected = GetEntPropEnt(survivor, Prop_Send, "m_carryAttacker"); + } + else if ( IS_VALID_INFECTED(GetEntPropEnt(survivor, Prop_Send, "m_jockeyAttacker")) ) + { + infected = GetEntPropEnt(survivor, Prop_Send, "m_jockeyAttacker"); + } + else if ( IS_VALID_INFECTED(GetEntPropEnt(survivor, Prop_Send, "m_tongueOwner")) ) + { + infected = GetEntPropEnt(survivor, Prop_Send, "m_tongueOwner"); + } + } + + HandleCarAlarmTriggered( + survivor, + infected, + (IS_VALID_INGAME(survivor)) ? g_iLastCarAlarmReason[survivor] : CALARM_UNKNOWN + ); + } +} + + +/* throwactivate .. for more reliable rock-tracking? +public Action: L4D_OnCThrowActivate ( ability ) +{ + // tank throws rock + if ( !IsValidEntity(ability) ) { return Plugin_Continue; } + + // find tank player + new tank = GetEntPropEnt(ability, Prop_Send, "m_owner"); + if ( !IS_VALID_INGAME(tank) ) { return Plugin_Continue; } + + ... +} +*/ + +/* + Reporting and forwards + ---------------------- +*/ +// boomer pop +stock HandlePop( attacker, victim, shoveCount, Float:timeAlive ) +{ + // report? + if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_POP ) + { + if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) ) + { + PrintToChatAll( "\x04%N\x01 popped \x05%N\x01.", attacker, victim ); + } + else if ( IS_VALID_INGAME(attacker) ) + { + PrintToChatAll( "\x04%N\x01 popped a boomer.", attacker ); + } + } + + Call_StartForward(g_hForwardBoomerPop); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_PushCell(shoveCount); + Call_PushFloat(timeAlive); + Call_Finish(); +} + +// charger level +stock HandleLevel( attacker, victim ) +{ + // report? + if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_LEVEL ) + { + if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) ) + { + PrintToChatAll( "\x04%N\x01 leveled \x05%N\x01.", attacker, victim ); + } + else if ( IS_VALID_INGAME(attacker) ) + { + PrintToChatAll( "\x04%N\x01 leveled a charger.", attacker ); + } + else { + PrintToChatAll( "A charger was leveled." ); + } + } + + // call forward + Call_StartForward(g_hForwardLevel); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_Finish(); +} +// charger level hurt +stock HandleLevelHurt( attacker, victim, damage ) +{ + // report? + if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_HURTLEVEL ) + { + if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) ) + { + PrintToChatAll( "\x04%N\x01 chip-leveled \x05%N\x01 (\x03%i\x01 damage).", attacker, victim, damage ); + } + else if ( IS_VALID_INGAME(attacker) ) + { + PrintToChatAll( "\x04%N\x01 chip-leveled a charger. (\x03%i\x01 damage)", attacker, damage ); + } + else { + PrintToChatAll( "A charger was chip-leveled (\x03%i\x01 damage).", damage ); + } + } + + // call forward + Call_StartForward(g_hForwardLevelHurt); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_PushCell(damage); + Call_Finish(); +} + +// deadstops +stock HandleDeadstop( attacker, victim ) +{ + // report? + if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_DEADSTOP ) + { + if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) ) + { + PrintToChatAll( "\x04%N\x01 deadstopped \x05%N\x01.", attacker, victim ); + } + else if ( IS_VALID_INGAME(attacker) ) + { + PrintToChatAll( "\x04%N\x01 deadstopped a hunter.", attacker ); + } + } + + Call_StartForward(g_hForwardHunterDeadstop); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_Finish(); +} + +stock HandleShove( attacker, victim, zombieClass ) +{ + // report? + if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_SHOVE ) + { + if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) ) + { + PrintToChatAll( "\x04%N\x01 shoved \x05%N\x01.", attacker, victim ); + } + else if ( IS_VALID_INGAME(attacker) ) + { + PrintToChatAll( "\x04%N\x01 shoved an SI.", attacker ); + } + } + + Call_StartForward(g_hForwardSIShove); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_PushCell(zombieClass); + Call_Finish(); +} + +// real skeet +stock HandleSkeet( attacker, victim, bool:bMelee = false, bool:bSniper = false, bool:bGL = false ) +{ + // report? + if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_SKEET ) + { + if ( attacker == -2 ) + { + // team skeet sets to -2 + if ( IS_VALID_INGAME(victim) && !IsFakeClient(victim) ) { + PrintToChatAll( "\x05%N\x01 was team-skeeted.", victim ); + } else { + PrintToChatAll( "\x01A hunter was team-skeeted." ); + } + } + else if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) ) + { + PrintToChatAll( "\x04%N\x01 %sskeeted \x05%N\x01.", + attacker, + (bMelee) ? "melee-": ((bSniper) ? "headshot-" : ((bGL) ? "grenade-" : "") ), + victim + ); + } + else if ( IS_VALID_INGAME(attacker) ) + { + PrintToChatAll( "\x04%N\x01 %sskeeted a hunter.", + attacker, + (bMelee) ? "melee-": ((bSniper) ? "headshot-" : ((bGL) ? "grenade-" : "") ) + ); + } + } + + // call forward + if ( bSniper ) + { + Call_StartForward(g_hForwardSkeetSniper); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_Finish(); + } + else if ( bGL ) + { + Call_StartForward(g_hForwardSkeetGL); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_Finish(); + } + else if ( bMelee ) + { + Call_StartForward(g_hForwardSkeetMelee); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_Finish(); + } + else + { + Call_StartForward(g_hForwardSkeet); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_Finish(); + } +} + +// hurt skeet / non-skeet +// NOTE: bSniper not set yet, do this +stock HandleNonSkeet( attacker, victim, damage, bool:bOverKill = false, bool:bMelee = false, bool:bSniper = false ) +{ + // report? + if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_HURTSKEET ) + { + if ( IS_VALID_INGAME(victim) ) + { + PrintToChatAll( "\x05%N\x01 was \x04not\x01 skeeted (\x03%i\x01 damage).%s", victim, damage, (bOverKill) ? "(Would've skeeted if hunter were unchipped!)" : "" ); + } + else + { + PrintToChatAll( "\x01Hunter was \x04not\x01 skeeted (\x03%i\x01 damage).%s", damage, (bOverKill) ? "(Would've skeeted if hunter were unchipped!)" : "" ); + } + } + + // call forward + if ( bSniper ) + { + Call_StartForward(g_hForwardSkeetSniperHurt); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_PushCell(damage); + Call_PushCell(bOverKill); + Call_Finish(); + } + else if ( bMelee ) + { + Call_StartForward(g_hForwardSkeetMeleeHurt); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_PushCell(damage); + Call_PushCell(bOverKill); + Call_Finish(); + } + else + { + Call_StartForward(g_hForwardSkeetHurt); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_PushCell(damage); + Call_PushCell(bOverKill); + Call_Finish(); + } +} + + +// crown +HandleCrown( attacker, damage ) +{ + // report? + if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_CROWN ) + { + if ( IS_VALID_INGAME(attacker) ) + { + PrintToChatAll( "\x04%N\x01 crowned a witch (\x03%i\x01 damage).", attacker, damage ); + } + else { + PrintToChatAll( "A witch was crowned." ); + } + } + + // call forward + Call_StartForward(g_hForwardCrown); + Call_PushCell(attacker); + Call_PushCell(damage); + Call_Finish(); +} +// drawcrown +HandleDrawCrown( attacker, damage, chipdamage ) +{ + // report? + if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_DRAWCROWN ) + { + if ( IS_VALID_INGAME(attacker) ) + { + PrintToChatAll( "\x04%N\x01 draw-crowned a witch (\x03%i\x01 damage, \x05%i\x01 chip).", attacker, damage, chipdamage ); + } + else { + PrintToChatAll( "A witch was draw-crowned (\x03%i\x01 damage, \x05%i\x01 chip).", damage, chipdamage ); + } + } + + // call forward + Call_StartForward(g_hForwardDrawCrown); + Call_PushCell(attacker); + Call_PushCell(damage); + Call_PushCell(chipdamage); + Call_Finish(); +} + +// smoker clears +HandleTongueCut( attacker, victim ) +{ + // report? + if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_TONGUECUT ) + { + if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) ) + { + PrintToChatAll( "\x04%N\x01 cut \x05%N\x01's tongue.", attacker, victim ); + } + else if ( IS_VALID_INGAME(attacker) ) + { + PrintToChatAll( "\x04%N\x01 cut smoker tongue.", attacker ); + } + } + + // call forward + Call_StartForward(g_hForwardTongueCut); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_Finish(); +} + +HandleSmokerSelfClear( attacker, victim, bool:withShove = false ) +{ + // report? + if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_SELFCLEAR && + (!withShove || GetConVarInt(g_hCvarReport) & REP_SELFCLEARSHOVE ) + ) { + if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) ) + { + PrintToChatAll( "\x04%N\x01 self-cleared from \x05%N\x01's tongue%s.", attacker, victim, (withShove) ? " by shoving" : "" ); + } + else if ( IS_VALID_INGAME(attacker) ) + { + PrintToChatAll( "\x04%N\x01 self-cleared from a smoker tongue%s.", attacker, (withShove) ? " by shoving" : "" ); + } + } + + // call forward + Call_StartForward(g_hForwardSmokerSelfClear); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_PushCell(withShove); + Call_Finish(); +} + +// rocks +HandleRockEaten( attacker, victim ) +{ + Call_StartForward(g_hForwardRockEaten); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_Finish(); +} +HandleRockSkeeted( attacker, victim ) +{ + // report? + if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_ROCKSKEET ) + { + /* + if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) ) + { + PrintToChatAll( "\x04%N\x01 skeeted \x05%N\x01's rock.", attacker, victim ); + } + else if ( IS_VALID_INGAME(attacker) ) + { + } + */ + PrintToChatAll( "\x04%N\x01 skeeted a tank rock.", attacker ); + } + + Call_StartForward(g_hForwardRockSkeeted); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_Finish(); +} + +// highpounces +stock HandleHunterDP( attacker, victim, actualDamage, Float:calculatedDamage, Float:height, bool:playerIncapped = false ) +{ + // report? + if ( GetConVarBool(g_hCvarReport) + && GetConVarInt(g_hCvarReportFlags) & REP_HUNTERDP + && height >= GetConVarFloat(g_hCvarHunterDPThresh) + && !playerIncapped + ) { + if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(attacker) ) + { + PrintToChatAll( "\x04%N\x01 high-pounced \x05%N\x01 (\x03%i\x01 damage, height: \x05%i\x01).", attacker, victim, RoundFloat(calculatedDamage), RoundFloat(height) ); + } + else if ( IS_VALID_INGAME(victim) ) + { + PrintToChatAll( "A hunter high-pounced \x05%N\x01 (\x03%i\x01 damage, height: \x05%i\x01).", victim, RoundFloat(calculatedDamage), RoundFloat(height) ); + } + } + + Call_StartForward(g_hForwardHunterDP); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_PushCell(actualDamage); + Call_PushFloat(calculatedDamage); + Call_PushFloat(height); + Call_PushCell( (height >= GetConVarFloat(g_hCvarHunterDPThresh)) ? 1 : 0 ); + Call_PushCell( (playerIncapped) ? 1 : 0 ); + Call_Finish(); +} +stock HandleJockeyDP( attacker, victim, Float:height ) +{ + // report? + if ( GetConVarBool(g_hCvarReport) + && GetConVarInt(g_hCvarReportFlags) & REP_JOCKEYDP + && height >= GetConVarFloat(g_hCvarJockeyDPThresh) + ) { + if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(attacker) ) + { + PrintToChatAll( "\x04%N\x01 jockey high-pounced \x05%N\x01 (height: \x05%i\x01).", attacker, victim, RoundFloat(height) ); + } + else if ( IS_VALID_INGAME(victim) ) + { + PrintToChatAll( "A jockey high-pounced \x05%N\x01 (height: \x05%i\x01).", victim, RoundFloat(height) ); + } + } + + Call_StartForward(g_hForwardJockeyDP); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_PushFloat(height); + Call_PushCell( (height >= GetConVarFloat(g_hCvarJockeyDPThresh)) ? 1 : 0 ); + Call_Finish(); +} + +// deathcharges +stock HandleDeathCharge( attacker, victim, Float:height, Float:distance, bool:bCarried = true ) +{ + // report? + if ( GetConVarBool(g_hCvarReport) && + GetConVarInt(g_hCvarReportFlags) & REP_DEATHCHARGE && + height >= GetConVarFloat(g_hCvarDeathChargeHeight) + ) { + if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(attacker) ) + { + PrintToChatAll( "\x04%N\x01 death-charged \x05%N\x01 %s(height: \x05%i\x01).", + attacker, + victim, + (bCarried) ? "" : "by bowling ", + RoundFloat(height) + ); + } + else if ( IS_VALID_INGAME(victim) ) + { + PrintToChatAll( "A charger death-charged \x05%N\x01 %s(height: \x05%i\x01).", + victim, + (bCarried) ? "" : "by bowling ", + RoundFloat(height) + ); + } + } + + Call_StartForward(g_hForwardDeathCharge); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_PushFloat(height); + Call_PushFloat(distance); + Call_PushCell( (bCarried) ? 1 : 0 ); + Call_Finish(); +} + +// SI clears (cleartimeA = pummel/pounce/ride/choke, cleartimeB = tongue drag, charger carry) +stock HandleClear( attacker, victim, pinVictim, zombieClass, Float:clearTimeA, Float:clearTimeB, bool:bWithShove = false ) +{ + // sanity check: + if ( clearTimeA < 0 && clearTimeA != -1.0 ) { clearTimeA = 0.0; } + if ( clearTimeB < 0 && clearTimeB != -1.0 ) { clearTimeB = 0.0; } + + PrintDebug(0, "Clear: %i freed %i from %i: time: %.2f / %.2f -- class: %s (with shove? %i)", attacker, pinVictim, victim, clearTimeA, clearTimeB, g_csSIClassName[zombieClass], bWithShove ); + + if ( attacker != pinVictim && GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_INSTACLEAR ) + { + new Float: fMinTime = GetConVarFloat(g_hCvarInstaTime); + new Float: fClearTime = clearTimeA; + if ( zombieClass == ZC_CHARGER || zombieClass == ZC_SMOKER ) { fClearTime = clearTimeB; } + + + if ( fClearTime != -1.0 && fClearTime <= fMinTime ) + { + if ( IS_VALID_INGAME(attacker) && IS_VALID_INGAME(victim) && !IsFakeClient(victim) ) + { + if ( IS_VALID_INGAME(pinVictim) ) + { + PrintToChatAll( "\x04%N\x01 insta-cleared \x05%N\x01 from \x04%N\x01 (%s) (%.2f seconds).", + attacker, pinVictim, victim, + g_csSIClassName[zombieClass], + fClearTime + ); + } else { + PrintToChatAll( "\x04%N\x01 insta-cleared a teammate from \x04%N\x01 (%s) (%.2f seconds).", + attacker, victim, + g_csSIClassName[zombieClass], + fClearTime + ); + } + } + else if ( IS_VALID_INGAME(attacker) ) + { + if ( IS_VALID_INGAME(pinVictim) ) + { + PrintToChatAll( "\x04%N\x01 insta-cleared \x05%N\x01 from a %s (%.2f seconds).", + attacker, pinVictim, + g_csSIClassName[zombieClass], + fClearTime + ); + } else { + PrintToChatAll( "\x04%N\x01 insta-cleared a teammate from a %s (%.2f seconds).", + attacker, + g_csSIClassName[zombieClass], + fClearTime + ); + } + } + } + } + + Call_StartForward(g_hForwardClear); + Call_PushCell(attacker); + Call_PushCell(victim); + Call_PushCell(pinVictim); + Call_PushCell(zombieClass); + Call_PushFloat(clearTimeA); + Call_PushFloat(clearTimeB); + Call_PushCell( (bWithShove) ? 1 : 0 ); + Call_Finish(); +} + +// booms +stock HandleVomitLanded( attacker, boomCount ) +{ + Call_StartForward(g_hForwardVomitLanded); + Call_PushCell(attacker); + Call_PushCell(boomCount); + Call_Finish(); +} + +// bhaps +stock HandleBHopStreak( survivor, streak, Float: maxVelocity ) +{ + if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_BHOPSTREAK && + IS_VALID_INGAME(survivor) && !IsFakeClient(survivor) && + streak >= GetConVarInt(g_hCvarBHopMinStreak) + ) { + PrintToChatAll( "\x04%N\x01 got \x05%i\x01 bunnyhop%s in a row (top speed: \x05%.1f\x01).", + survivor, + streak, + ( streak > 1 ) ? "s" : "", + maxVelocity + ); + } + + Call_StartForward(g_hForwardBHopStreak); + Call_PushCell(survivor); + Call_PushCell(streak); + Call_PushFloat(maxVelocity); + Call_Finish(); +} +// car alarms +stock HandleCarAlarmTriggered( survivor, infected, reason ) +{ + if ( GetConVarBool(g_hCvarReport) && GetConVarInt(g_hCvarReportFlags) & REP_CARALARM && + IS_VALID_INGAME(survivor) && !IsFakeClient(survivor) + ) { + if ( reason == CALARM_HIT ) { + PrintToChatAll( "\x05%N\x01 triggered an alarm with a hit.", survivor ); + } + else if ( reason == CALARM_TOUCHED ) + { + // if a survivor touches an alarmed car, it might be due to a special infected... + if ( IS_VALID_INFECTED(infected) ) + { + if ( !IsFakeClient(infected) ) + { + PrintToChatAll( "\x04%N\x01 made \x05%N\x01 trigger an alarm.", infected, survivor ); + } + else { + switch ( GetEntProp(infected, Prop_Send, "m_zombieClass") ) + { + case ZC_SMOKER: { PrintToChatAll( "\x01A hunter made \x05%N\x01 trigger an alarm.", survivor ); } + case ZC_JOCKEY: { PrintToChatAll( "\x01A jockey made \x05%N\x01 trigger an alarm.", survivor ); } + case ZC_CHARGER: { PrintToChatAll( "\x01A charger made \x05%N\x01 trigger an alarm.", survivor ); } + default: { PrintToChatAll( "\x01A bot infected made \x05%N\x01 trigger an alarm.", survivor ); } + } + } + } + else + { + PrintToChatAll( "\x05%N\x01 touched an alarmed car.", survivor ); + } + } + else if ( reason == CALARM_EXPLOSION ) { + PrintToChatAll( "\x05%N\x01 triggered an alarm with an explosion.", survivor ); + } + else if ( reason == CALARM_BOOMER ) + { + if ( IS_VALID_INFECTED(infected) && !IsFakeClient(infected) ) + { + PrintToChatAll( "\x05%N\x01 triggered an alarm by killing a boomer \x04%N\x01.", survivor, infected ); + } + else + { + PrintToChatAll( "\x05%N\x01 triggered an alarm by shooting a boomer.", survivor ); + } + } + else { + PrintToChatAll( "\x05%N\x01 triggered an alarm.", survivor ); + } + } + + Call_StartForward(g_hForwardAlarmTriggered); + Call_PushCell(survivor); + Call_PushCell(infected); + Call_PushCell(reason); + Call_Finish(); +} + + +// support +// ------- + +stock GetSurvivorPermanentHealth(client) +{ + return GetEntProp(client, Prop_Send, "m_iHealth"); +} + +stock GetSurvivorTempHealth(client) +{ + new temphp = RoundToCeil( + GetEntPropFloat(client, Prop_Send, "m_healthBuffer") + - ( (GetGameTime() - GetEntPropFloat(client, Prop_Send, "m_healthBufferTime") ) + * GetConVarFloat( FindConVar("pain_pills_decay_rate"))) + ) - 1; + return (temphp > 0 ? temphp : 0); +} + +stock Float: GetSurvivorDistance(client) +{ + return L4D2Direct_GetFlowDistance(client); +} +stock ShiftTankThrower() +{ + new tank = -1; + + if ( !g_iRocksBeingThrownCount ) { return -1; } + + tank = g_iRocksBeingThrown[0]; + + // shift the tank array downwards, if there are more than 1 throwers + if ( g_iRocksBeingThrownCount > 1 ) + { + for ( new x = 1; x <= g_iRocksBeingThrownCount; x++ ) + { + g_iRocksBeingThrown[x-1] = g_iRocksBeingThrown[x]; + } + } + + g_iRocksBeingThrownCount--; + + return tank; +} +/* Height check.. + not required now + maybe for some other 'skill'? +static Float: GetHeightAboveGround( Float:pos[3] ) +{ + // execute Trace straight down + new Handle:trace = TR_TraceRayFilterEx( pos, ANGLE_STRAIGHT_DOWN, MASK_SHOT, RayType_Infinite, ChargeTraceFilter ); + + if (!TR_DidHit(trace)) + { + LogError("Tracer Bug: Trace did not hit anything..."); + } + + decl Float:vEnd[3]; + TR_GetEndPosition(vEnd, trace); // retrieve our trace endpoint + CloseHandle(trace); + + return GetVectorDistance(pos, vEnd, false); +} + +public bool: ChargeTraceFilter (entity, contentsMask) +{ + if ( !entity || !IsValidEntity(entity) ) // dont let WORLD, or invalid entities be hit + { + return false; + } + return true; +} +*/ + +stock PrintDebug(debuglevel, const String:Message[], any:... ) +{ + decl String:DebugBuff[256]; + VFormat(DebugBuff, sizeof(DebugBuff), Message, 3); + LogMessage(DebugBuff); +} +stock bool: IsWitch(entity) +{ + if ( !IsValidEntity(entity) ) { return false; } + + decl String: classname[24]; + new strOEC: classnameOEC; + GetEdictClassname(entity, classname, sizeof(classname)); + if ( !GetTrieValue(g_hTrieEntityCreated, classname, classnameOEC) || classnameOEC != OEC_WITCH ) { return false; } + + return true; +} \ No newline at end of file diff --git a/scripting/l4d2_target_test.sp b/scripting/l4d2_target_test.sp new file mode 100644 index 0000000..f76755f --- /dev/null +++ b/scripting/l4d2_target_test.sp @@ -0,0 +1,136 @@ +#pragma semicolon 1 +#pragma newdecls required + +//#define DEBUG + +#define PLUGIN_VERSION "1.0" +#define GAMEDATA "l4d_target_override" + +#include +#include +#include + +enum L4D2Infected +{ + L4D2Infected_None = 0, + L4D2Infected_Smoker = 1, + L4D2Infected_Boomer = 2, + L4D2Infected_Hunter = 3, + L4D2Infected_Spitter = 4, + L4D2Infected_Jockey = 5, + L4D2Infected_Charger = 6, + L4D2Infected_Witch = 7, + L4D2Infected_Tank = 8 +} + +public Plugin myinfo = +{ + name = "L4D2 target poo", + author = "jackzmc", + description = "", + version = PLUGIN_VERSION, + url = "" +}; + +bool g_bIsVictim[MAXPLAYERS+1]; +Handle g_hDetour; + +public void OnPluginStart() +{ + EngineVersion g_Game = GetEngineVersion(); + if(g_Game != Engine_Left4Dead2) + { + SetFailState("This plugin is for L4D2 only."); + } + + + RegAdminCmd("sm_set_victim", Cmd_SetVictim, ADMFLAG_CHEATS); + + HookEvent("player_death", Event_PlayerDeath); +} + +public void OnPluginEnd() +{ +} + +public Action Cmd_SetVictim(int client, int args) { + if(args == 0) { + ReplyToCommand(client, "Please enter a player to target"); + }else{ + char arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + char target_name[MAX_TARGET_LENGTH]; + int target_list[1], target_count; + bool tn_is_ml; + + if ((target_count = ProcessTargetString( + arg1, + client, + target_list, + MAXPLAYERS, + COMMAND_FILTER_ALIVE, /* Only allow alive players */ + target_name, + sizeof(target_name), + tn_is_ml)) <= 0) + { + /* This function replies to the admin with a failure message */ + ReplyToTargetError(client, target_count); + return Plugin_Handled; + } + for(int i = 0; i < target_count; i++) { + int victim = target_list[i]; + //g_iSITargets + g_bIsVictim[victim] = !g_bIsVictim[victim]; + ReplyToCommand(client, "Successfully toggled %N victim status to: %b", victim, g_bIsVictim[victim]); + ShowActivity(client, "toggled special infected victim status for %N to %b", victim, g_bIsVictim[victim]); + } + } + return Plugin_Handled; +} + +static int b_attackerTarget[MAXPLAYERS+1]; +public Action L4D2_OnChooseVictim(int attacker, int &curTarget) { + // ========================= + // OVERRIDE VICTIM + // ========================= + L4D2Infected class = view_as(GetEntProp(attacker, Prop_Send, "m_zombieClass")); + if(class != L4D2Infected_Tank) { + int existingTarget = GetClientOfUserId(b_attackerTarget[attacker]); + if(existingTarget > 0) { + curTarget = existingTarget; + return Plugin_Changed; + } + + float closestDistance, survPos[3], spPos[3]; + GetClientAbsOrigin(attacker, spPos); + int closestClient = -1; + for(int i = 1; i <= MaxClients; i++) { + if(g_bIsVictim[i] && IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) == 2) { + GetClientAbsOrigin(i, survPos); + float dist = GetVectorDistance(survPos, spPos, true); + if(closestClient == -1 || dist < closestDistance) { + closestDistance = dist; + closestClient = i; + } + } + } + + if(closestClient > 0) { + PrintToConsoleAll("Attacker %N new target: %N", attacker, closestClient); + b_attackerTarget[attacker] = GetClientUserId(closestClient); + curTarget = closestClient; + return Plugin_Changed; + } + } + return Plugin_Continue; +} + +public Action Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast) { + int client = GetClientOfUserId(event.GetInt("userid")); + b_attackerTarget[client] = 0; +} + +public void OnClientDisconnect(int client) { + b_attackerTarget[client] = 0; +} \ No newline at end of file diff --git a/scripting/l4d_tank_hp_sprite.sp b/scripting/l4d_tank_hp_sprite.sp index 7e5fa09..c3035cf 100644 --- a/scripting/l4d_tank_hp_sprite.sp +++ b/scripting/l4d_tank_hp_sprite.sp @@ -2,11 +2,24 @@ // ==================================================================================================== Change Log: +1.0.6 (12-February-2021) + - Fixed custom model cvar typo. (thanks "weffer" for reporting) + +1.0.5 (11-February-2021) + - Fixed a bug not rendering custom sprites right after turning it on. + - Added one more custom sprite option with an alpha background filling the bar. + +1.0.4 (11-February-2021) + - Added custom sprite option. + +1.0.3 (08-February-2021) + - Fixed missing client in-game in visibility check. (thanks to "Krufftys Killers" and "Striker black") + 1.0.2 (08-February-2021) - Fixed wrong value on max health calculation. - Fixed sprite hiding behind tank rocks. - Fixed sprite hiding while tank throws rocks (ability use). - - Moved visibility logic to timer. + - Moved visibility logic to timer handle. 1.0.1 (30-January-2021) - Public release. @@ -23,7 +36,7 @@ Change Log: #define PLUGIN_NAME "[L4D1 & L4D2] Tank HP Sprite" #define PLUGIN_AUTHOR "Mart" #define PLUGIN_DESCRIPTION "Shows a sprite at the tank head that goes from green to red based on its HP" -#define PLUGIN_VERSION "1.0.2" +#define PLUGIN_VERSION "1.0.5" #define PLUGIN_URL "https://forums.alliedmods.net/showthread.php?t=330370" // ==================================================================================================== @@ -66,6 +79,7 @@ public Plugin myinfo = // Defines // ==================================================================================================== #define CLASSNAME_ENV_SPRITE "env_sprite" +#define CLASSNAME_ENV_TEXTURETOGGLE "env_texturetoggle" #define CLASSNAME_TANK_ROCK "tank_rock" #define TEAM_SPECTATOR 1 @@ -102,6 +116,9 @@ static ConVar g_hCvar_DeadAlpha; static ConVar g_hCvar_DeadScale; static ConVar g_hCvar_DeadColor; static ConVar g_hCvar_Team; +static ConVar g_hCvar_CustomModel; +static ConVar g_hCvar_CustomModelVMT; +static ConVar g_hCvar_CustomModelVTF; static ConVar g_hCvar_AllSpecials; // ==================================================================================================== @@ -115,6 +132,7 @@ static bool g_bCvar_Sight; static bool g_bCvar_AttackDelay; static bool g_bCvar_AliveShow; static bool g_bCvar_DeadShow; +static bool g_bCvar_CustomModel; // ==================================================================================================== // int - Plugin Variables @@ -147,11 +165,14 @@ static char g_sCvar_DeadAlpha[4]; static char g_sCvar_DeadScale[5]; static char g_sCvar_DeadColor[12]; static char g_sCvar_FadeDistance[5]; +static char g_sCvar_CustomModelVMT[100]; +static char g_sCvar_CustomModelVTF[100]; // ==================================================================================================== // client - Plugin Variables // ==================================================================================================== static int gc_iTankSpriteRef[MAXPLAYERS+1] = { INVALID_ENT_REFERENCE, ... }; +static int gc_iTankSpriteFrameRef[MAXPLAYERS+1] = { INVALID_ENT_REFERENCE, ... }; static bool gc_bVisible[MAXPLAYERS+1][MAXPLAYERS+1]; static float gc_fLastAttack[MAXPLAYERS+1][MAXPLAYERS+1]; @@ -185,21 +206,24 @@ public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max public void OnPluginStart() { CreateConVar("l4d_tank_hp_sprite_version", PLUGIN_VERSION, PLUGIN_DESCRIPTION, CVAR_FLAGS_PLUGIN_VERSION); - g_hCvar_Enabled = CreateConVar("l4d_tank_hp_sprite_enable", "1", "Enable/Disable the plugin.\n0 = Disable, 1 = Enable", CVAR_FLAGS, true, 0.0, true, 1.0); - g_hCvar_ZAxis = CreateConVar("l4d_tank_hp_sprite_z_axis", "92", "Additional Z distance based on the tank position.", CVAR_FLAGS, true, 0.0); - g_hCvar_FadeDistance = CreateConVar("l4d_tank_hp_sprite_fade_distance", "-1", "Minimum distance that a client must be from the tank to see the sprite (both alive and dead sprites).\n-1 = Always visible.", CVAR_FLAGS, true, -1.0, true, 9999.0); - g_hCvar_Sight = CreateConVar("l4d_tank_hp_sprite_sight", "1", "Show the sprite to the survivor only if the Tank is on sight.\n0 = OFF, 1 = ON.", CVAR_FLAGS, true, 0.0, true, 1.0); - g_hCvar_AttackDelay = CreateConVar("l4d_tank_hp_sprite_attack_delay", "0.0", "Show the sprite to the survivor attacker, by this amount of time in seconds, after hitting the Tank.\n0 = OFF.", CVAR_FLAGS, true, 0.0); - g_hCvar_AliveShow = CreateConVar("l4d_tank_hp_sprite_alive_show", "1", "Show the alive sprite while tank is alive.\n0 = OFF, 1 = ON.", CVAR_FLAGS, true, 0.0, true, 1.0); - g_hCvar_AliveModel = CreateConVar("l4d_tank_hp_sprite_alive_model", "materials/vgui/healthbar_white.vmt", "Model of alive tank sprite."); - g_hCvar_AliveAlpha = CreateConVar("l4d_tank_hp_sprite_alive_alpha", "200", "Alpha of alive tank sprite.\n0 = Invisible, 255 = Fully Visible", CVAR_FLAGS, true, 0.0, true, 255.0); - g_hCvar_AliveScale = CreateConVar("l4d_tank_hp_sprite_alive_scale", "0.25", "Scale of alive tank sprite (increases both height and width).\nNote: Some range values maintain the same size. (e.g. from 0.0 to 0.38 the size doesn't change).", CVAR_FLAGS, true, 0.0); - g_hCvar_DeadShow = CreateConVar("l4d_tank_hp_sprite_dead_show", "1", "Show the dead sprite when a tank dies.\n0 = OFF, 1 = ON.", CVAR_FLAGS, true, 0.0, true, 1.0); - g_hCvar_DeadModel = CreateConVar("l4d_tank_hp_sprite_dead_model", "materials/sprites/death_icon.vmt", "Model of dead tank sprite."); - g_hCvar_DeadAlpha = CreateConVar("l4d_tank_hp_sprite_dead_alpha", "200", "Alpha of dead tank sprite.\n0 = Invisible, 255 = Fully Visible", CVAR_FLAGS, true, 0.0, true, 255.0); - g_hCvar_DeadScale = CreateConVar("l4d_tank_hp_sprite_dead_scale", "0.25", "Scale of dead tank sprite (increases both height and width).\nSome range values maintain the size the same.", CVAR_FLAGS, true, 0.0); - g_hCvar_DeadColor = CreateConVar("l4d_tank_hp_sprite_dead_color", "225 0 0", "Color of dead tank sprite.\nUse three values between 0-255 separated by spaces (\"<0-255> <0-255> <0-255>\").", CVAR_FLAGS); - g_hCvar_Team = CreateConVar("l4d_tank_hp_sprite_team", "3", "Which teams should the sprite be visible.\n0 = NONE, 1 = SURVIVOR, 2 = INFECTED, 4 = SPECTATOR, 8 = HOLDOUT.\nAdd numbers greater than 0 for multiple options.\nExample: \"3\", enables for SURVIVOR and INFECTED.", CVAR_FLAGS, true, 0.0, true, 15.0); + g_hCvar_Enabled = CreateConVar("l4d_tank_hp_sprite_enable", "1", "Enable/Disable the plugin.\n0 = Disable, 1 = Enable.", CVAR_FLAGS, true, 0.0, true, 1.0); + g_hCvar_ZAxis = CreateConVar("l4d_tank_hp_sprite_z_axis", "92", "Additional Z distance based on the tank position.", CVAR_FLAGS, true, 0.0); + g_hCvar_FadeDistance = CreateConVar("l4d_tank_hp_sprite_fade_distance", "-1", "Minimum distance that a client must be from the tank to see the sprite (both alive and dead sprites).\n-1 = Always visible.", CVAR_FLAGS, true, -1.0, true, 9999.0); + g_hCvar_Sight = CreateConVar("l4d_tank_hp_sprite_sight", "1", "Show the sprite to the survivor only if the Tank is on sight.\n0 = OFF, 1 = ON.", CVAR_FLAGS, true, 0.0, true, 1.0); + g_hCvar_AttackDelay = CreateConVar("l4d_tank_hp_sprite_attack_delay", "0.0", "Show the sprite to the survivor attacker, by this amount of time in seconds, after hitting the Tank.\n0 = OFF.", CVAR_FLAGS, true, 0.0); + g_hCvar_AliveShow = CreateConVar("l4d_tank_hp_sprite_alive_show", "1", "Show the alive sprite while tank is alive.\n0 = OFF, 1 = ON.", CVAR_FLAGS, true, 0.0, true, 1.0); + g_hCvar_AliveModel = CreateConVar("l4d_tank_hp_sprite_alive_model", "materials/vgui/healthbar_white.vmt", "Model of alive tank sprite."); + g_hCvar_AliveAlpha = CreateConVar("l4d_tank_hp_sprite_alive_alpha", "200", "Alpha of alive tank sprite.\n0 = Invisible, 255 = Fully Visible", CVAR_FLAGS, true, 0.0, true, 255.0); + g_hCvar_AliveScale = CreateConVar("l4d_tank_hp_sprite_alive_scale", "0.25", "Scale of alive tank sprite (increases both height and width).\nNote: Some range values maintain the same size. (e.g. from 0.0 to 0.38 the size doesn't change).", CVAR_FLAGS, true, 0.0); + g_hCvar_DeadShow = CreateConVar("l4d_tank_hp_sprite_dead_show", "1", "Show the dead sprite when a tank dies.\n0 = OFF, 1 = ON.", CVAR_FLAGS, true, 0.0, true, 1.0); + g_hCvar_DeadModel = CreateConVar("l4d_tank_hp_sprite_dead_model", "materials/sprites/death_icon.vmt", "Model of dead tank sprite."); + g_hCvar_DeadAlpha = CreateConVar("l4d_tank_hp_sprite_dead_alpha", "200", "Alpha of dead tank sprite.\n0 = Invisible, 255 = Fully Visible", CVAR_FLAGS, true, 0.0, true, 255.0); + g_hCvar_DeadScale = CreateConVar("l4d_tank_hp_sprite_dead_scale", "0.25", "Scale of dead tank sprite (increases both height and width).\nSome range values maintain the size the same.", CVAR_FLAGS, true, 0.0); + g_hCvar_DeadColor = CreateConVar("l4d_tank_hp_sprite_dead_color", "225 0 0", "Color of dead tank sprite.\nUse three values between 0-255 separated by spaces (\"<0-255> <0-255> <0-255>\").", CVAR_FLAGS); + g_hCvar_Team = CreateConVar("l4d_tank_hp_sprite_team", "3", "Which teams should the sprite be visible.\n0 = NONE, 1 = SURVIVOR, 2 = INFECTED, 4 = SPECTATOR, 8 = HOLDOUT.\nAdd numbers greater than 0 for multiple options.\nExample: \"3\", enables for SURVIVOR and INFECTED.", CVAR_FLAGS, true, 0.0, true, 15.0); + g_hCvar_CustomModel = CreateConVar("l4d_tank_hp_sprite_custom_model", "0", "Use a custom sprite for the alive model\nNote: This requires the client downloading the custom model (.vmt and .vtf) to work.\nSearch for FastDL for more info.\n0 = OFF, 1 = ON.", CVAR_FLAGS, true, 0.0, true, 1.0); + g_hCvar_CustomModelVMT = CreateConVar("l4d_tank_hp_sprite_custom_model_vmt", "materials/mart/mart_custombar.vmt", "Custom sprite VMT path."); + g_hCvar_CustomModelVTF = CreateConVar("l4d_tank_hp_sprite_custom_model_vtf", "materials/mart/mart_custombar.vtf", "Custom sprite VTF path."); g_hCvar_AllSpecials = CreateConVar("l4d_tank_hp_sprite_all_specials", "1", "Should all specials have healthbar or only tanks\n0 = Tanks Only, 1 = All Specials", CVAR_FLAGS, true, 0.0, true, 1.0); g_hCvar_Enabled.AddChangeHook(Event_ConVarChanged); @@ -217,6 +241,9 @@ public void OnPluginStart() g_hCvar_DeadScale.AddChangeHook(Event_ConVarChanged); g_hCvar_DeadColor.AddChangeHook(Event_ConVarChanged); g_hCvar_Team.AddChangeHook(Event_ConVarChanged); + g_hCvar_CustomModel.AddChangeHook(Event_ConVarChanged); + g_hCvar_CustomModelVMT.AddChangeHook(Event_ConVarChanged); + g_hCvar_CustomModelVTF.AddChangeHook(Event_ConVarChanged); // Load plugin configs from .cfg AutoExecConfig(true, CONFIG_FILENAME); @@ -234,7 +261,7 @@ public void OnPluginStart() public void OnPluginEnd() { int entity; - char targetname[64]; + char targetname[19]; entity = INVALID_ENT_REFERENCE; while ((entity = FindEntityByClassname(entity, CLASSNAME_ENV_SPRITE)) != INVALID_ENT_REFERENCE) @@ -285,7 +312,6 @@ public void GetCvars() g_bCvar_AliveShow = g_hCvar_AliveShow.BoolValue; g_hCvar_AliveModel.GetString(g_sCvar_AliveModel, sizeof(g_sCvar_AliveModel)); TrimString(g_sCvar_AliveModel); - PrecacheModel(g_sCvar_AliveModel, true); g_iCvar_AliveAlpha = g_hCvar_AliveAlpha.IntValue; FormatEx(g_sCvar_AliveAlpha, sizeof(g_sCvar_AliveAlpha), "%i", g_iCvar_AliveAlpha); g_fCvar_AliveScale = g_hCvar_AliveScale.FloatValue; @@ -293,7 +319,6 @@ public void GetCvars() g_bCvar_DeadShow = g_hCvar_DeadShow.BoolValue; g_hCvar_DeadModel.GetString(g_sCvar_DeadModel, sizeof(g_sCvar_DeadModel)); TrimString(g_sCvar_DeadModel); - PrecacheModel(g_sCvar_DeadModel, true); g_iCvar_DeadAlpha = g_hCvar_DeadAlpha.IntValue; FormatEx(g_sCvar_DeadAlpha, sizeof(g_sCvar_DeadAlpha), "%i", g_iCvar_DeadAlpha); g_fCvar_DeadScale = g_hCvar_DeadScale.FloatValue; @@ -301,6 +326,28 @@ public void GetCvars() g_hCvar_DeadColor.GetString(g_sCvar_DeadColor, sizeof(g_sCvar_DeadColor)); TrimString(g_sCvar_DeadColor); g_iCvar_Team = g_hCvar_Team.IntValue; + g_bCvar_CustomModel = g_hCvar_CustomModel.BoolValue; + g_hCvar_CustomModelVMT.GetString(g_sCvar_CustomModelVMT, sizeof(g_sCvar_CustomModelVMT)); + TrimString(g_sCvar_CustomModelVMT); + g_hCvar_CustomModelVTF.GetString(g_sCvar_CustomModelVTF, sizeof(g_sCvar_CustomModelVTF)); + TrimString(g_sCvar_CustomModelVTF); + + if (g_bCvar_AliveShow) + { + if (g_bCvar_CustomModel) + { + AddFileToDownloadsTable(g_sCvar_CustomModelVMT); + AddFileToDownloadsTable(g_sCvar_CustomModelVTF); + PrecacheModel(g_sCvar_CustomModelVMT, true); + } + else + { + PrecacheModel(g_sCvar_AliveModel, true); + } + } + + if (g_bCvar_DeadShow) + PrecacheModel(g_sCvar_DeadModel, true); } /****************************************************************************************************/ @@ -309,8 +356,10 @@ public void LateLoad() { for (int client = 1; client <= MaxClients; client++) { - if (IsPlayerSpecialInfected(client)) - TankSprite(client); + if (!IsPlayerTank(client)) + continue; + + TankSprite(client); } } @@ -322,6 +371,7 @@ public void OnClientDisconnect(int client) return; gc_iTankSpriteRef[client] = INVALID_ENT_REFERENCE; + gc_iTankSpriteFrameRef[client] = INVALID_ENT_REFERENCE; for (int target = 1; target <= MaxClients; target++) { @@ -372,6 +422,7 @@ public void HookEvents(bool hook) { g_bEventsHooked = true; + HookEvent("tank_spawn", Event_TankSpawn); HookEvent("player_hurt", Event_PlayerHurt); return; @@ -381,6 +432,7 @@ public void HookEvents(bool hook) { g_bEventsHooked = false; + UnhookEvent("tank_spawn", Event_TankSpawn); UnhookEvent("player_hurt", Event_PlayerHurt); return; @@ -389,27 +441,37 @@ public void HookEvents(bool hook) /****************************************************************************************************/ +public void Event_TankSpawn(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); -public void OnClientPutInServer(int client) { - if(GetClientTeam(client) == TEAM_INFECTED) { - //If all specials turned off and not tank; ignore. - if(!g_hCvar_AllSpecials.BoolValue && GetZombieClass(client) != g_iTankClass) return; - TankSprite(client); - } + if (!IsValidClient(client)) + return; + + TankSprite(client); } /****************************************************************************************************/ -public void Event_PlayerHurt(Event event, const char[] name, bool dontBroadcast) { - int tank = GetClientOfUserId(event.GetInt("userid")); - if (IsPlayerSpecialInfected(tank)) { - TankSprite(tank); - if(g_bCvar_AttackDelay) { - int attacker = GetClientOfUserId(event.GetInt("attacker")); - if (IsValidClient(attacker) && GetClientTeam(attacker) != TEAM_SURVIVOR) - gc_fLastAttack[tank][attacker] = GetGameTime(); - } - } +public void Event_PlayerHurt(Event event, const char[] name, bool dontBroadcast) +{ + if (!g_bCvar_AttackDelay) + return; + + int target = GetClientOfUserId(event.GetInt("userid")); + + if (!IsPlayerTank(target)) + return; + + int attacker = GetClientOfUserId(event.GetInt("attacker")); + + if (!IsValidClient(attacker)) + return; + + if (GetClientTeam(attacker) != TEAM_SURVIVOR) + return; + + gc_fLastAttack[target][attacker] = GetGameTime(); } /****************************************************************************************************/ @@ -419,21 +481,26 @@ public Action TimerKill(Handle timer) if (!g_bConfigLoaded) return Plugin_Continue; - for (int client = 1; client <= MaxClients; client++) + for (int target = 1; target <= MaxClients; target++) { - if (gc_iTankSpriteRef[client] != INVALID_ENT_REFERENCE && !IsPlayerSpecialInfected(client)) { - int entity = EntRefToEntIndex(gc_iTankSpriteRef[client]); + if (gc_iTankSpriteRef[target] == INVALID_ENT_REFERENCE) + continue; - if (entity != INVALID_ENT_REFERENCE) - AcceptEntityInput(entity, "Kill"); + if (g_bCvar_Enabled && IsPlayerTank(target)) + continue; - gc_iTankSpriteRef[client] = INVALID_ENT_REFERENCE; + int entity = EntRefToEntIndex(gc_iTankSpriteRef[target]); - for (int client2 = 1; client2 <= MaxClients; client2++) - { - gc_bVisible[client][client2] = false; - gc_fLastAttack[client][client2] = 0.0; - } + if (entity != INVALID_ENT_REFERENCE) + AcceptEntityInput(entity, "Kill"); + + gc_iTankSpriteRef[target] = INVALID_ENT_REFERENCE; + gc_iTankSpriteFrameRef[target] = INVALID_ENT_REFERENCE; + + for (int client = 1; client <= MaxClients; client++) + { + gc_bVisible[target][client] = false; + gc_fLastAttack[target][client] = 0.0; } } @@ -455,38 +522,31 @@ public Action TimerVisible(Handle timer) if (gc_iTankSpriteRef[target] == INVALID_ENT_REFERENCE) continue; - if (!IsClientConnected(target)) + if (!IsClientInGame(target)) continue; for (int client = 1; client <= MaxClients; client++) { - if (!IsClientConnected(client)) + gc_bVisible[target][client] = false; + + if (!IsClientInGame(client)) continue; if (IsFakeClient(client)) continue; - if (!(GetClientTeamFlag(client) & g_iCvar_Team)) - { - gc_bVisible[target][client] = false; + if (!(GetTeamFlag(GetClientTeam(client)) & g_iCvar_Team)) continue; - } if (g_bCvar_AttackDelay || g_bCvar_Sight) { if (GetClientTeam(client) == TEAM_SURVIVOR || GetClientTeam(client) == TEAM_HOLDOUT) { if (g_bCvar_AttackDelay && (GetGameTime() - gc_fLastAttack[target][client] > g_fCvar_AttackDelay)) - { - gc_bVisible[target][client] = false; continue; - } if (g_bCvar_Sight && !IsVisibleTo(client, target)) - { - gc_bVisible[target][client] = false; continue; - } } } @@ -509,7 +569,7 @@ public Action TimerRender(Handle timer) for (int target = 1; target <= MaxClients; target++) { - if (!IsPlayerSpecialInfected(target)) + if (!IsPlayerTank(target)) continue; TankSprite(target); @@ -529,14 +589,43 @@ public void TankSprite(int client) if (entity == INVALID_ENT_REFERENCE) { + char targetname[22]; + FormatEx(targetname, sizeof(targetname), "%s-%i", "l4d_tank_hp_sprite", client); + entity = CreateEntityByName(CLASSNAME_ENV_SPRITE); - DispatchKeyValue(entity, "targetname", "l4d_tank_hp_sprite"); + ge_iOwner[entity] = client; + gc_iTankSpriteRef[client] = EntIndexToEntRef(entity); + DispatchKeyValue(entity, "targetname", targetname); DispatchKeyValue(entity, "spawnflags", "1"); DispatchKeyValue(entity, "fademindist", g_sCvar_FadeDistance); SetEntProp(entity, Prop_Data, "m_iHammerID", -1); SDKHook(entity, SDKHook_SetTransmit, OnSetTransmit); - ge_iOwner[entity] = client; - gc_iTankSpriteRef[client] = EntIndexToEntRef(entity); + } + + if (g_bCvar_CustomModel) + { + int entityFrame = INVALID_ENT_REFERENCE; + + if (gc_iTankSpriteFrameRef[client] != INVALID_ENT_REFERENCE) + entityFrame = EntRefToEntIndex(gc_iTankSpriteFrameRef[client]); + + if (entityFrame == INVALID_ENT_REFERENCE) + { + char targetname[22]; + FormatEx(targetname, sizeof(targetname), "%s-%i", "l4d_tank_hp_sprite", client); + + entityFrame = CreateEntityByName(CLASSNAME_ENV_TEXTURETOGGLE); + gc_iTankSpriteFrameRef[client] = EntIndexToEntRef(entityFrame); + DispatchKeyValue(entityFrame, "targetname", "l4d_tank_hp_sprite"); + DispatchKeyValue(entityFrame, "target", targetname); + + TeleportEntity(entityFrame, g_fVPos, NULL_VECTOR, NULL_VECTOR); + DispatchSpawn(entityFrame); + ActivateEntity(entityFrame); + + SetVariantString("!activator"); + AcceptEntityInput(entityFrame, "SetParent", entity); + } } if (IsPlayerIncapacitated(client)) @@ -547,12 +636,15 @@ public void TankSprite(int client) DispatchKeyValue(entity, "rendercolor", g_sCvar_DeadColor); DispatchKeyValue(entity, "renderamt", g_sCvar_DeadAlpha); // If renderamt goes before rendercolor, it doesn't render DispatchKeyValue(entity, "scale", g_sCvar_DeadScale); + + TeleportEntity(entity, g_fVPos, NULL_VECTOR, NULL_VECTOR); DispatchSpawn(entity); + ActivateEntity(entity); + SetEntPropEnt(entity, Prop_Send, "m_hOwnerEntity", client); SetVariantString("!activator"); AcceptEntityInput(entity, "SetParent", client); AcceptEntityInput(entity, "ShowSprite"); - TeleportEntity(entity, g_fVPos, NULL_VECTOR, NULL_VECTOR); } return; @@ -568,9 +660,7 @@ public void TankSprite(int client) int currentHealth = GetClientHealth(client); float percentageHealth; - if (maxHealth == 0) - percentageHealth = 0.0; - else + if (maxHealth > 0) percentageHealth = (float(currentHealth) / float(maxHealth)); bool halfHealth = (percentageHealth <= 0.5); @@ -578,16 +668,35 @@ public void TankSprite(int client) char sRenderColor[12]; Format(sRenderColor, sizeof(sRenderColor), "%i %i 0", halfHealth ? 255 : RoundFloat(255.0 * ((1.0 - percentageHealth) * 2)), halfHealth ? RoundFloat(255.0 * (percentageHealth) * 2) : 255); - DispatchKeyValue(entity, "model", g_sCvar_AliveModel); + DispatchKeyValue(entity, "model", g_bCvar_CustomModel ? g_sCvar_CustomModelVMT : g_sCvar_AliveModel); DispatchKeyValue(entity, "rendercolor", sRenderColor); DispatchKeyValue(entity, "renderamt", g_sCvar_AliveAlpha); // If renderamt goes before rendercolor, it doesn't render DispatchKeyValue(entity, "scale", g_sCvar_AliveScale); + + TeleportEntity(entity, g_fVPos, NULL_VECTOR, NULL_VECTOR); DispatchSpawn(entity); + ActivateEntity(entity); + SetEntPropEnt(entity, Prop_Send, "m_hOwnerEntity", client); SetVariantString("!activator"); AcceptEntityInput(entity, "SetParent", client); AcceptEntityInput(entity, "ShowSprite"); - TeleportEntity(entity, g_fVPos, NULL_VECTOR, NULL_VECTOR); + + if (!g_bCvar_CustomModel) + return; + + int entityFrame = EntRefToEntIndex(gc_iTankSpriteFrameRef[client]); + + if (entityFrame == INVALID_ENT_REFERENCE) + return; + + int frame = RoundFloat(percentageHealth * 100); + + char input[38]; + FormatEx(input, sizeof(input), "OnUser1 !self:SetTextureIndex:%i:0:1", frame); + SetVariantString(input); + AcceptEntityInput(entityFrame, "AddOutput"); + AcceptEntityInput(entityFrame, "FireUser1"); } /****************************************************************************************************/ @@ -612,14 +721,14 @@ bool IsVisibleTo(int client, int target) float vClientPos[3]; float vEntityPos[3]; float vLookAt[3]; - float vAngles[3]; + float vAng[3]; GetClientEyePosition(client, vClientPos); GetClientEyePosition(target, vEntityPos); MakeVectorFromPoints(vClientPos, vEntityPos, vLookAt); - GetVectorAngles(vLookAt, vAngles); + GetVectorAngles(vLookAt, vAng); - Handle trace = TR_TraceRayFilterEx(vClientPos, vAngles, MASK_PLAYERSOLID, RayType_Infinite, TraceFilter, target); + Handle trace = TR_TraceRayFilterEx(vClientPos, vAng, MASK_PLAYERSOLID, RayType_Infinite, TraceFilter, target); bool isVisible; @@ -682,6 +791,9 @@ public Action CmdPrintCvars(int client, int args) PrintToConsole(client, "l4d_tank_hp_sprite_dead_scale : %.2f", g_fCvar_DeadScale); PrintToConsole(client, "l4d_tank_hp_sprite_dead_color : \"%s\"", g_sCvar_DeadColor); PrintToConsole(client, "l4d_tank_hp_sprite_team : %i", g_iCvar_Team); + PrintToConsole(client, "l4d_tank_hp_sprite_custom_model : %b (%s)", g_bCvar_CustomModel, g_bCvar_CustomModel ? "true" : "false"); + PrintToConsole(client, "l4d_tank_hp_sprite_custom_model_vmt : \"%s\"", g_sCvar_CustomModelVMT); + PrintToConsole(client, "l4d_tank_hp_sprite_custom_model_vtf : \"%s\"", g_sCvar_CustomModelVTF); PrintToConsole(client, ""); PrintToConsole(client, "======================================================================"); PrintToConsole(client, ""); @@ -777,7 +889,8 @@ bool IsPlayerIncapacitated(int client) * @param client Client index. * @return True if client is a tank, false otherwise. */ -bool IsPlayerSpecialInfected(int client) { +bool IsPlayerTank(int client) +{ bool isValid = IsValidClient(client) && GetClientTeam(client) == TEAM_INFECTED && IsPlayerAlive(client) && !IsPlayerGhost(client); if(!g_hCvar_AllSpecials.BoolValue && GetZombieClass(client) != g_iTankClass) return false; @@ -788,14 +901,14 @@ bool IsPlayerSpecialInfected(int client) { /****************************************************************************************************/ /** - * Returns the team flag from a client. + * Returns the team flag from a team. * - * @param client Client index. - * @return Client team flag. + * @param team Team index. + * @return Team flag. */ -int GetClientTeamFlag(int client) +int GetTeamFlag(int team) { - switch (GetClientTeam(client)) + switch (team) { case TEAM_SURVIVOR: return FLAG_TEAM_SURVIVOR; diff --git a/scripting/l4dunreservelobby.sp b/scripting/l4dunreservelobby.sp new file mode 100644 index 0000000..1814300 --- /dev/null +++ b/scripting/l4dunreservelobby.sp @@ -0,0 +1,149 @@ +#include +#include +#include + +#define UNRESERVE_VERSION "1.1.1" + +#define UNRESERVE_DEBUG 0 +#define UNRESERVE_DEBUG_LOG 0 + +#define L4D_MAXCLIENTS MaxClients +#define L4D_MAXCLIENTS_PLUS1 (L4D_MAXCLIENTS + 1) + +#define L4D_MAXHUMANS_LOBBY_VERSUS 8 +#define L4D_MAXHUMANS_LOBBY_OTHER 4 + +new Handle:cvarGameMode = INVALID_HANDLE; + +public Plugin:myinfo = +{ + name = "L4D1/2 Remove Lobby Reservation", + author = "Downtown1", + description = "Removes lobby reservation when server is full", + version = UNRESERVE_VERSION, + url = "http://forums.alliedmods.net/showthread.php?t=87759" +} + +new Handle:cvarUnreserve = INVALID_HANDLE; + +public OnPluginStart() +{ + LoadTranslations("common.phrases"); + + RegAdminCmd("sm_unreserve", Command_Unreserve, ADMFLAG_BAN, "sm_unreserve - manually force removes the lobby reservation"); + + cvarUnreserve = CreateConVar("l4d_unreserve_full", "1", "Automatically unreserve server after a full lobby joins", FCVAR_SPONLY|FCVAR_NOTIFY); + CreateConVar("l4d_unreserve_version", UNRESERVE_VERSION, "Version of the Lobby Unreserve plugin.", FCVAR_SPONLY|FCVAR_NOTIFY); + + cvarGameMode = FindConVar("mp_gamemode"); +} + +bool:IsScavengeMode() +{ + decl String:sGameMode[32]; + GetConVarString(cvarGameMode, sGameMode, sizeof(sGameMode)); + if (StrContains(sGameMode, "scavenge") > -1) + { + return true; + } + else + { + return false; + } +} + +bool:IsVersusMode() +{ + decl String:sGameMode[32]; + GetConVarString(cvarGameMode, sGameMode, sizeof(sGameMode)); + if (StrContains(sGameMode, "versus") > -1) + { + return true; + } + else + { + return false; + } +} + +IsServerLobbyFull() +{ + new humans = GetHumanCount(); + + DebugPrintToAll("IsServerLobbyFull : humans = %d", humans); + + if(IsVersusMode() || IsScavengeMode()) + { + return humans >= L4D_MAXHUMANS_LOBBY_VERSUS; + } + return humans >= L4D_MAXHUMANS_LOBBY_OTHER; +} + +public OnClientPutInServer(client) +{ + DebugPrintToAll("Client put in server %N", client); + + if(GetConVarBool(cvarUnreserve) && /*L4D_LobbyIsReserved() &&*/ IsServerLobbyFull()) + { + //PrintToChatAll("[SM] A full lobby has connected, automatically unreserving the server."); + L4D_LobbyUnreserve(); + } +} + +public Action:Command_Unreserve(client, args) +{ + /*if(!L4D_LobbyIsReserved()) + { + ReplyToCommand(client, "[SM] Server is already unreserved."); + }*/ + + L4D_LobbyUnreserve(); + PrintToChatAll("[SM] Lobby reservation has been removed."); + + return Plugin_Handled; +} + + +//client is in-game and not a bot +stock bool:IsClientInGameHuman(client) +{ + return IsClientInGame(client) && !IsFakeClient(client); +} + +stock GetHumanCount() +{ + new humans = 0; + + new i; + for(i = 1; i < L4D_MAXCLIENTS_PLUS1; i++) + { + if(IsClientInGameHuman(i)) + { + humans++ + } + } + + return humans; +} + +DebugPrintToAll(const String:format[], any:...) +{ + #if UNRESERVE_DEBUG || UNRESERVE_DEBUG_LOG + decl String:buffer[192]; + + VFormat(buffer, sizeof(buffer), format, 2); + + #if UNRESERVE_DEBUG + PrintToChatAll("[UNRESERVE] %s", buffer); + PrintToConsole(0, "[UNRESERVE] %s", buffer); + #endif + + LogMessage("%s", buffer); + #else + //suppress "format" never used warning + if(format[0]) + return; + else + return; + #endif +} \ No newline at end of file diff --git a/scripting/sceneprocessor.sp b/scripting/sceneprocessor.sp new file mode 100644 index 0000000..dc2c2d1 --- /dev/null +++ b/scripting/sceneprocessor.sp @@ -0,0 +1,855 @@ +#define PLUGIN_VERSION "1.33.2" + +#pragma semicolon 1 +#pragma newdecls required + +#include +#include +#include + +#include + +int iSkippedFrames; +char sVocalizeScene[MAXPLAYERS+1][MAX_VOCALIZE_LENGTH]; +bool bSceneHasInitiator[MAXPLAYERS+1], bScenesUnprocessed, bUnvocalizedCommands, bJailbreakVocalize, bIsL4D; + +float fStartTimeStamp, fVocalizePreDelay[MAXPLAYERS+1], fVocalizePitch[MAXPLAYERS+1]; +Handle hSceneStageForward, hVocalizeCommandForward; + +ArrayList alVocalize; +ArrayStack asScene; + +enum struct SceneData +{ + SceneStages ssDataBit; + bool bInFakePostSpawn; + float fTimeStampData; + int iActorData; + int iInitiatorData; + char sFileData[MAX_SCENEFILE_LENGTH]; + char sVocalizeData[MAX_VOCALIZE_LENGTH]; + float fPreDelayData; + float fPitchData; +} + +SceneData nSceneData[2048]; +int iScenePlaying[MAXPLAYERS+1], iVocalizeTick[MAXPLAYERS+1], iVocalizeInitiator[MAXPLAYERS+1]; + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + EngineVersion evGame = GetEngineVersion(); + if (evGame == Engine_Left4Dead) + { + bIsL4D = true; + } + else if (evGame != Engine_Left4Dead2) + { + strcopy(error, err_max, "[SP] Plugin Supports L4D And L4D2 Only!"); + return APLRes_Failure; + } + + CreateNative("GetSceneStage", SP_GetSceneStage); + CreateNative("GetSceneStartTimeStamp", SP_GetSceneStartTimeStamp); + CreateNative("GetActorFromScene", SP_GetSceneActor); + CreateNative("GetSceneFromActor", SP_GetActorScene); + CreateNative("GetSceneInitiator", SP_GetSceneInitiator); + CreateNative("GetSceneFile", SP_GetSceneFile); + CreateNative("GetSceneVocalize", SP_GetSceneVocalize); + CreateNative("GetScenePreDelay", SP_GetScenePreDelay); + CreateNative("SetScenePreDelay", SP_SetScenePreDelay); + CreateNative("GetScenePitch", SP_GetScenePitch); + CreateNative("SetScenePitch", SP_SetScenePitch); + CreateNative("CancelScene", SP_CancelScene); + CreateNative("PerformScene", SP_PerformScene); + CreateNative("PerformSceneEx", SP_PerformSceneEx); + + RegPluginLibrary("sceneprocessor"); + return APLRes_Success; +} + +public any SP_GetSceneStage(Handle plugin, int numParams) +{ + if (numParams == 0) + { + return SceneStage_Unknown; + } + + int scene = GetNativeCell(1); + if (scene < 1 || scene > 2048 || !IsValidEntity(scene)) + { + return SceneStage_Unknown; + } + + return nSceneData[scene].ssDataBit; +} + +public any SP_GetSceneStartTimeStamp(Handle plugin, int numParams) +{ + if (numParams == 0) + { + return 0.0; + } + + int scene = GetNativeCell(1); + if (!IsValidScene(scene)) + { + return 0.0; + } + + return nSceneData[scene].fTimeStampData; +} + +public int SP_GetActorScene(Handle plugin, int numParams) +{ + if (numParams == 0) + { + return INVALID_ENT_REFERENCE; + } + + int iActor = GetNativeCell(1); + if (iActor < 1 || iActor > MaxClients || !IsClientInGame(iActor) || GetClientTeam(iActor) != 2 || !IsPlayerAlive(iActor)) + { + return INVALID_ENT_REFERENCE; + } + + return iScenePlaying[iActor]; +} + +public int SP_GetSceneActor(Handle plugin, int numParams) +{ + if (numParams == 0) + { + return 0; + } + + int scene = GetNativeCell(1); + if (!IsValidScene(scene)) + { + return 0; + } + + return nSceneData[scene].iActorData; +} + +public int SP_GetSceneInitiator(Handle plugin, int numParams) +{ + if (numParams == 0) + { + return 0; + } + + int scene = GetNativeCell(1); + if (!IsValidScene(scene)) + { + return 0; + } + + return nSceneData[scene].iInitiatorData; +} + +public int SP_GetSceneFile(Handle plugin, int numParams) +{ + if (numParams != 3) + { + return 0; + } + + int scene = GetNativeCell(1); + if (!IsValidScene(scene)) + { + return 0; + } + + int len = GetNativeCell(3); + + int bytesWritten; + SetNativeString(2, nSceneData[scene].sFileData, len, _, bytesWritten); + return bytesWritten; +} + +public int SP_GetSceneVocalize(Handle plugin, int numParams) +{ + if (numParams != 3) + { + return 0; + } + + int scene = GetNativeCell(1); + if (!IsValidScene(scene)) + { + return 0; + } + + int len = GetNativeCell(3); + + int bytesWritten; + SetNativeString(2, nSceneData[scene].sVocalizeData, len, _, bytesWritten); + return bytesWritten; +} + +public any SP_GetScenePreDelay(Handle plugin, int numParams) +{ + if (numParams == 0) + { + return 0.0; + } + + int scene = GetNativeCell(1); + if (!IsValidScene(scene)) + { + return 0.0; + } + + return nSceneData[scene].fPreDelayData; +} + +public int SP_SetScenePreDelay(Handle plugin, int numParams) +{ + if (numParams != 2) + { + return; + } + + int scene = GetNativeCell(1); + if (!IsValidScene(scene)) + { + return; + } + + float fPreDelay = GetNativeCell(2); + + SetEntPropFloat(scene, Prop_Data, "m_flPreDelay", fPreDelay); + nSceneData[scene].fPreDelayData = fPreDelay; +} + +public any SP_GetScenePitch(Handle plugin, int numParams) +{ + if (numParams == 0) + { + return 0.0; + } + + int scene = GetNativeCell(1); + if (!IsValidScene(scene)) + { + return 0.0; + } + + return nSceneData[scene].fPitchData; +} + +public int SP_SetScenePitch(Handle plugin, int numParams) +{ + if (numParams != 2) + { + return; + } + + int scene = GetNativeCell(1); + if (!IsValidScene(scene)) + { + return; + } + + float fPitch = GetNativeCell(2); + + SetEntPropFloat(scene, Prop_Data, "m_fPitch", fPitch); + nSceneData[scene].fPitchData = fPitch; +} + +public int SP_CancelScene(Handle plugin, int numParams) +{ + if (numParams == 0) + { + return; + } + + int scene = GetNativeCell(1); + if (scene < 1 || scene > 2048 || !IsValidEntity(scene)) + { + return; + } + + SceneStages ssBit = nSceneData[scene].ssDataBit; + if (ssBit == SceneStage_Unknown) + { + return; + } + else if (ssBit == SceneStage_Started || (ssBit == SceneStage_SpawnedPost && nSceneData[scene].bInFakePostSpawn)) + { + AcceptEntityInput(scene, "Cancel"); + } + else if (ssBit != SceneStage_Cancelled && ssBit != SceneStage_Completion && ssBit != SceneStage_Killed) + { + AcceptEntityInput(scene, "Kill"); + } +} + +public int SP_PerformScene(Handle plugin, int numParams) +{ + if (numParams < 2) + { + return; + } + + int client = GetNativeCell(1); + + if (client < 1 || client > MaxClients || !IsClientInGame(client) || GetClientTeam(client) != 2 || !IsPlayerAlive(client)) + { + return; + } + + static char sVocalize[MAX_VOCALIZE_LENGTH], sFile[MAX_SCENEFILE_LENGTH]; + float fPreDelay = DEFAULT_SCENE_PREDELAY, fPitch = DEFAULT_SCENE_PITCH; + int iInitiator = SCENE_INITIATOR_PLUGIN; + + if (GetNativeString(2, sVocalize, MAX_VOCALIZE_LENGTH) != SP_ERROR_NONE) + { + ThrowNativeError(SP_ERROR_NATIVE, "Unknown Vocalize Parameter!"); + return; + } + + if (numParams >= 3) + { + if (GetNativeString(3, sFile, MAX_SCENEFILE_LENGTH) != SP_ERROR_NONE) + { + ThrowNativeError(SP_ERROR_NATIVE, "Unknown File Parameter!"); + return; + } + } + + if (numParams >= 4) + { + fPreDelay = GetNativeCell(4); + } + + if (numParams >= 5) + { + fPitch = GetNativeCell(5); + } + + if (numParams >= 6) + { + iInitiator = GetNativeCell(6); + } + + Scene_Perform(client, sVocalize, sFile, fPreDelay, fPitch, iInitiator); +} + +public int SP_PerformSceneEx(Handle plugin, int numParams) +{ + if (numParams < 2) + { + return; + } + + int client = GetNativeCell(1); + if (client < 1 || client > MaxClients || !IsClientInGame(client) || GetClientTeam(client) != 2 || !IsPlayerAlive(client)) + { + return; + } + + static char sVocalize[MAX_VOCALIZE_LENGTH], sFile[MAX_SCENEFILE_LENGTH]; + float fPreDelay = DEFAULT_SCENE_PREDELAY, fPitch = DEFAULT_SCENE_PITCH; + int iInitiator = SCENE_INITIATOR_PLUGIN; + + if (GetNativeString(2, sVocalize, MAX_VOCALIZE_LENGTH) != SP_ERROR_NONE) + { + ThrowNativeError(SP_ERROR_NATIVE, "Unknown Vocalize Parameter!"); + return; + } + + if (numParams >= 3) + { + if (GetNativeString(3, sFile, MAX_SCENEFILE_LENGTH) != SP_ERROR_NONE) + { + ThrowNativeError(SP_ERROR_NATIVE, "Unknown File Parameter!"); + return; + } + } + + if (numParams >= 4) + { + fPreDelay = GetNativeCell(4); + } + + if (numParams >= 5) + { + fPitch = GetNativeCell(5); + } + + if (numParams >= 6) + { + iInitiator = GetNativeCell(6); + } + + Scene_Perform(client, sVocalize, sFile, fPreDelay, fPitch, iInitiator, true); +} + +public Plugin myinfo = +{ + name = "Scene Processor", + author = "Buster \"Mr. Zero\" Nielsen (Fork by cravenge & Dragokas)", + description = "Provides Forwards and Natives For Scenes' Manipulation.", + version = PLUGIN_VERSION, + url = "https://forums.alliedmods.net/showthread.php?t=241585" +}; + +public void OnPluginStart() +{ + hSceneStageForward = CreateGlobalForward("OnSceneStageChanged", ET_Ignore, Param_Cell, Param_Cell); + hVocalizeCommandForward = CreateGlobalForward("OnVocalizeCommand", ET_Hook, Param_Cell, Param_String, Param_Cell); + + CreateConVar("sceneprocessor_version", PLUGIN_VERSION, "Scene Processor Version", FCVAR_SPONLY|FCVAR_NOTIFY|FCVAR_DONTRECORD); + if (!bIsL4D) + { + ConVar spJailbreakVocalize = CreateConVar("sceneprocessor_jailbreak_vocalize", "1", "Enable/Disable Jailbreak Vocalizations", FCVAR_SPONLY|FCVAR_NOTIFY); + spJailbreakVocalize.AddChangeHook(OnSPCVarChanged); + bJailbreakVocalize = spJailbreakVocalize.BoolValue; + } + + AddCommandListener(OnVocalizeCmd, "vocalize"); + + asScene = new ArrayStack(); + alVocalize = new ArrayList(MAX_VOCALIZE_LENGTH); + + for (int i = 1; i < 2049; i++) + { + if (IsValidEntity(i) && IsValidEdict(i)) + { + SceneData_SetStage(i, SceneStage_Unknown); + } + } + + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i)) + { + ResetClientVocalizeData(i); + } + } +} + +public void OnSPCVarChanged(ConVar cvar, const char[] sOldValue, const char[] sNewValue) +{ + bJailbreakVocalize = cvar.BoolValue; +} + +public Action OnVocalizeCmd(int client, const char[] command, int args) +{ + if (client == 0 || args == 0) + { + return Plugin_Continue; + } + + if (!IsClientInGame(client)) + { + return Plugin_Handled; + } + + static char sVocalize[128]; + GetCmdArg(1, sVocalize, sizeof(sVocalize)); + + if (!bIsL4D && args != 2) + { + if (bJailbreakVocalize) + { + JailbreakVocalize(client, sVocalize); + } + return Plugin_Handled; + } + + int iTick = GetGameTickCount(); + + if (!bSceneHasInitiator[client] || (iVocalizeTick[client] > 0 && iVocalizeTick[client] != iTick)) + { + iVocalizeInitiator[client] = client; + + if (!bIsL4D && args > 1 && StrEqual(sVocalize, "smartlook", false)) + { + static char sTime[32]; + GetCmdArg(2, sTime, sizeof(sTime)); + if (StrEqual(sTime, "auto", false)) + { + iVocalizeInitiator[client] = SCENE_INITIATOR_WORLD; + } + } + } + + strcopy(sVocalizeScene[client], MAX_VOCALIZE_LENGTH, sVocalize); + iVocalizeTick[client] = iTick; + + Action aResult = Plugin_Continue; + + Call_StartForward(hVocalizeCommandForward); + Call_PushCell(client); + Call_PushString(sVocalize); + Call_PushCell(iVocalizeInitiator[client]); + Call_Finish(aResult); + + return (aResult == Plugin_Stop) ? Plugin_Handled : Plugin_Continue; +} + +public void OnPluginEnd() +{ + RemoveCommandListener(OnVocalizeCmd, "vocalize"); +} + +public void OnMapStart() +{ + iSkippedFrames = 0; + fStartTimeStamp = GetGameTime(); +} + +public void OnEntityCreated(int entity, const char[] classname) +{ + if (entity < 1 || entity > 2048) + { + return; + } + + if (StrEqual(classname, "instanced_scripted_scene")) + { + SDKHook(entity, SDKHook_SpawnPost, OnSpawnPost); + SceneData_SetStage(entity, SceneStage_Created); + } +} + +public void OnSpawnPost(int entity) +{ + int iActor = GetEntPropEnt(entity, Prop_Data, "m_hOwner"); + nSceneData[entity].iActorData = iActor; + + static char sFile[MAX_SCENEFILE_LENGTH]; + GetEntPropString(entity, Prop_Data, "m_iszSceneFile", sFile, MAX_SCENEFILE_LENGTH); + + strcopy(nSceneData[entity].sFileData, MAX_SCENEFILE_LENGTH, sFile); + nSceneData[entity].fPitchData = GetEntPropFloat(entity, Prop_Data, "m_fPitch"); + + if (iActor > 0 && iActor <= MaxClients && IsClientInGame(iActor)) + { + if (iVocalizeTick[iActor] == GetGameTickCount()) + { + strcopy(nSceneData[entity].sVocalizeData, MAX_VOCALIZE_LENGTH, sVocalizeScene[iActor]); + + nSceneData[entity].iInitiatorData = iVocalizeInitiator[iActor]; + nSceneData[entity].fPreDelayData = fVocalizePreDelay[iActor]; + nSceneData[entity].fPitchData = fVocalizePitch[iActor]; + } + ResetClientVocalizeData(iActor); + } + + SetEntPropFloat(entity, Prop_Data, "m_fPitch", nSceneData[entity].fPitchData); + SetEntPropFloat(entity, Prop_Data, "m_flPreDelay", nSceneData[entity].fPreDelayData); + + asScene.Push(entity); + bScenesUnprocessed = true; + + HookSingleEntityOutput(entity, "OnStart", OnSceneStart_EntOutput); + HookSingleEntityOutput(entity, "OnCanceled", OnSceneCanceled_EntOutput); + + SceneData_SetStage(entity, SceneStage_Spawned); +} + +public void OnSceneStart_EntOutput(const char[] output, int caller, int activator, float delay) +{ + if (caller < 1 || caller > 2048 || !IsValidEntity(caller)) + { + return; + } + + static char sFile[MAX_SCENEFILE_LENGTH]; + strcopy(sFile, MAX_SCENEFILE_LENGTH, nSceneData[caller].sFileData); + if (!sFile[0]) + { + return; + } + + nSceneData[caller].fTimeStampData = GetEngineTime(); + + if (nSceneData[caller].ssDataBit == SceneStage_Spawned) + { + nSceneData[caller].bInFakePostSpawn = true; + SceneData_SetStage(caller, SceneStage_SpawnedPost); + } + + if (nSceneData[caller].ssDataBit == SceneStage_SpawnedPost) + { + int iActor = nSceneData[caller].iActorData; + if (iActor > 0 && iActor <= MaxClients && IsClientInGame(iActor)) + { + iScenePlaying[iActor] = caller; + } + SceneData_SetStage(caller, SceneStage_Started); + } +} + +public void OnSceneCanceled_EntOutput(const char[] output, int caller, int activator, float delay) +{ + if (caller < 1 || caller > 2048 || !IsValidEntity(caller)) + { + return; + } + + for (int i = 1; i <= MaxClients; i++) + { + if (iScenePlaying[i] == caller) + { + iScenePlaying[i] = INVALID_ENT_REFERENCE; + break; + } + } + + SceneData_SetStage(caller, SceneStage_Cancelled); +} + +public void OnEntityDestroyed(int entity) +{ + if (entity < 1 || entity > 2048 || !IsValidEdict(entity)) + { + return; + } + + static char sEntityClass[64]; + GetEdictClassname(entity, sEntityClass, sizeof(sEntityClass)); + if (!StrEqual(sEntityClass, "instanced_scripted_scene")) + { + return; + } + + SDKUnhook(entity, SDKHook_SpawnPost, OnSpawnPost); + + SceneStages ssBit = nSceneData[entity].ssDataBit; + if (ssBit != SceneStage_Unknown) + { + if (ssBit == SceneStage_Started) + { + SceneData_SetStage(entity, SceneStage_Completion); + } + SceneData_SetStage(entity, SceneStage_Killed); + + for (int i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || iScenePlaying[i] != entity) + { + continue; + } + + iScenePlaying[i] = INVALID_ENT_REFERENCE; + break; + } + } + + SceneData_SetStage(entity, SceneStage_Unknown); +} + +public void OnClientDisconnect(int client) +{ + if (client == 0) + { + return; + } + + iScenePlaying[client] = INVALID_ENT_REFERENCE; +} + +public void OnGameFrame() +{ + iSkippedFrames += 1; + if (iSkippedFrames < 3) + { + return; + } + + iSkippedFrames = 1; + if (bScenesUnprocessed) + { + bScenesUnprocessed = false; + + int dScene; + while (!asScene.Empty) + { + asScene.Pop(dScene); + if (dScene < 1 || dScene > 2048 || !IsValidEntity(dScene)) + { + continue; + } + + if (nSceneData[dScene].ssDataBit != SceneStage_Spawned) + { + continue; + } + + nSceneData[dScene].fPreDelayData = GetEntPropFloat(dScene, Prop_Data, "m_flPreDelay"); + nSceneData[dScene].bInFakePostSpawn = false; + + SceneData_SetStage(dScene, SceneStage_SpawnedPost); + } + } + + if (bUnvocalizedCommands) + { + int iArraySize = alVocalize.Length, + iCurrentTick = GetGameTickCount(); + + static char sVocalize[MAX_VOCALIZE_LENGTH]; + float fPreDelay, fPitch; + int client, dInitiator, dTick; + + for (int i = 0; i < iArraySize; i += 6) + { + dTick = alVocalize.Get(i + 5); + if (iCurrentTick != dTick) + { + continue; + } + + client = alVocalize.Get(i + 0); + alVocalize.GetString(i + 1, sVocalize, MAX_VOCALIZE_LENGTH); + fPreDelay = view_as(alVocalize.Get(i + 2)); + fPitch = view_as(alVocalize.Get(i + 3)); + dInitiator = alVocalize.Get(i + 4); + + Scene_Perform(client, sVocalize, _, fPreDelay, fPitch, dInitiator, true); + + for (int j = 0; j < 6; j++) + { + alVocalize.Erase(i); + iArraySize -= 1; + } + } + if (iArraySize < 1) + { + alVocalize.Clear(); + bUnvocalizedCommands = false; + } + } +} + +public void OnMapEnd() +{ + iSkippedFrames = 0; + + bScenesUnprocessed = false; + bUnvocalizedCommands = false; + + while (!asScene.Empty) + { + PopStack(asScene); + } + alVocalize.Clear(); + + for (int i = 1; i < 2049; i++) + { + if (IsValidEntity(i) && IsValidEdict(i)) + { + SceneData_SetStage(i, SceneStage_Unknown); + } + } + + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i)) + { + iScenePlaying[i] = INVALID_ENT_REFERENCE; + } + } +} + +void ResetClientVocalizeData(int client) +{ + iVocalizeTick[client] = 0; + sVocalizeScene[client] = "\0"; + bSceneHasInitiator[client] = false; + iVocalizeInitiator[client] = SCENE_INITIATOR_WORLD; + fVocalizePreDelay[client] = DEFAULT_SCENE_PREDELAY; + fVocalizePitch[client] = DEFAULT_SCENE_PITCH; +} + +void SceneData_SetStage(int scene, SceneStages stage) +{ + nSceneData[scene].ssDataBit = stage; + + if (stage != SceneStage_Unknown) + { + Call_StartForward(hSceneStageForward); + Call_PushCell(scene); + Call_PushCell(stage); + Call_Finish(); + } + else + { + nSceneData[scene].bInFakePostSpawn = false; + nSceneData[scene].fTimeStampData = 0.0; + nSceneData[scene].iActorData = 0; + nSceneData[scene].iInitiatorData = 0; + strcopy(nSceneData[scene].sFileData, MAX_SCENEFILE_LENGTH, "\0"); + strcopy(nSceneData[scene].sVocalizeData, MAX_VOCALIZE_LENGTH, "\0"); + nSceneData[scene].fPreDelayData = DEFAULT_SCENE_PREDELAY; + nSceneData[scene].fPitchData = DEFAULT_SCENE_PITCH; + } +} + +void Scene_Perform(int client, const char[] sVocalizeParam, const char[] sFileParam = "", float fScenePreDelay = DEFAULT_SCENE_PREDELAY, float fScenePitch = DEFAULT_SCENE_PITCH, int iSceneInitiator = SCENE_INITIATOR_PLUGIN, bool bVocalizeNow = false) +{ + if (sFileParam[0] && FileExists(sFileParam, true)) + { + int iScene = CreateEntityByName("instanced_scripted_scene"); + DispatchKeyValue(iScene, "SceneFile", sFileParam); + + SetEntPropEnt(iScene, Prop_Data, "m_hOwner", client); + nSceneData[iScene].iActorData = client; + SetEntPropFloat(iScene, Prop_Data, "m_flPreDelay", fScenePreDelay); + nSceneData[iScene].fPreDelayData = fScenePreDelay; + SetEntPropFloat(iScene, Prop_Data, "m_fPitch", fScenePitch); + nSceneData[iScene].fPitchData = fScenePitch; + + nSceneData[iScene].iInitiatorData = iSceneInitiator; + strcopy(nSceneData[iScene].sVocalizeData, MAX_VOCALIZE_LENGTH, sVocalizeParam); + + DispatchSpawn(iScene); + ActivateEntity(iScene); + + AcceptEntityInput(iScene, "Start", client, client); + } + else if (sVocalizeParam[0]) + { + if (bVocalizeNow) + { + iVocalizeInitiator[client] = iSceneInitiator; + bSceneHasInitiator[client] = true; + fVocalizePreDelay[client] = fScenePreDelay; + fVocalizePitch[client] = fScenePitch; + + if (bIsL4D) + { + FakeClientCommandEx(client, "vocalize %s", sVocalizeParam); + } + else + { + JailbreakVocalize(client, sVocalizeParam); + } + } + else + { + alVocalize.Push(client); + alVocalize.PushString(sVocalizeParam); + alVocalize.Push(fScenePreDelay); + alVocalize.Push(fScenePitch); + alVocalize.Push(iSceneInitiator); + alVocalize.Push(GetGameTickCount() + 10 - 1); + + bUnvocalizedCommands = true; + } + } +} + +void JailbreakVocalize(int client, const char[] sVocalize) +{ + char sBuffer[2][32]; + FloatToString((GetGameTime() - fStartTimeStamp) + 2.0, sBuffer[0], 32); + ExplodeString(sBuffer[0], ".", sBuffer, 2, 32); + + Format(sBuffer[1], 2, "%s\0", sBuffer[1][0]); + FakeClientCommandEx(client, "vocalize %s #%s%s", sVocalize, sBuffer[0], sBuffer[1]); +}