diff --git a/plugins/Block3Person.smx b/plugins/Block3Person.smx new file mode 100644 index 0000000..220db70 Binary files /dev/null and b/plugins/Block3Person.smx differ diff --git a/plugins/L4D2FFKickProtection.smx b/plugins/L4D2FFKickProtection.smx index 516234b..9f01014 100644 Binary files a/plugins/L4D2FFKickProtection.smx and b/plugins/L4D2FFKickProtection.smx differ diff --git a/plugins/globalbans.smx b/plugins/globalbans.smx index 3674f75..8637ea5 100644 Binary files a/plugins/globalbans.smx and b/plugins/globalbans.smx differ diff --git a/plugins/l4d2_ai_minigun.smx b/plugins/l4d2_ai_minigun.smx index 0b84af1..41cc630 100644 Binary files a/plugins/l4d2_ai_minigun.smx and b/plugins/l4d2_ai_minigun.smx differ diff --git a/plugins/l4d2_baseball.smx b/plugins/l4d2_baseball.smx new file mode 100644 index 0000000..623f516 Binary files /dev/null and b/plugins/l4d2_baseball.smx differ diff --git a/plugins/l4d2_forceset.smx b/plugins/l4d2_forceset.smx new file mode 100644 index 0000000..71c5021 Binary files /dev/null and b/plugins/l4d2_forceset.smx differ diff --git a/plugins/l4d2_witch_force_attack_cmd.smx b/plugins/l4d2_witch_force_attack_cmd.smx new file mode 100644 index 0000000..f683833 Binary files /dev/null and b/plugins/l4d2_witch_force_attack_cmd.smx differ diff --git a/plugins/l4d_anti_rush.smx b/plugins/l4d_anti_rush.smx new file mode 100644 index 0000000..27e0e2a Binary files /dev/null and b/plugins/l4d_anti_rush.smx differ diff --git a/plugins/l4d_sm_respawn.smx b/plugins/l4d_sm_respawn.smx new file mode 100644 index 0000000..9da210f Binary files /dev/null and b/plugins/l4d_sm_respawn.smx differ diff --git a/plugins/sm_player_recorder.smx b/plugins/sm_player_recorder.smx new file mode 100644 index 0000000..ff4be29 Binary files /dev/null and b/plugins/sm_player_recorder.smx differ diff --git a/plugins/spray_control.smx b/plugins/spray_control.smx new file mode 100644 index 0000000..8697727 Binary files /dev/null and b/plugins/spray_control.smx differ diff --git a/pluginsl4d2_extraplayeritems.smx b/pluginsl4d2_extraplayeritems.smx new file mode 100644 index 0000000..e4c32a9 Binary files /dev/null and b/pluginsl4d2_extraplayeritems.smx differ diff --git a/scripting/Block3Person.sp b/scripting/Block3Person.sp new file mode 100644 index 0000000..cdf3a96 --- /dev/null +++ b/scripting/Block3Person.sp @@ -0,0 +1,123 @@ +#define PLUGIN_VERSION "1.1" + +#pragma semicolon 1 +#pragma newdecls required + +#include + +public Plugin myinfo = +{ + name = "Block3person", + author = "Dragokas", + description = "Block 3-rd person view by creating blindness", + version = PLUGIN_VERSION, + url = "https://github.com/dragokas" +} + +bool aBlinded[MAXPLAYERS]; +UserMsg g_FadeUserMsgId; + +static const int BLIND_DURATION = 50; + + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + char sGameName[12]; + GetGameFolderName(sGameName, sizeof(sGameName)); + if( strcmp(sGameName, "left4dead", false) ) + { + strcopy(error, err_max, "Plugin only supports Left 4 Dead 1."); + return APLRes_SilentFailure; + } + g_FadeUserMsgId = GetUserMessageId("Fade"); + if (g_FadeUserMsgId == INVALID_MESSAGE_ID) { + strcopy(error, err_max, "Cannot find Fade user message ID."); + return APLRes_SilentFailure; + } + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("Block3Person.phrases"); + HookEvent("round_start", OnRoundStart, EventHookMode_PostNoCopy); +} + +public Action OnRoundStart(Event event, const char[] name, bool dontBroadcast) +{ + CreateTimer(0.9, Timer_CheckClientViewState, _, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE); + return Plugin_Continue; +} + +public Action Timer_CheckClientViewState(Handle timer) +{ + for (int i = 1; i <= MaxClients; i++) + { + if(IsClientInGame(i) && GetClientTeam(i) == 2 && !IsFakeClient(i) && IsPlayerAlive(i)) + QueryClientConVar(i, "c_thirdpersonshoulder", QueryClientConVarCallback); + } + return Plugin_Continue; +} + +public void QueryClientConVarCallback(QueryCookie cookie, int client, ConVarQueryResult result, const char[] sCvarName, const char[] bCvarValue) +{ + if (StringToInt(bCvarValue) != 0) { + if (!aBlinded[client]) { + aBlinded[client] = true; + BlindClient(client, true); + PrintHintText(client, "%t", "Blind_Warning"); + } + } else { + if (aBlinded[client]) { + aBlinded[client] = false; + PrintHintText(client, "%t", "Unblind_tip"); + BlindClient(client, false); + } + } +} + +void BlindClient(int target, bool bDoBlind = true) +{ + int targets[2]; + targets[0] = target; + + int holdtime; + + int flags; + if (!bDoBlind) + { + flags = (0x0001 | 0x0010); + holdtime = 10000; + } + else + { + flags = (0x0002 | 0x0008); + holdtime = 10; + } + + int color[4] = { 0, 0, 0, 0 }; + color[3] = 255; + + Handle message = StartMessageEx(g_FadeUserMsgId, targets, 1); + if (GetUserMessageType() == UM_Protobuf) + { + Protobuf pb = UserMessageToProtobuf(message); + pb.SetInt("duration", BLIND_DURATION); + pb.SetInt("hold_time", holdtime); + pb.SetInt("flags", flags); + pb.SetColor("clr", color); + } + else + { + BfWrite bf = UserMessageToBfWrite(message); + bf.WriteShort(BLIND_DURATION); + bf.WriteShort(holdtime); + bf.WriteShort(flags); + bf.WriteByte(color[0]); + bf.WriteByte(color[1]); + bf.WriteByte(color[2]); + bf.WriteByte(color[3]); + } + + EndMessage(); +} \ No newline at end of file diff --git a/scripting/BumpMineGiver.sp b/scripting/BumpMineGiver.sp index 3011921..73b70e1 100644 --- a/scripting/BumpMineGiver.sp +++ b/scripting/BumpMineGiver.sp @@ -95,7 +95,7 @@ public Action Command_GiveOthersBMP(int client, int args) { client, target_list, MAXPLAYERS, - COMMAND_FILTER_ALIVE, /* Only allow alive players */ + COMMAND_FILTER_ALIVE, target_name, sizeof(target_name), tn_is_ml)) <= 0) diff --git a/scripting/L4D2Tools.sp b/scripting/L4D2Tools.sp index 4bf560a..9db18b3 100644 --- a/scripting/L4D2Tools.sp +++ b/scripting/L4D2Tools.sp @@ -1,7 +1,7 @@ #pragma semicolon 1 #pragma newdecls required -//#define DEBUG +#define DEBUG 1 #define PLUGIN_VERSION "1.0" @@ -22,7 +22,8 @@ static ConVar hLaserNotice, hFinaleTimer, hFFNotice, hMPGamemode, hPingDropThres static int iFinaleStartTime, botDropMeleeWeapon[MAXPLAYERS+1], iHighPingCount[MAXPLAYERS+1], reserveMode; static bool isHighPingIdle[MAXPLAYERS+1], isL4D1Survivors; static Handle hTakeOverBot, hGoAwayFromKeyboard; -static char lastSound[MAXPLAYERS+1][64]; +static StringMap SteamIDs; +static char lastSound[MAXPLAYERS+1][64], gamemode[32]; static float OUT_OF_BOUNDS[3] = {0.0, -1000.0, 0.0}; @@ -66,7 +67,6 @@ public void OnPluginStart() { hFinaleTimer = CreateConVar("sm_time_finale", "0.0", "Record the time it takes to complete finale. 0 -> OFF, 1 -> Gauntlets Only, 2 -> All finales", FCVAR_NONE, true, 0.0, true, 2.0); hFFNotice = CreateConVar("sm_ff_notice", "0.0", "Notify players if a FF occurs. 0 -> Disabled, 1 -> In chat, 2 -> In Hint text", FCVAR_NONE, true, 0.0, true, 2.0); hPingDropThres = CreateConVar("sm_autoidle_ping_max", "0.0", "The highest ping a player can have until they will automatically go idle.\n0=OFF, Min is 30", FCVAR_NONE, true, 0.0, true, 1000.0); - hMPGamemode = FindConVar("mp_gamemode"); hForceSurvivorSet = FindConVar("l4d_force_survivorset"); hFFNotice.AddChangeHook(CVC_FFNotice); @@ -75,6 +75,12 @@ public void OnPluginStart() { } LasersUsed = new ArrayList(1, 0); + SteamIDs = new StringMap(); + + ConVar hGamemode = FindConVar("mp_gamemode"); + hGamemode.GetString(gamemode, sizeof(gamemode)); + hGamemode.AddChangeHook(Event_GamemodeChange); + Event_GamemodeChange(hGamemode, gamemode, gamemode); HookEvent("player_use", Event_PlayerUse); HookEvent("round_end", Event_RoundEnd); @@ -111,25 +117,48 @@ public void OnPluginStart() { RegAdminCmd("sm_perms", Command_SetServerPermissions, ADMFLAG_KICK, "Sets the server's permissions."); RegAdminCmd("sm_permissions", Command_SetServerPermissions, ADMFLAG_KICK, "Sets the server's permissions."); RegConsoleCmd("sm_pmodels", Command_ListClientModels, "Lists all player's models"); + RegAdminCmd("sm_skipoutro", Command_SkipOutro, ADMFLAG_KICK, "Skips the outro"); CreateTimer(8.0, Timer_CheckPlayerPings, _, TIMER_REPEAT); } +public void Event_GamemodeChange(ConVar cvar, const char[] oldValue, const char[] newValue) { + cvar.GetString(gamemode, sizeof(gamemode)); +} + public void OnClientConnected(int client) { if(!IsFakeClient(client) && reserveMode == 1) { - PrintToChatAll("%N is connecting", client); + PrintChatToAdmins("%N is connecting", client); } } +stock void PrintChatToAdmins(const char[] format, any ...) { + char buffer[254]; + VFormat(buffer, sizeof(buffer), format, 2); + for(int i = 1; i < MaxClients; i++) { + if(IsClientConnected(i) && IsClientInGame(i)) { + AdminId admin = GetUserAdmin(i); + if(admin != INVALID_ADMIN_ID) { + PrintToChat(i, "%s", buffer); + } + } + } + PrintToServer("%s", buffer); +} + + public void OnClientPostAdminCheck(int client) { if(!IsFakeClient(client)) { - if(reserveMode == 2) { - if(GetUserAdmin(client) == INVALID_ADMIN_ID) { + static char auth[32]; + GetClientAuthId(client, AuthId_Steam2, auth, sizeof(auth)); + if(reserveMode == 3 || (reserveMode == 2 && GetUserAdmin(client) == INVALID_ADMIN_ID)) { + int index; + if(!SteamIDs.GetValue(auth, index)) { KickClient(client, "Sorry, server is reserved"); + return; } - }else if(reserveMode == 3) { - KickClient(client, "Sorry, server is reserved"); } + SteamIDs.SetValue(auth, client); } } @@ -139,7 +168,7 @@ public Action Command_SetServerPermissions(int client, int args) { GetCmdArg(1, arg1, sizeof(arg1)); if(StrEqual(arg1, "public", false)) { reserveMode = 0; - }else if(StrContains(arg1, "notice", false) > -1) { + }else if(StrContains(arg1, "noti", false) > -1) { reserveMode = 1; }else if(StrContains(arg1, "admin", false) > -1) { reserveMode = 2; @@ -158,6 +187,7 @@ public Action Command_SetServerPermissions(int client, int args) { public Action Timer_CheckPlayerPings(Handle timer) { + if(StrEqual(gamemode, "hideandseek")) return Plugin_Continue; if(hPingDropThres.IntValue != 0) { for (int i = 1; i <= MaxClients; i++ ) { if(IsClientConnected(i) && IsClientInGame(i) && !IsFakeClient(i) && IsPlayerAlive(i) && GetClientTeam(i) > 1) { @@ -256,7 +286,14 @@ public Action Command_SwapPlayer(int client, int args) { return Plugin_Handled; } -//TODO: Implement idle bot support +public Action Command_SkipOutro(int client, int args) { + for(int i = 1; i <= MaxClients; i++) { + if(IsClientConnected(i) && IsClientInGame(i)) { + ClientCommand(i, "skipouttro"); + } + } + return Plugin_Handled; +} public Action Command_ListClientModels(int client, int args) { char model[64]; for(int i = 1; i <= MaxClients; i++) { @@ -359,7 +396,23 @@ public Action Command_SetClientModel(int client, int args) { GetCmdArg(2, arg2, sizeof(arg2)); GetCmdArg(3, arg3, sizeof(arg3)); - char modelPath[64]; + // If args sm_model -> sm_model + static char modelPath[64]; + if(args == 1) { + int modelID = GetSurvivorId(arg1, false); + if(modelID != -1) { + int team = GetClientTeam(client); + if(team != 2 && team != 4) { + ReplyToCommand(client, "You must be a survivor."); + return Plugin_Handled; + } + GetSurvivorModel(modelID, modelPath, sizeof(modelPath)); + if(isL4D1Survivors && hForceSurvivorSet != null && hForceSurvivorSet.IntValue < 2) modelID = GetSurvivorId(arg2, true); + SetCharacter(client, modelID, modelPath, false); + return Plugin_Handled; + } + } + int modelID = GetSurvivorId(arg2, false); if(modelID == -1) { ReplyToCommand(client, "Invalid survivor type entered. Case-sensitive, full name required."); @@ -386,34 +439,13 @@ public Action Command_SetClientModel(int client, int args) { ReplyToTargetError(client, target_count); return Plugin_Handled; } - int target; for (int i = 0; i < target_count; i++) { - target = target_list[i]; + int target = target_list[i]; bool keepModel = StrEqual(arg3, "keep", false); if(IsClientConnected(target) && IsClientInGame(target) && IsPlayerAlive(target)) { int team = GetClientTeam(target_list[i]); if(team == 2 || team == 4) { - SetEntProp(target, Prop_Send, "m_survivorCharacter", modelID); - SetEntityModel(target, modelPath); - if (IsFakeClient(target)) { - char name[32]; - GetSurvivorName(target, name, sizeof(name)); - SetClientInfo(target, "name", name); - } - UpdatePlayerIdentity(target, view_as(modelID), keepModel); - - DataPack pack = new DataPack(); - pack.WriteCell(GetClientUserId(target)); - for(int slot = 0; slot <= 1; slot++) { - int weapon = GetPlayerWeaponSlot(target, slot); - if( weapon > 0 ) { - SDKHooks_DropWeapon(target, weapon, NULL_VECTOR); - pack.WriteCell(EntIndexToEntRef(weapon)); // Save last held weapon to switch back - - } - } - CreateTimer(0.1, Timer_RequipWeapon, pack); - + SetCharacter(target, modelID, modelPath, keepModel); } } } @@ -421,6 +453,29 @@ public Action Command_SetClientModel(int client, int args) { return Plugin_Handled; } +void SetCharacter(int target, int modelID, char[] modelPath, bool keepModel) { + SetEntProp(target, Prop_Send, "m_survivorCharacter", modelID); + SetEntityModel(target, modelPath); + if (IsFakeClient(target)) { + char name[32]; + GetSurvivorName(target, name, sizeof(name)); + SetClientInfo(target, "name", name); + } + UpdatePlayerIdentity(target, view_as(modelID), keepModel); + + DataPack pack = new DataPack(); + pack.WriteCell(GetClientUserId(target)); + for(int slot = 0; slot <= 1; slot++) { + int weapon = GetPlayerWeaponSlot(target, slot); + if( weapon > 0 ) { + SDKHooks_DropWeapon(target, weapon, NULL_VECTOR); + pack.WriteCell(EntIndexToEntRef(weapon)); // Save last held weapon to switch back + + } + } + CreateTimer(0.1, Timer_RequipWeapon, pack); +} + public Action Cmd_SetSurvivor(int client, int args) { if(args < 1) { ReplyToCommand(client, "Usage: sm_surv "); @@ -492,6 +547,11 @@ public void OnClientDisconnect(int client) { TeleportEntity(botDropMeleeWeapon[client], pos, NULL_VECTOR, NULL_VECTOR); botDropMeleeWeapon[client] = -1; } + if(!IsFakeClient(client)) { + static char auth[32]; + GetClientAuthId(client, AuthId_Steam2, auth, sizeof(auth)); + SteamIDs.Remove(auth); + } } int disabledItem[2048]; //Can also probably prevent kit drop to pick them up @@ -515,11 +575,11 @@ public Action Timer_AllowKitPickup(Handle h, int entity) { } public void OnMapStart() { AddFileToDownloadsTable("sound/custom/meow1.mp3"); - PrecacheSound("sound/custom/meow1.mp3"); + PrecacheSound("custom/meow1.mp3"); AddFileToDownloadsTable("sound/custom/xen_teleport.mp3"); - PrecacheSound("sound/custom/xen_teleport.mp3"); + PrecacheSound("custom/xen_teleport.mp3"); AddFileToDownloadsTable("sound/custom/mariokartmusic.mp3"); - PrecacheSound("sound/custom/mariokartmusic.mp3"); + PrecacheSound("custom/mariokartmusic.mp3"); HookEntityOutput("info_changelevel", "OnStartTouch", EntityOutput_OnStartTouchSaferoom); HookEntityOutput("trigger_changelevel", "OnStartTouch", EntityOutput_OnStartTouchSaferoom); @@ -544,7 +604,7 @@ public void OnSceneStageChanged(int scene, SceneStages stage) { public Action Event_BotPlayerSwap(Event event, const char[] name, bool dontBroadcast) { int bot = GetClientOfUserId(event.GetInt("bot")); if(StrEqual(name, "player_bot_replace")) { - //Bot replaced player + //Bot replaced player, hook any drop events SDKHook(bot, SDKHook_WeaponDrop, Event_OnWeaponDrop); }else{ //Player replaced a bot @@ -562,10 +622,10 @@ public Action Event_BotPlayerSwap(Event event, const char[] name, bool dontBroad } } public Action Event_OnWeaponDrop(int client, int weapon) { - if(!IsValidEntity(weapon)) return Plugin_Continue; - char wpn[32]; + if(!IsValidEntity(weapon) || !IsFakeClient(client)) return Plugin_Continue; + static char wpn[32]; GetEdictClassname(weapon, wpn, sizeof(wpn)); - if(IsFakeClient(client) && StrEqual(wpn, "weapon_melee") && GetEntProp(client, Prop_Send, "m_humanSpectatorUserID") > 0) { + if(StrEqual(wpn, "weapon_melee") && GetEntProp(client, Prop_Send, "m_humanSpectatorUserID") > 0) { #if defined DEBUG 0 PrintToServer("Bot %N dropped melee weapon %s", client, wpn); #endif @@ -607,9 +667,7 @@ public void EntityOutput_OnStartTouchSaferoom(const char[] output, int caller, i EquipPlayerWeapon(client, botDropMeleeWeapon[client]); botDropMeleeWeapon[client] = -1; } - char currentGamemode[16]; - hMPGamemode.GetString(currentGamemode, sizeof(currentGamemode)); - if(StrEqual(currentGamemode, "tankrun", false)) { + if(StrEqual(gamemode, "tankrun", false)) { if(!IsFakeClient(client)) { CreateTimer(1.0, Timer_TPBots, client, TIMER_FLAG_NO_MAPCHANGE); } @@ -726,15 +784,6 @@ stock int GetAnyValidClient() { return -1; } -stock int GetRealClient(int client) { - if(IsFakeClient(client)) { - int realPlayer = GetClientOfUserId(GetEntProp(client, Prop_Send, "m_humanSpectatorUserID")); - return realPlayer > 0 ? realPlayer : -1; - }else{ - return client; - } -} - stock int GetIdleBot(int client) { for(int i = 1; i <= MaxClients; i++ ) { if(IsClientConnected(i) && HasEntProp(i, Prop_Send, "m_humanSpectatorUserID")) { diff --git a/scripting/disable_cameras.sp b/scripting/disable_cameras.sp new file mode 100644 index 0000000..b7bc167 --- /dev/null +++ b/scripting/disable_cameras.sp @@ -0,0 +1,84 @@ +#include +#include +#include + +// Fixed issues: +// - Server crash when kicking a bot who have been an active target of camera (point_viewcontrol_survivor) +// - Multiple visual spectator bugs after team swap in finale + +public void Event_round_start_pre_entity(Event event, const char[] name, bool dontBroadcast) +{ + static char classes[][] = { + "point_viewcontrol", + "point_viewcontrol_survivor", + "point_viewcontrol_multiplayer", + }; + + for (int i = 0; i < sizeof(classes); i++) { + int entity = INVALID_ENT_REFERENCE; + while ((entity = FindEntityByClassname(entity, classes[i])) != INVALID_ENT_REFERENCE) { + // Invoke a "Disable" input on camera entities to free all players + // Doing so on round_start_pre_entity should help to not let map logic kick in too early + AcceptEntityInput(entity, "Disable"); + } + } +} + +public void OnClientDisconnect(int client) +{ + if (!IsClientInGame(client)) { + return; + } + + int viewEntity = GetEntPropEnt(client, Prop_Send, "m_hViewEntity"); + if (!IsValidEdict(viewEntity)) { + return; + } + + char cls[64]; + GetEdictClassname(viewEntity, cls, sizeof(cls)); + if (strncmp(cls, "point_viewcontrol", 17) != 0) { + return; + } + + // Matches CSurvivorCamera, CTriggerCamera + if (strcmp(cls[17], "_survivor") == 0 || cls[17] == '\0') { + // Disable entity to prevent CMoveableCamera::FollowTarget to cause a crash + // m_hTargetEnt EHANDLE is not checked for existence and can be NULL + // CBaseEntity::GetAbsAngles being called on causing a crash + AcceptEntityInput(viewEntity, "Disable"); + } + + // Matches CTriggerCameraMultiplayer + if (strcmp(cls[17], "_multiplayer") == 0) { + AcceptEntityInput(viewEntity, "RemovePlayer", client); + } +} + +public void OnPluginStart() +{ + HookEvent("round_start_pre_entity", Event_round_start_pre_entity, EventHookMode_PostNoCopy); +} + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + switch (GetEngineVersion()) { + case Engine_Left4Dead2, Engine_Left4Dead: + { + return APLRes_Success; + } + } + + strcopy(error, err_max, "Plugin only supports Left 4 Dead and Left 4 Dead 2."); + + return APLRes_SilentFailure; +} + +public Plugin myinfo = +{ + name = "[L4D/2] Unlink Camera Entities", + author = "shqke", + description = "Frees cached players from camera entity", + version = "1.1", + url = "https://github.com/shqke/sp_public" +}; diff --git a/scripting/globalbans.sp b/scripting/globalbans.sp index f40a7d1..22f7a67 100644 --- a/scripting/globalbans.sp +++ b/scripting/globalbans.sp @@ -174,12 +174,14 @@ public void DB_OnConnectCheck(Database db, DBResultSet results, const char[] err LogMessage("%N is banned: %s", client, reason); if(hKickType.IntValue > 0) { if(reasonResult == DBVal_Data) - KickClient(client, "You have been banned: %s", reason); + KickClient(client, "You have been banned:\n%s", reason); else KickClient(client, "You have been banned from this server."); + static char query[128]; + g_db.Format(query, sizeof(query), "UPDATE bans SET times_tried=times_tried+1 WHERE steamid = '%s'", steamid); + g_db.Query(DB_OnBanQuery, query); } else { PrintChatToAdmins("%N was banned from this server for: \"%s\"", client, reason); - return; } static char query[128]; g_db.Format(query, sizeof(query), "UPDATE bans SET times_tried=times_tried+1 WHERE steamid = '%s'", steamid); diff --git a/scripting/include/feedthetrolls/events.inc b/scripting/include/feedthetrolls/events.inc index 842feeb..7c531a1 100644 --- a/scripting/include/feedthetrolls/events.inc +++ b/scripting/include/feedthetrolls/events.inc @@ -138,16 +138,30 @@ public Action L4D2_OnChooseVictim(int attacker, int &curTarget) { L4D2Infected class = view_as(GetEntProp(attacker, Prop_Send, "m_zombieClass")); // Check for any existing victims int existingTarget = GetClientOfUserId(g_iAttackerTarget[attacker]); - if(existingTarget > 0 && IsPlayerAlive(existingTarget)) { - if(gInstaSpecialMagnet[existingTarget] > 0) { - curTarget = existingTarget; - return Plugin_Changed; - } else if(class == L4D2Infected_Tank && (!IsPlayerIncapped(existingTarget) || hMagnetTargetMode.IntValue & 2) && WillMagnetRun(Trolls[tankMagnetID], existingTarget)) { - curTarget = existingTarget; - return Plugin_Changed; - }else if(class != L4D2Infected_Tank && (!IsPlayerIncapped(existingTarget) || hMagnetTargetMode.IntValue & 1) && WillMagnetRun(Trolls[spMagnetID], existingTarget)) { - curTarget = existingTarget; - return Plugin_Changed; + if(existingTarget > 0) { + if(IsPlayerAlive(existingTarget)) { + // Insta-specials ALWAYS target + if(gInstaSpecialMagnet[existingTarget] > 0) { + curTarget = existingTarget; + return Plugin_Changed; + } + // Stop targetting if no longer magnetted: + if(class == L4D2Infected_Tank) { + if(!Trolls[tankMagnetID].IsActive(existingTarget) || !WillMagnetRun(Trolls[tankMagnetID], existingTarget)) return Plugin_Continue; + } else if(class != L4D2Infected_Tank) { + if(!Trolls[spMagnetID].IsActive(existingTarget) || !WillMagnetRun(Trolls[spMagnetID], existingTarget)) return Plugin_Continue; + } + + // Only set target based on incap rules: + if(class == L4D2Infected_Tank && (!IsPlayerIncapped(existingTarget) || hMagnetTargetMode.IntValue & 2) && WillMagnetRun(Trolls[tankMagnetID], existingTarget)) { + curTarget = existingTarget; + return Plugin_Changed; + }else if(class != L4D2Infected_Tank && (!IsPlayerIncapped(existingTarget) || hMagnetTargetMode.IntValue & 1) && WillMagnetRun(Trolls[spMagnetID], existingTarget)) { + curTarget = existingTarget; + return Plugin_Changed; + } + } else { + g_iAttackerTarget[attacker] = 0; } } @@ -157,13 +171,18 @@ public Action L4D2_OnChooseVictim(int attacker, int &curTarget) { int closestClient = -1; for(int i = 1; i <= MaxClients; i++) { if(IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) == 2 && IsPlayerAlive(i)) { - if(class == L4D2Infected_Tank && Trolls[tankMagnetID].IsActive(i) && !WillMagnetRun(Trolls[tankMagnetID], i)) continue; - else if(class != L4D2Infected_Tank && Trolls[spMagnetID].IsActive(i) && !WillMagnetRun(Trolls[spMagnetID], i)) continue; + if(class == L4D2Infected_Tank) { + if(!Trolls[tankMagnetID].IsActive(i) || !WillMagnetRun(Trolls[tankMagnetID], i)) continue; + } else if(class != L4D2Infected_Tank) { + if(!Trolls[spMagnetID].IsActive(i) || !WillMagnetRun(Trolls[spMagnetID], i)) continue; + } if(IsPlayerIncapped(i)) { if((class == L4D2Infected_Tank && hMagnetTargetMode.IntValue & 2 == 0) || (class != L4D2Infected_Tank && hMagnetTargetMode.IntValue & 1 == 0)) continue; } + PrintToConsoleAll("[FTT/Debug] Adding possible magnet victim %N for %N", i, attacker); + GetClientAbsOrigin(i, survPos); float dist = GetVectorDistance(survPos, spPos, true); if(closestClient == -1 || dist < closestDistance) { @@ -176,14 +195,14 @@ public Action L4D2_OnChooseVictim(int attacker, int &curTarget) { if(closestClient > 0) { g_iAttackerTarget[attacker] = GetClientUserId(closestClient); curTarget = closestClient; - PrintToConsoleAll("[FTT] New target for %d: %N", attacker, curTarget); return Plugin_Changed; } return Plugin_Continue; } bool WillMagnetRun(const Troll troll, int i) { - if(troll.activeFlagClients[i] == 0) return true; + if(troll.activeFlagClients[i] == 0) return false; + float cChance = 1.0; //Skip first bit as it is ('Always') if(troll.activeFlagClients[i] & 2) // 2nd: 50% @@ -422,6 +441,7 @@ public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3 if(g_bPendingItemGive[client] && !(buttons & IN_ATTACK2)) { int target = GetClientAimTarget(client, true); if(target > -1) { + ClientCommand(client, "slot5"); buttons |= IN_ATTACK2; RequestFrame(StopItemGive, client); return Plugin_Changed; @@ -509,16 +529,17 @@ public Action Event_TakeDamage(int victim, int& attacker, int& inflictor, float& if(hBotReverseFFDefend.IntValue > 0 && IsFakeClient(attacker) && shootAtTarget[attacker] == 0 && GetClientTeam(attacker) == 2 && GetClientTeam(victim) == 2) return Plugin_Stop; if(attacker != victim && hBotReverseFFDefend.IntValue > 0 && hBotReverseFFDefend.IntValue == 2 || GetUserAdmin(attacker) == INVALID_ADMIN_ID) { if(IsFakeClient(victim) && !IsFakeClient(attacker) && GetClientTeam(attacker) == 2 && GetClientTeam(victim) == 2) { - - if(shootAtTarget[victim] == attacker) { - shootAtTargetHP[attacker] -= RoundFloat(damage); - shootAtTargetLoops[victim] += 4; - return Plugin_Continue; - } else if(shootAtTarget[victim] > 0) { - // Don't switch, wait for timer to stop - return Plugin_Continue; + if(hBotDefendChance.IntValue >= GetRandomFloat()) { + if(shootAtTarget[victim] == attacker) { + shootAtTargetHP[attacker] -= RoundFloat(damage); + shootAtTargetLoops[victim] += 4; + return Plugin_Continue; + } else if(shootAtTarget[victim] > 0) { + // Don't switch, wait for timer to stop + return Plugin_Continue; + } + SetBotTarget(attacker, victim, GetClientRealHealth(attacker) - RoundFloat(damage)); } - SetBotTarget(attacker, victim, GetClientRealHealth(attacker) - RoundFloat(damage)); } } } @@ -531,7 +552,7 @@ public Action SoundHook(int[] clients, int& numClients, char sample[PLATFORM_MAX if(honkID == 0) honkID = GetTrollID("Honk / Meow"); if(vocalGagID == 0) vocalGagID = GetTrollID("Vocalize Gag"); - if(lastButtonUser > -1 && StrEqual(sample, "npc/mega_mob/mega_mob_incoming.wav")) { + if(lastButtonUser > -1 && !IsFakeClient(lastButtonUser) && StrEqual(sample, "npc/mega_mob/mega_mob_incoming.wav")) { PrintToConsoleAll("CRESCENDO STARTED BY %N", lastButtonUser); #if defined DEBUG PrintToChatAll("CRESCENDO STARTED BY %N", lastButtonUser); @@ -570,10 +591,9 @@ public Action Event_WitchVictimSet(Event event, const char[] name, bool dontBroa static int witchTrollID; if(witchTrollID == 0) witchTrollID = GetTrollID("Witch Magnet"); - int witch = event.GetInt("witchid"); + int witch = event.GetInt("witchid"), closestClient = -1; float closestDistance, survPos[3], witchPos[3]; GetEntPropVector(witch, Prop_Send, "m_vecOrigin", witchPos); - int closestClient = -1; for(int i = 1; i <= MaxClients; i++) { if(IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) == 2 && IsPlayerAlive(i)) { @@ -681,4 +701,17 @@ int FindClosestVisibleClient(int source) { public bool TraceEntityFilterPlayer(int entity, int mask, any data) { return data != entity && entity <= MaxClients && GetClientTeam(entity) == 2 && IsPlayerAlive(entity); +} + +float iLastAntiRushEvent[MAXPLAYERS+1]; +public Action OnAntiRush(int client, int &type, float distance) { + PrintToConsoleAll("[FTT] Antirush: %N (dist=%d) (GameTime=%f)", client, distance, GetGameTime()); + if(type == 3 && IsPlayerAlive(client) && !IsPlayerIncapped(client)) { + if(GetGameTime() - iLastAntiRushEvent[client] > 30.0) { + int special = GetRandomInt(0,6); + iLastAntiRushEvent[client] = GetGameTime(); + SpawnSpecialNear(client, special); + PrintToConsoleAll("[FTT] Spawning anti-rush special on %N (dist=%f) (special=%d)", client, distance, special); + } + } } \ No newline at end of file diff --git a/scripting/include/ftt.inc b/scripting/include/ftt.inc index 3d50246..60f7fe8 100644 --- a/scripting/include/ftt.inc +++ b/scripting/include/ftt.inc @@ -25,6 +25,7 @@ ConVar hMagnetTargetMode; ConVar hBadThrowHitSelf; ConVar hBotReverseFFDefend; ConVar hSbFriendlyFire; +ConVar hBotDefendChance; bool g_bPendingItemGive[MAXPLAYERS+1]; diff --git a/scripting/include/l4d2_detections.inc b/scripting/include/l4d2_detections.inc new file mode 100644 index 0000000..b3b7593 --- /dev/null +++ b/scripting/include/l4d2_detections.inc @@ -0,0 +1,10 @@ +// Called when a player takes two kits +forward void OnDoubleKit(int client); +// Called when a bile is thrown when no zombies around +forward void OnNoHordeBileWaste(int client, int commons); + +// Called when a door is closed within range of another player. +// Victim will be the closest victim to the door, may be incorrect. +forward void OnDoorCloseInFace(int client, int victim); +// Called on saferoom doors, with a count. Resets after 20s of no door opening. +forward void OnDoorCloseInFaceSaferoom(int client, int victim, int count); \ No newline at end of file diff --git a/scripting/include/l4d2_usermsg.inc b/scripting/include/l4d2_usermsg.inc index dc21cc0..1832322 100644 --- a/scripting/include/l4d2_usermsg.inc +++ b/scripting/include/l4d2_usermsg.inc @@ -1,7 +1,7 @@ enum PZDamage_Type { - PZDamage_Killed, + PZDamage_Killed, //0 PZDamage_Incapped, - PZDamage_Killed2, + PZDamage_Killed2, //2 PZDamage_Revived, PZDamage_Progress, PZDamage_DestroyedCan, diff --git a/scripting/include/l4d2_weapon_stocks.inc b/scripting/include/l4d2_weapon_stocks.inc index 67c8085..f47be67 100644 --- a/scripting/include/l4d2_weapon_stocks.inc +++ b/scripting/include/l4d2_weapon_stocks.inc @@ -3,12 +3,13 @@ #endif #define l4d2_weapons_inc_ -#define GETWEAPONNAME(%0) (IsValidWeaponId(WeaponId:(%0)) ? (WeaponNames[_:(%0)]) : "") -#define GETLONGWEAPONNAME(%0) (IsValidWeaponId(WeaponId:(%0)) ? (LongWeaponNames[_:(%0)]) : "") -#define GETMELEEWEAPONNAME(%0) (IsValidWeaponId(MeleeWeaponId:(%0)) ? (MeleeWeaponNames[_:(%0)]) : "") -#define GETLONGMELEEWEAPONNAME(%0) (IsValidWeaponId(MeleeWeaponId:(%0)) ? (LongMeleeWeaponNames[_:(%0)]) : "") -#define GETWEAPONMODEL(%0) (HasValidWeaponModel(WeaponId:(%0)) ? (WeaponModels[_:(%0)]) : "") -#define GETMELEEWEAPONMODEL(%0) (HasValidWeaponModel(MeleeWeaponId:(%0)) ? (MeleeWeaponModels[_:(%0)]) : "") +#define GETWEAPONNAME(%0) (IsValidWeaponId(WeaponId (%0)) ? (WeaponNames[(%0)]) : "") +#define GETLONGWEAPONNAME(%0) (IsValidWeaponId(WeaponId (%0)) ? (LongWeaponNames[(%0)]) : "") +#define GETMELEEWEAPONNAME(%0) (IsValidWeaponId(MeleeWeaponId (%0)) ? (MeleeWeaponNames[(%0)]) : "") +#define GETLONGMELEEWEAPONNAME(%0) (IsValidWeaponId(MeleeWeaponId (%0)) ? (LongMeleeWeaponNames[(%0)]) : "") +#define GETWEAPONMODEL(%0) (HasValidWeaponModel(WeaponId (%0)) ? (WeaponModels[(%0)]) : "") +#define GETMELEEWEAPONMODEL(%0) (HasValidWeaponModel(MeleeWeaponId (%0)) ? (MeleeWeaponModels[(%0)]) : "") + // Weapon ID enumerations. // These values are *NOT* arbitrary! @@ -92,8 +93,7 @@ enum MeleeWeaponId }; // Weapon names for each of the weapons, used in identification. -const char WeaponNames[sizeof(WeaponId)][] = -{ +char WeaponNames[56][] = { "weapon_none", "weapon_pistol", "weapon_smg", // 0 "weapon_pumpshotgun", "weapon_autoshotgun", "weapon_rifle", // 3 "weapon_hunting_rifle", "weapon_smg_silenced", "weapon_shotgun_chrome", // 6 @@ -116,8 +116,7 @@ const char WeaponNames[sizeof(WeaponId)][] = }; // Long weapon names -const char LongWeaponNames[WeaponId][] = -{ +char LongWeaponNames[56][] = { "None", "Pistol", "Uzi", // 0 "Pump", "Autoshotgun", "M-16", // 3 "Hunting Rifle", "Mac", "Chrome", // 6 @@ -160,7 +159,7 @@ char MeleeWeaponNames[MeleeWeaponId][] = }; // Long melee weapon names -const char LongMeleeWeaponNames[MeleeWeaponId][] = +char LongMeleeWeaponNames[MeleeWeaponId][] = { "None", "Knife", @@ -181,8 +180,7 @@ const char LongMeleeWeaponNames[MeleeWeaponId][] = // World weapon models for each of the weapons. Useful for making new weapon spawns. // Some models are left blank because no single model can be given, the model is known or none exist. -const char WeaponModels[WeaponId][] = -{ +char WeaponModels[56][] = { "", "/w_models/weapons/w_pistol_B.mdl", "/w_models/weapons/w_smg_uzi.mdl", @@ -241,7 +239,7 @@ const char WeaponModels[WeaponId][] = "" }; -const char MeleeWeaponModels[MeleeWeaponId][] = +char MeleeWeaponModels[15][] = { "", "/w_models/weapons/w_knife_t.mdl", @@ -260,8 +258,7 @@ const char MeleeWeaponModels[MeleeWeaponId][] = "/weapons/melee/w_tonfa.mdl" }; -const int WeaponSlots[WeaponId] = -{ +int WeaponSlots[56] = { -1, // WEPID_NONE 1, // WEPID_PISTOL 0, // WEPID_SMG @@ -335,17 +332,16 @@ static Handle hMeleeWeaponModelsTrie = INVALID_HANDLE; stock void InitWeaponNamesTrie() { hWeaponNamesTrie = CreateTrie(); - for(new i = 0; i < _:WeaponId; i++) - { - SetTrieValue(hWeaponNamesTrie, WeaponNames[WeaponId:i], i); + for(int i = 0; i < view_as(WeaponId); i++) { + SetTrieValue(hWeaponNamesTrie, WeaponNames[i], i); } hMeleeWeaponNamesTrie = CreateTrie(); hMeleeWeaponModelsTrie = CreateTrie(); - for (new i = 0; i < _:MeleeWeaponId; ++i) + for (int i = 0; i < view_as(MeleeWeaponId); ++i) { - SetTrieValue(hMeleeWeaponNamesTrie, MeleeWeaponNames[MeleeWeaponId:i], i); - SetTrieString(hMeleeWeaponModelsTrie, MeleeWeaponModels[MeleeWeaponId:i], MeleeWeaponNames[MeleeWeaponId:i]); + SetTrieValue(hMeleeWeaponNamesTrie, MeleeWeaponNames[i], i); + SetTrieString(hMeleeWeaponModelsTrie, MeleeWeaponModels[i], MeleeWeaponNames[i]); } } @@ -356,13 +352,12 @@ stock void InitWeaponNamesTrie() { * @param wepid WeaponId to check for validity * @return True if wepid is valid, false otherwise. */ -stock bool IsValidWeaponId({WeaponId, MeleeWeaponId} wepid, tagType = tagof(wepid)) -{ - if (tagType == tagof(MeleeWeaponId)) - { - return MeleeWeaponId:wepid >= WEPID_MELEE_NONE && MeleeWeaponId:wepid < MeleeWeaponId; - } - return wepid >= WEPID_NONE && wepid < WeaponId; +stock bool IsValidWeaponId(WeaponId wepid){ + return wepid != WEPID_NONE; +} + +stock bool IsValidMeleeWeaponId(MeleeWeaponId wepid) { + return MeleeWeaponId:wepid >= WEPID_MELEE_NONE && MeleeWeaponId:wepid < MeleeWeaponId; } /** @@ -371,8 +366,7 @@ stock bool IsValidWeaponId({WeaponId, MeleeWeaponId} wepid, tagType = tagof(wepi * @param wepid WeaponId to get the slot for. * @return Slot number (0-4) or -1 for invalid WeaponId or no slot */ -stock int GetSlotFromWeaponId(WeaponId wepid) -{ +stock int GetSlotFromWeaponId(WeaponId wepid) { return IsValidWeaponId(wepid) ? WeaponSlots[wepid] : -1; } @@ -383,13 +377,14 @@ stock int GetSlotFromWeaponId(WeaponId wepid) * @param wepid WeaponId to check for a known weapon model for. * @return True if a valid weapon model exists for wepid, false otherwise. */ -stock bool:HasValidWeaponModel({WeaponId, MeleeWeaponId}:wepid, tagType = tagof(wepid)) -{ - if (tagType == tagof(MeleeWeaponId)) - { +stock bool HasValidWeaponModel(WeaponId wepid) { + return IsValidWeaponId(wepid) && WeaponModels[wepid][0] != '\0'; +} + +stock bool HasValidMeleeWeaponModel(MeleeWeaponId wepid) { + if (tagType == tagof(MeleeWeaponId)) { return IsValidWeaponId(MeleeWeaponId:wepid) && MeleeWeaponModels[MeleeWeaponId:wepid][0] != '\0'; } - return IsValidWeaponId(wepid) && WeaponModels[wepid][0] != '\0'; } /** @@ -398,16 +393,13 @@ stock bool:HasValidWeaponModel({WeaponId, MeleeWeaponId}:wepid, tagType = tagof( * @param weaponName Weapon name string to look up Id from * @return The corresponding WeaponId if found, else WEPID_NONE */ -stock WeaponId WeaponNameToId(const char weaponName[]) -{ - new WeaponID:id; - if(hWeaponNamesTrie == INVALID_HANDLE) - { +stock WeaponId WeaponNameToId(const char[] weaponName) { + int id; + if(hWeaponNamesTrie == INVALID_HANDLE) { InitWeaponNamesTrie(); } - if(GetTrieValue(hWeaponNamesTrie, weaponName, id)) - { - return WeaponId:id; + if(GetTrieValue(hWeaponNamesTrie, weaponName, id)) { + return view_as(id); } return WEPID_NONE; } @@ -420,16 +412,12 @@ stock WeaponId WeaponNameToId(const char weaponName[]) * @param length Max length which can be written to the buffer. * @return Number of bytes written to buffer, or 0 for invalid weaponId. */ -stock GetWeaponName({WeaponId, MeleeWeaponId}:wepid, String:nameBuffer[], length, tagType = tagof(wepid)) -{ - if (tagType == tagof(MeleeWeaponId)) - { - strcopy(nameBuffer, length, GETMELEEWEAPONNAME(wepid)); - } - else - { - strcopy(nameBuffer, length, GETWEAPONNAME(wepid)); - } +stock int GetWeaponName(WeaponId wepid, char[] nameBuffer, int length) { + return IsValidWeaponId(wepid) ? strcopy(nameBuffer, length, WeaponNames[wepid]) : 0; +} + +stock int GetMeleeWeaponName(WeaponId wepid, char[] nameBuffer, int length) { + return IsValidWeaponId(wepid) ? strcopy(nameBuffer, length, MeleeWeaponNames[wepid]) : 0; } /** @@ -440,16 +428,12 @@ stock GetWeaponName({WeaponId, MeleeWeaponId}:wepid, String:nameBuffer[], length * @param length Max length which can be written to the buffer. * @return Number of bytes written to buffer, or 0 for invalid weaponId. */ -stock GetLongWeaponName({WeaponId, MeleeWeaponId}:wepid, String:nameBuffer[], length, tagType = tagof(wepid)) -{ - if (tagType == tagof(MeleeWeaponId)) - { - strcopy(nameBuffer, length, GETLONGMELEEWEAPONNAME(wepid)); - } - else - { - strcopy(nameBuffer, length, GETLONGWEAPONNAME(wepid)); - } +stock int GetLongWeaponName(WeaponId wepid, char[] nameBuffer, int length) { + strcopy(nameBuffer, length, GETLONGMELEEWEAPONNAME(wepid)); +} + +stock int GetLongMeleeWeaponName(WeaponId wepid, char[] nameBuffer, int length) { + strcopy(nameBuffer, length, GETLONGWEAPONNAME(wepid)); } /** @@ -461,16 +445,12 @@ stock GetLongWeaponName({WeaponId, MeleeWeaponId}:wepid, String:nameBuffer[], le * @param length Max length which can be written to the buffer. * @return Number of bytes written to buffer, or 0 for invalid weaponid or no weapon model available. */ -stock GetWeaponModel({WeaponId, MeleeWeaponId}:wepid, String:modelBuffer[], length, tagType = tagof(wepid)) -{ - if (tagType == tagof(MeleeWeaponId)) - { - strcopy(modelBuffer, length, GETMELEEWEAPONMODEL(wepid)); - } - else - { - strcopy(modelBuffer, length, GETWEAPONMODEL(wepid)); - } +stock int GetWeaponModel(MeleeWeaponId wepid, char[] modelBuffer, int length) { + strcopy(modelBuffer, length, GETWEAPONMODEL(wepid)); +} + +stock int GetMeleeWeaponModel(MeleeWeaponId wepid, char[] modelBuffer, int length) { + strcopy(modelBuffer, length, GETMELEEWEAPONMODEL(wepid)); } /** @@ -480,26 +460,21 @@ stock GetWeaponModel({WeaponId, MeleeWeaponId}:wepid, String:modelBuffer[], leng * @param entity Index of entity to identify * @return WeaponID for the entity if it is a weapon, WEPID_NONE otherwise */ -stock WeaponId:IdentifyWeapon(entity) -{ - if(!entity || !IsValidEntity(entity) || !IsValidEdict(entity)) - { +stock WeaponId IdentifyWeapon(int entity) { + if(!entity || !IsValidEntity(entity) || !IsValidEdict(entity)) { return WEPID_NONE; } - decl String:class[64]; - if(!GetEdictClassname(entity, class, sizeof(class))) - { + static char class[64]; + if(!GetEdictClassname(entity, class, sizeof(class))) { return WEPID_NONE; } - if(StrEqual(class, "weapon_spawn")) - { - return WeaponId:GetEntProp(entity,Prop_Send,"m_weaponID"); + if(StrEqual(class, "weapon_spawn")) { + return view_as(GetEntProp(entity ,Prop_Send, "m_weaponID")); } - new len = strlen(class); - if(len-6 > 0 && StrEqual(class[len-6], "_spawn")) - { + int len = strlen(class); + if(len-6 > 0 && StrEqual(class[len-6], "_spawn")) { class[len-6]='\0'; return WeaponNameToId(class); } @@ -508,41 +483,30 @@ stock WeaponId:IdentifyWeapon(entity) } // Helper function used for getting an entity's internal melee name -stock bool:GetMeleeWeaponNameFromEntity(entity, String:buffer[], length) { - decl String:classname[64]; - if (! GetEdictClassname(entity, classname, sizeof(classname))) - { +stock bool GetMeleeWeaponNameFromEntity(int entity, char[] buffer, int length) { + static char classname[64]; + if (!GetEdictClassname(entity, classname, sizeof(classname))) { return false; } - if (StrEqual(classname, "weapon_melee_spawn")) - { - if (hMeleeWeaponModelsTrie == INVALID_HANDLE) - { + if (StrEqual(classname, "weapon_melee_spawn")) { + if (hMeleeWeaponModelsTrie == INVALID_HANDLE) { InitWeaponNamesTrie(); } - decl String:sModelName[128]; + static char sModelName[128]; GetEntPropString(entity, Prop_Data, "m_ModelName", sModelName, sizeof(sModelName)); // Strip models directory - if (strncmp(sModelName, "models/", 7, false) == 0) - { + if (strncmp(sModelName, "models/", 7, false) == 0) { strcopy(sModelName, sizeof(sModelName), sModelName[6]); } - if (GetTrieString(hMeleeWeaponModelsTrie, sModelName, buffer, length)) - { - return true; - } - return false; - } - else if (StrEqual(classname, "weapon_melee")) - { + return GetTrieString(hMeleeWeaponModelsTrie, sModelName, buffer, length) + } else if (StrEqual(classname, "weapon_melee")) { GetEntPropString(entity, Prop_Data, "m_strMapSetScriptName", buffer, length); return true; } - return false; } @@ -553,28 +517,23 @@ stock bool:GetMeleeWeaponNameFromEntity(entity, String:buffer[], length) { * @param entity Index of entity to identify * @return MeleeWeaponId for the entity if it is a weapon, WEPID_MELEE_NONE otherwise */ -stock MeleeWeaponId:IdentifyMeleeWeapon(entity) -{ - if (IdentifyWeapon(entity) != WEPID_MELEE) - { +stock MeleeWeaponId IdentifyMeleeWeapon(int entity) { + if (IdentifyWeapon(entity) != WEPID_MELEE) { return WEPID_MELEE_NONE; } - decl String:sName[128]; - if (! GetMeleeWeaponNameFromEntity(entity, sName, sizeof(sName))) - { + static char sName[128]; + if (! GetMeleeWeaponNameFromEntity(entity, sName, sizeof(sName))) { return WEPID_MELEE_NONE; } - if (hMeleeWeaponNamesTrie == INVALID_HANDLE) - { + if (hMeleeWeaponNamesTrie == INVALID_HANDLE) { InitWeaponNamesTrie(); } - new id; - if(GetTrieValue(hMeleeWeaponNamesTrie, sName, id)) - { - return MeleeWeaponId:id; + int id; + if(GetTrieValue(hMeleeWeaponNamesTrie, sName, id)) { + return id; } return WEPID_MELEE_NONE; } @@ -590,7 +549,7 @@ stock MeleeWeaponId:IdentifyMeleeWeapon(entity) * @param model World model to use for the weapon spawn * @return entity of the new weapon spawn, or -1 on errors. */ -stock ConvertWeaponSpawn(entity, WeaponId:wepid, count=5, const String:model[] = "") +stock int ConvertWeaponSpawn(int entity, WeaponId wepid, int count = 5, const char model[] = "") { if(!IsValidEntity(entity)) return -1; if(!IsValidWeaponId(wepid)) return -1; @@ -609,12 +568,9 @@ stock ConvertWeaponSpawn(entity, WeaponId:wepid, count=5, const String:model[] = SetEntProp(entity, Prop_Send, "m_weaponID", wepid); decl String:buf[64]; - if(model[0] == '\0') - { + if(model[0] == '\0') { SetEntityModel(entity, model); - } - else - { + } else { GetWeaponModel(wepid, buf, sizeof(buf)); SetEntityModel(entity, buf); } diff --git a/scripting/include/l4d_anti_rush.inc b/scripting/include/l4d_anti_rush.inc new file mode 100644 index 0000000..81eea9e --- /dev/null +++ b/scripting/include/l4d_anti_rush.inc @@ -0,0 +1 @@ +forward Action OnAntiRush(int client, int &type, float distance); \ No newline at end of file diff --git a/scripting/include/left4dhooks.inc b/scripting/include/left4dhooks.inc index 868c95f..e681500 100644 --- a/scripting/include/left4dhooks.inc +++ b/scripting/include/left4dhooks.inc @@ -18,7 +18,7 @@ * Copyright (C) 2017 "Accelerator74" * * Left 4 DHooks Direct SourceMod plugin - * Copyright (C) 2021 "SilverShot" / "Silvers" + * Copyright (C) 2022 "SilverShot" / "Silvers" * * ============================================================================= * @@ -59,8 +59,8 @@ // Natives: -// L4D1 = 24 [left4downtown] + 47 [l4d_direct] + 15 [l4d2addresses] + 44 [silvers - mine!] + 4 [anim] = 126 -// L4D2 = 53 [left4downtown] + 61 [l4d_direct] + 26 [l4d2addresses] + 79 [silvers - mine!] + 4 [anim] = 212 +// L4D1 = 25 [left4downtown] + 47 [l4d_direct] + 15 [l4d2addresses] + 46 [silvers - mine!] + 4 [anim] = 128 +// L4D2 = 54 [left4downtown] + 61 [l4d_direct] + 26 [l4d2addresses] + 81 [silvers - mine!] + 4 [anim] = 215 // Forwards: // L4D1 = 61; @@ -196,6 +196,10 @@ public void __pl_l4dh_SetNTVOptional() MarkNativeAsOptional("L4D_GetMobSpawnTimerDuration"); MarkNativeAsOptional("L4D2_ChangeFinaleStage"); MarkNativeAsOptional("L4D2_SpawnWitchBride"); + MarkNativeAsOptional("L4D_LobbyUnreserve"); + MarkNativeAsOptional("L4D_LobbyIsReserved"); + MarkNativeAsOptional("L4D_GetLobbyReservation"); + MarkNativeAsOptional("L4D_SetLobbyReservation"); // l4d2weapons.inc MarkNativeAsOptional("L4D_GetWeaponID"); @@ -497,7 +501,7 @@ native int AnimGetFromActivity(char[] activity); * @remarks Only used for bot special spawns (not players) * @remarks zombieClass: 1=Smoker, 2=Boomer, 3=Hunter, 4=Spitter, 5=Jockey, 6=Charger * - * @param zombieClass Zombie class that will be spawned. + * @param zombieClass Zombie class that will be spawned * @param vecPos Vector coordinate where special will be spawned * @param vecAng QAngle where spcial will be facing * @@ -511,8 +515,8 @@ forward Action L4D_OnSpawnSpecial(int &zombieClass, const float vecPos[3], const * @remarks Only used for bot special spawns (not players) * @remarks zombieClass: 1=Smoker, 2=Boomer, 3=Hunter, 4=Spitter, 5=Jockey, 6=Charger * - * @param client The client index who spawned - * @param zombieClass Zombie class that will be spawned. + * @param client The client index who spawned. Can be 0 if spawning failed + * @param zombieClass Zombie class that will be spawned * @param vecPos Vector coordinate where special will be spawned * @param vecAng QAngle where spcial will be facing * @@ -1351,6 +1355,15 @@ forward Action L4D_OnVomitedUpon(int victim, int &attacker, bool &boomerExplosio */ forward Action L4D2_OnHitByVomitJar(int victim, int &attacker); +/** + * @brief Called when the server changes hibernation status + * + * @param hibernating true when hibernating, false when waking up + * + * @noreturn + */ +forward void L4D_OnServerHibernationUpdate(bool hibernating); + /** * @brief Called when the client's material system is expecting instructions from the server in regards to addons * @remarks Doesn't fire if l4d2_addons_eclipse is -1 or 0 @@ -2325,11 +2338,39 @@ native int L4D2_SpawnWitchBride(const float vecPos[3], const float vecAng[3]); /** * @brief Removes lobby reservation from a server - * @remarks Sets the reservation cookie to 0, - * it is safe to call this even if it's unreserved. + * @remarks Sets the reservation cookie to 0, it is safe to call this even if it's unreserved. + * + * @noreturn */ native void L4D_LobbyUnreserve(); +/** + * @brief Checks if the server is currently reserved for a lobby + * @remarks Server is automatically unreserved if it hibernates or if all players leave. + * + * @return true if reserved, false if not reserved + */ +native bool L4D_LobbyIsReserved(); + +/** + * @brief Returns the lobby reservation ID + * + * @reservation String to store the reservation ID to + * @maxlength Maximum length of the string to store to + * + * @noreturn + */ +native void L4D_GetLobbyReservation(char [] reservation, int maxlength); + +/** + * @brief Sets the lobby reservation ID + * + * @reservation The reservation ID to set + * + * @noreturn + */ +native void L4D_SetLobbyReservation(char reservation[20]); + /** * @brief Get the current campaign scores stored in the Director * @remarks The campaign scores are updated after L4D_OnSetCampaignScores @@ -2345,17 +2386,6 @@ native void L4D_LobbyUnreserve(); #pragma deprecated Use GetTeamScore and OnClearTeamScores instead native int L4D_GetCampaignScores(int &scoreA, int &scoreB); -/** - * @brief Checks if the server is currently reserved for a lobby - * @remarks Server is automatically unreserved if it hibernates or - * if all players leave. - * - * @deprecated This will always return false on L4D2 or on Linux. - * - * @return true if reserved, false if not reserved - */ -#pragma deprecated This will always return false on L4D2 or on Linux. -native bool L4D_LobbyIsReserved(); /** * @brief Get the time remaining before the next director horde. * @remarks This timer is used for scripted event hordes and natural timed hordes @@ -2704,6 +2734,8 @@ enum L4D2FloatWeaponAttributes L4D2FWA_PelletScatterYaw, L4D2FWA_VerticalPunch, L4D2FWA_HorizontalPunch, // Requires "z_gun_horiz_punch" cvar changed to "1". + L4D2FWA_GainRange, + L4D2FWA_ReloadDuration, MAX_SIZE_L4D2FloatWeaponAttributes }; diff --git a/scripting/include/left4dhooks_anim.inc b/scripting/include/left4dhooks_anim.inc index feea0c4..45505b1 100644 --- a/scripting/include/left4dhooks_anim.inc +++ b/scripting/include/left4dhooks_anim.inc @@ -1,6 +1,6 @@ /* * Left 4 DHooks Direct -* Copyright (C) 2021 Silvers +* Copyright (C) 2022 Silvers * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/scripting/include/left4dhooks_lux_library.inc b/scripting/include/left4dhooks_lux_library.inc index f6f8d91..f71fdc9 100644 --- a/scripting/include/left4dhooks_lux_library.inc +++ b/scripting/include/left4dhooks_lux_library.inc @@ -1,5 +1,5 @@ /** -* Copyright (C) 2021 LuxLuma +* Copyright (C) 2022 LuxLuma * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/scripting/include/left4dhooks_silver.inc b/scripting/include/left4dhooks_silver.inc index 071790c..463cfb7 100644 --- a/scripting/include/left4dhooks_silver.inc +++ b/scripting/include/left4dhooks_silver.inc @@ -1,6 +1,6 @@ /* * Left 4 DHooks Direct - Stock Functions -* Copyright (C) 2021 Silvers +* Copyright (C) 2022 Silvers * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -827,7 +827,10 @@ stock int GetAnyRandomClient() } if( aClients.Length > 0 ) + { + SetRandomSeed(GetGameTickCount()); client = aClients.Get(GetRandomInt(0, aClients.Length - 1)); + } delete aClients; @@ -866,7 +869,7 @@ stock int Local_GetRandomClient(int team, int alive = -1, int bots = -1) for( int i = 1; i <= MaxClients; i++ ) { - if( IsClientInGame(i) && GetClientTeam(i) == team && (alive == -1 || IsPlayerAlive(i) == view_as(alive)) && (bots == -1 || IsFakeClient(i) == view_as(alive)) ) + if( IsClientInGame(i) && GetClientTeam(i) == team && (alive == -1 || IsPlayerAlive(i) == view_as(alive)) && (bots == -1 || IsFakeClient(i) == view_as(bots)) ) { aClients.Push(i); } @@ -875,7 +878,10 @@ stock int Local_GetRandomClient(int team, int alive = -1, int bots = -1) int client; if( aClients.Length > 0 ) + { + SetRandomSeed(GetGameTickCount()); client = aClients.Get(GetRandomInt(0, aClients.Length - 1)); + } delete aClients; diff --git a/scripting/include/left4dhooks_stocks.inc b/scripting/include/left4dhooks_stocks.inc index 3441b8b..e998946 100644 --- a/scripting/include/left4dhooks_stocks.inc +++ b/scripting/include/left4dhooks_stocks.inc @@ -1,7 +1,7 @@ /** * ============================================================================= * Left 4 Dead Stocks Library (C)2011-2012 Buster "Mr. Zero" Nielsen - * Syntax Update and merge into "Left 4 DHooks Direct" (C) 2021 "SilverShot" + * Syntax Update and merge into "Left 4 DHooks Direct" (C) 2022 "SilverShot" * ============================================================================= * * This program is free software; you can redistribute it and/or modify it diff --git a/scripting/include/smrcon.inc b/scripting/include/smrcon.inc new file mode 100644 index 0000000..f176620 --- /dev/null +++ b/scripting/include/smrcon.inc @@ -0,0 +1,50 @@ +/** + * @brief Called when an RCon session auth is processed + * + * @param rconId RCon listener ID, unique per session. + * @param address Originating IP address. + * @param password Password sent by RCon client. + * @param allow True to grant auth, false otherwise. + * @return Plugin_Changed to use given allow value, Plugin_Continue to let engine process. + */ +forward Action SMRCon_OnAuth(int rconId, const char[] address, const char[] password, bool &allow); + +/** + * @brief Called when an RCon command is processed. + * + * @note Rejection here does not count as a bad password attempt; + * however, the RCon log line will be annotated in the form + * of 'command (rejected) "%s"' rather than just 'command "%s"' + * + * @param rconId RCon listener ID, unique per session. + * @param address Originating IP address. + * @param command Command sent by RCon client. + * @param allow True to allow command to be processed, false otherwise. + * @return Plugin_Changed to use given allow value, Plugin_Continue to let engine process. + */ +forward Action SMRCon_OnCommand(int rconId, const char[] address, const char[] command, bool &allow); + +/** + * @brief Called when an RCon session is disconnected. + * + * @param rconId RCon listener ID, unique per session. + */ +forward void SMRCon_OnDisconnect(int rconId); + +/** + * @brief Called when an RCon log line is written + * + * @param rconId RCon listener ID, unique per session. + * @param address Originating IP address. + * @param logdata Log data (usually either "Bad Password" or "command" + * followed by the command. + * @return Plugin_Continue to log, Plugin_Handled to block. + */ +forward Action SMRCon_OnLog(int rconId, const char[] address, const char[] logdata); + +/** + * @brief Determines whether current server command originated from an RCon session. + * + * @return True if command originated from RCon session, false if from console or not in server command callback. + */ +native bool SMRCon_IsCmdFromRCon(); \ No newline at end of file diff --git a/scripting/l4d2_baseball.sp b/scripting/l4d2_baseball.sp new file mode 100644 index 0000000..cc763f5 --- /dev/null +++ b/scripting/l4d2_baseball.sp @@ -0,0 +1,171 @@ +#pragma semicolon 1 +#pragma newdecls required + +#include +#include + +#define HIT_1 "weapons/golf_club/wpn_golf_club_melee_01.wav" +#define HIT_2 "weapons/golf_club/wpn_golf_club_melee_02.wav" + +ConVar sv_melee_force_projectile, sv_melee_radius_projectile, sv_melee_force_boost_projectile_up; +int g_iLaser, g_iGlow; + +public Plugin myinfo = +{ + name = "[L4D2] Baseball", + author = "BHaType", + description = "Melee weapons can now deflect projectile", + version = "0.0", + url = "" +} + +public void OnPluginStart() +{ + sv_melee_force_projectile = CreateConVar("sv_melee_force_projectile", "0.6"); + sv_melee_force_boost_projectile_up = CreateConVar("sv_melee_force_boost_projectile_up", "250.0"); + sv_melee_radius_projectile = CreateConVar("sv_melee_radius_projectile", "75.0"); + + AutoExecConfig(true, "l4d2_baseball"); + + HookEvent("weapon_fire", weapon_fire); + HookEvent("entity_shoved", entity_shoved); +} + +public void OnMapStart() +{ + PrecacheSound(HIT_1, true); + PrecacheSound(HIT_2, true); + + g_iLaser = PrecacheModel("materials/sprites/laserbeam.vmt"); + g_iGlow = PrecacheModel("materials/sprites/glow.vmt"); +} + +public void entity_shoved (Event event, const char[] name, bool dontbroadcast) +{ + int entity = event.GetInt("entityid"); + + static char szName[36]; + GetEntityClassname(entity, szName, sizeof szName); + + if ( StrContains(szName, "_projectile") != -1 ) + { + float vVelocity[3]; + vVelocity = CalculateBaseForce(entity); + vVelocity[2] += sv_melee_force_boost_projectile_up.FloatValue; + + TeleportEntity(entity, NULL_VECTOR, NULL_VECTOR, vVelocity); + } +} + +public void weapon_fire (Event event, const char[] name, bool dontbroadcast) +{ + static char szName[36]; + event.GetString("weapon", szName, sizeof szName); + + if ( strcmp(szName, "melee") != 0 ) + return; + + int client = event.GetInt("userid"); + timer (CreateTimer(0.1, timer, client, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE), client); +} + +public Action timer (Handle timer, int client) +{ + client = GetClientOfUserId(client); + + if ( !client ) + return Plugin_Stop; + + int weapon = GetPlayerWeaponSlot(client, 1); + + if ( weapon == -1 || GetEntPropFloat(weapon, Prop_Send, "m_flNextPrimaryAttack") <= GetGameTime()) + return Plugin_Stop; + + float vAngles[3], vOrigin[3], vVector[3], vEnd[3]; + + GetClientEyePosition(client, vOrigin); + + GetClientEyeAngles(client, vAngles); + GetAngleVectors(vAngles, vVector, NULL_VECTOR, NULL_VECTOR); + ScaleVector(vVector, sv_melee_radius_projectile.FloatValue); + AddVectors(vOrigin, vVector, vEnd); + + GetClientEyePosition(client, vOrigin); + + #define hull 10.0 + static const float vMaxs[3] = { hull, hull, hull }; + static const float vMins[3] = { -hull, -hull, -hull }; + + TR_TraceHullFilter(vOrigin, vEnd, vMins, vMaxs, MASK_SOLID, TraceFilter, client); + float vHit[3]; + TR_GetEndPosition(vHit); + + if ( TR_DidHit () ) + { + int entity = TR_GetEntityIndex(); + + if ( entity != 0 ) + { + static char szName[36]; + GetEntityClassname(entity, szName, sizeof szName); + + if ( StrContains(szName, "_projectile") != -1 ) + { + float vVelocity[3], vVec[3]; + + vVelocity = CalculateBaseForce(entity, client); + GetEntPropVector(entity, Prop_Send, "m_vecOrigin", vOrigin); + + MakeVectorFromPoints(vHit, vOrigin, vVec); + ScaleVector(vVec, 1.0 - sv_melee_force_projectile.FloatValue * sv_melee_radius_projectile.FloatValue); + AddVectors(vVec, vVector, vVec); + AddVectors(vVec, vVelocity, vVelocity); + + TE_SetupSparks(vHit, vVelocity, 1, 1); + TE_SendToAll(); + + NegateVector(vVelocity); + + vVelocity[2] += sv_melee_force_boost_projectile_up.FloatValue; + TeleportEntity(entity, NULL_VECTOR, NULL_VECTOR, vVelocity); + + int color[4] = { 255, ... }; + for (int i; i <= 2; i++) + color[i] = GetRandomInt(0, 255); + + TE_SetupBeamFollow(entity, g_iLaser, g_iGlow, 4.6, 0.8, 0.8, 1, color); + TE_SendToAll(); + + EmitSoundToAll((GetRandomInt(0, 1) == 0 ? HIT_1 : HIT_2), SOUND_FROM_WORLD, .origin = vHit); + + // PrintToChatAll("\x04%N \x03baseballed projectile for \x04%.2f \x03velocity!", client, GetVectorLength(vVelocity)); + return Plugin_Stop; + } + } + } + + return Plugin_Continue; +} + +float[] CalculateBaseForce (int victim, int attacker = 0) +{ + float m_vecBaseVelocity[3], m_vecVelocity[3], vAngles[3]; + + if ( attacker ) + { + GetEntPropVector(attacker, Prop_Send, "m_vecBaseVelocity", m_vecBaseVelocity); + GetClientEyeAngles(attacker, vAngles); + } + + GetEntPropVector(victim, Prop_Data, "m_vecVelocity", m_vecVelocity); + AddVectors(m_vecBaseVelocity, m_vecVelocity, m_vecVelocity); + + ScaleVector(m_vecVelocity, sv_melee_force_projectile.FloatValue); + + return m_vecVelocity; +} + +public bool TraceFilter (int entity, int mask, int data) +{ + return entity != data; +} \ No newline at end of file diff --git a/scripting/l4d_anti_rush.sp b/scripting/l4d_anti_rush.sp new file mode 100644 index 0000000..ae7ce27 --- /dev/null +++ b/scripting/l4d_anti_rush.sp @@ -0,0 +1,1020 @@ +/* +* Anti Rush +* Copyright (C) 2021 Silvers +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + + + +#define PLUGIN_VERSION "1.17" +#define DEBUG_BENCHMARK 0 // 0=Off. 1=Benchmark logic function. + +/*====================================================================================== + Plugin Info: + +* Name : [L4D & L4D2] Anti Rush +* Author : SilverShot +* Descrp : Slowdown or teleport rushers and slackers back to the group. Uses flow distance for accuracy. +* Link : https://forums.alliedmods.net/showthread.php?t=322392 +* Plugins : https://sourcemod.net/plugins.php?exact=exact&sortby=title&search=1&author=Silvers + +======================================================================================== + Change Log: + +1.17 (04-Dec-2021) + - Changes to fix warnings when compiling on SourceMod 1.11. + +1.16 (19-Oct-2021) + - Plugin now ignores players who are being healed or revived, or players healing or reviving someone. Requested to "Psyk0tik". + +1.15 (09-Sep-2021) + - Fixed the last update breaking the plugin in L4D1. + +1.14 (25-Aug-2021) + - Plugin now ignores players being carried by the Charger. Thanks to "Darkwob" for reporting. + +1.13 (30-Jun-2021) + - Plugin now ignores players inside elevators. + +1.12 (16-Jun-2021) + - L4D1: Fixed throwing errors about missing property "m_type". Thanks to "Dragokas" for reporting. + +1.11 (20-Apr-2021) + - Added cvars "l4d_anti_rush_flags" and "l4d_anti_rush_ignore" to make players with certain flags immune to the plugins actions. Requested by "Shadowart". + +1.10a (12-Apr-2021) + - Updated data config "data/l4d_anti_rush.cfg" with new triggers. Thanks to "jeremyvillanueva" for providing. + +1.10 (23-Mar-2021) + - Cleaning source code from last update. + - Fixed potentially using the wrong "add" range from the config. + +1.9 (22-Mar-2021) + - Added optional config "data/l4d_anti_rush.cfg" to extend the trigger detection range during certain crescendo events. Requested by "SilentBr". + +1.8 (09-Oct-2020) + - Changed cvar "l4d_anti_rush_finale" to allow working in Gauntlet type finales only. Thanks to "Xanaguy" for requesting. + - Renamed cvar "l4d_anti_rush_inacpped" to "l4d_anti_rush_incapped" fixing spelling mistake. + +1.7 (09-Oct-2020) + - Added cvar "l4d_anti_rush_tanks" to control if the plugins active when any tank is alive. + - Fixed not resetting slowdown on team change or player death (optimization). + +1.6 (15-Jul-2020) + - Optionally added left4dhooks forwards "L4D_OnGetCrouchTopSpeed" and "L4D_OnGetWalkTopSpeed" to modify speed when walking or crouched. + - Uncomment the section and recompile if you want to enable. Only required to slowdown players more than default. + - Thanks to "SilentBr" for reporting. + +1.5 (10-May-2020) + - Added Traditional Chinese and Simplified Chinese translations. Thanks to "fbef0102". + - Extra checks to prevent "IsAllowedGameMode" throwing errors. + - Various changes to tidy up code. + +1.4 (10-Apr-2020) + - Added Hungarian translations. Thanks to "KasperH" for providing. + - Added Russian translations. Thanks to "Dragokas" for updating with new phrases. + - Added cvar "l4d_anti_rush_incapped" to ignored incapped players from being used to calculate rushers or slackers distance. + - Added cvars "l4d_anti_rush_warn_last" and "l4d_anti_rush_warn_lead" to warn players about being teleported or slowed down. + - Added cvar "l4d_anti_rush_warn_time" to control how often someone is warned. + - Removed minimum value being set for "l4d_anti_rush_range_lead" cvar which prevented turning off lead feature. + - The cvars "l4d_anti_rush_range_last" and "l4d_anti_rush_range_lead" minimum values are now set internally (1500.0). + - Translation files and plugin updated. + +1.3 (09-Apr-2020) + - Added reset slowdown in case players are out-of-bound or have invalid flow distances to calculate the range. + - Increased minimum value of "l4d_anti_rush_range_lead" cvar from 500.0 to 1000.0. + - Removed minimum value being set for "l4d_anti_rush_range_last" cvar. Thanks to "Alex101192" for reporting. + +1.2 (08-Apr-2020) + - Added cvar "l4d_anti_rush_finale" to allow or disallow the plugin in finales. + +1.1 (07-Apr-2020) + - Changed how the plugin functions. Now calculates rushers/slackers by their flow distance to the nearest half of Survivors. + - Fixed not accounting for multiple rushers with "type 2" setting. + - Fixed "IsAllowedGameMode" from throwing errors when the "_tog" cvar was changed before MapStart. + +1.0 (26-Mar-2020) + - Added Russian translations to the .zip. Thanks to "KRUTIK" for providing. + - No other changes. + +1.0 (26-Mar-2020) + - Initial release. + +======================================================================================*/ + +#pragma semicolon 1 +#pragma newdecls required + +#include +#include +#include +#include + +#if DEBUG_BENCHMARK +#include +Handle g_Prof; +float g_fBenchMin; +float g_fBenchMax; +float g_fBenchAvg; +float g_iBenchTicks; +#endif + +#define CVAR_FLAGS FCVAR_NOTIFY +#define MINIMUM_RANGE 1500.0 // Minimum range for last and lead cvars. +#define EVENTS_CONFIG "data/l4d_anti_rush.cfg" + + +ConVar g_hCvarAllow, g_hCvarMPGameMode, g_hCvarModes, g_hCvarModesOff, g_hCvarModesTog, g_hCvarFinale, g_hCvarFlags, g_hCvarIgnore, g_hCvarIncap, g_hCvarPlayers, g_hCvarRangeLast, g_hCvarRangeLead, g_hCvarSlow, g_hCvarTank, g_hCvarText, g_hCvarTime, g_hCvarType, g_hCvarWarnLast, g_hCvarWarnLead, g_hCvarWarnTime; +float g_fCvarRangeLast, g_fCvarRangeLead, g_fCvarSlow, g_fCvarTime, g_fCvarWarnLast, g_fCvarWarnLead, g_fCvarWarnTime; +int g_iCvarFinale, g_iCvarFlags, g_iCvarIgnore, g_iCvarIncap, g_iCvarPlayers, g_iCvarTank, g_iCvarText, g_iCvarType; +bool g_bCvarAllow, g_bMapStarted, g_bLeft4Dead2; + +bool g_bInhibit[MAXPLAYERS+1]; +float g_fHintLast[MAXPLAYERS+1]; +float g_fHintWarn[MAXPLAYERS+1]; +float g_fLastFlow[MAXPLAYERS+1]; +Handle g_hTimer; + +char g_sMap[PLATFORM_MAX_PATH]; +bool g_bFoundMap; +bool g_bEventStarted; +float g_fEventExtended; + +ArrayList g_hElevators; +GlobalForward g_OnRushForward; + + +// ==================================================================================================== +// PLUGIN INFO / START / END +// ==================================================================================================== +public Plugin myinfo = +{ + name = "[L4D & L4D2] Anti Rush", + author = "SilverShot", + description = "Slowdown or teleport rushers and slackers back to the group. Uses flow distance for accuracy.", + version = PLUGIN_VERSION, + url = "https://forums.alliedmods.net/showthread.php?t=322392" +} + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + EngineVersion test = GetEngineVersion(); + + if( test == Engine_Left4Dead ) g_bLeft4Dead2 = false; + else if( test == Engine_Left4Dead2 ) g_bLeft4Dead2 = true; + else + { + strcopy(error, err_max, "Plugin only supports Left 4 Dead 1 & 2."); + return APLRes_SilentFailure; + } + + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("anti_rush.phrases"); + + g_hCvarAllow = CreateConVar( "l4d_anti_rush_allow", "1", "0=Plugin off, 1=Plugin on.", CVAR_FLAGS ); + g_hCvarModes = CreateConVar( "l4d_anti_rush_modes", "", "Turn on the plugin in these game modes, separate by commas (no spaces). (Empty = all).", CVAR_FLAGS ); + g_hCvarModesOff = CreateConVar( "l4d_anti_rush_modes_off", "", "Turn off the plugin in these game modes, separate by commas (no spaces). (Empty = none).", CVAR_FLAGS ); + g_hCvarModesTog = CreateConVar( "l4d_anti_rush_modes_tog", "0", "Turn on the plugin in these game modes. 0=All, 1=Coop, 2=Survival, 4=Versus, 8=Scavenge. Add numbers together.", CVAR_FLAGS ); + g_hCvarFinale = CreateConVar( "l4d_anti_rush_finale", g_bLeft4Dead2 ? "2" : "0", "Should the plugin activate in finales. 0=Off. 1=All finales. 2=Gauntlet type finales (L4D2 only).", CVAR_FLAGS ); + g_hCvarFlags = CreateConVar( "l4d_anti_rush_flags", "", "Players with these flags will be immune from teleporting forward when behind or slowing down when ahead.", CVAR_FLAGS ); + g_hCvarIgnore = CreateConVar( "l4d_anti_rush_ignore", "0", "Should players with the immune flags be counted toward total flow distance. 0=Ignore them. 1=Count them.", CVAR_FLAGS ); + g_hCvarIncap = CreateConVar( "l4d_anti_rush_incapped", "0", "0=Off. How many survivors must be incapped before ignoring them in calculating rushers and slackers.", CVAR_FLAGS ); + g_hCvarPlayers = CreateConVar( "l4d_anti_rush_players", "3", "Minimum number of alive survivors before the function kicks in. Must be 3 or greater otherwise the lead/last and average cannot be detected.", CVAR_FLAGS, true, 3.0 ); + g_hCvarRangeLast = CreateConVar( "l4d_anti_rush_range_last", "3000.0", "How far behind someone can travel from the average Survivor distance before being teleported forward.", CVAR_FLAGS, true, MINIMUM_RANGE ); + g_hCvarRangeLead = CreateConVar( "l4d_anti_rush_range_lead", "3000.0", "How far forward someone can travel from the average Survivor distance before being teleported or slowed down.", CVAR_FLAGS, true, MINIMUM_RANGE ); + g_hCvarSlow = CreateConVar( "l4d_anti_rush_slow", "75.0", "Maximum speed someone can travel when being slowed down.", CVAR_FLAGS, true, 20.0 ); + g_hCvarTank = CreateConVar( "l4d_anti_rush_tanks", "1", "0=Off. 1=On. Should Anti-Rush be enabled when there are active Tanks.", CVAR_FLAGS ); + g_hCvarText = CreateConVar( "l4d_anti_rush_text", "1", "0=Off. 1=Print To Chat. 2=Hint Text. Display a message to someone rushing, or falling behind.", CVAR_FLAGS ); + g_hCvarTime = CreateConVar( "l4d_anti_rush_time", "10", "How often to print the message to someone if slowdown is enabled and affecting them.", CVAR_FLAGS ); + g_hCvarType = CreateConVar( "l4d_anti_rush_type", "1", "What to do with rushers. 1=Slowdown player speed when moving forward. 2=Teleport back to group. 3=Spawn Special On Them", CVAR_FLAGS ); + g_hCvarWarnLast = CreateConVar( "l4d_anti_rush_warn_last", "2500.0", "How far behind someone can travel from the average Survivor distance before being warned about being teleported.", CVAR_FLAGS, true, MINIMUM_RANGE ); + g_hCvarWarnLead = CreateConVar( "l4d_anti_rush_warn_lead", "2500.0", "How far forward someone can travel from the average Survivor distance before being warned about being teleported or slowed down.", CVAR_FLAGS, true, MINIMUM_RANGE ); + g_hCvarWarnTime = CreateConVar( "l4d_anti_rush_warn_time", "15.0", "0.0=Off. How often to print a message to someone warning them they are ahead or behind and will be teleported or slowed down.", CVAR_FLAGS ); + CreateConVar( "l4d_anti_rush_version", PLUGIN_VERSION, "Anti Rush plugin version.", FCVAR_NOTIFY|FCVAR_DONTRECORD); + AutoExecConfig(true, "l4d_anti_rush"); + + g_hCvarMPGameMode = FindConVar("mp_gamemode"); + g_hCvarMPGameMode.AddChangeHook(ConVarChanged_Allow); + g_hCvarModes.AddChangeHook(ConVarChanged_Allow); + g_hCvarModesOff.AddChangeHook(ConVarChanged_Allow); + g_hCvarModesTog.AddChangeHook(ConVarChanged_Allow); + g_hCvarAllow.AddChangeHook(ConVarChanged_Allow); + g_hCvarFinale.AddChangeHook(ConVarChanged_Cvars); + g_hCvarFlags.AddChangeHook(ConVarChanged_Cvars); + g_hCvarIgnore.AddChangeHook(ConVarChanged_Cvars); + g_hCvarIncap.AddChangeHook(ConVarChanged_Cvars); + g_hCvarPlayers.AddChangeHook(ConVarChanged_Cvars); + g_hCvarRangeLast.AddChangeHook(ConVarChanged_Cvars); + g_hCvarRangeLead.AddChangeHook(ConVarChanged_Cvars); + g_hCvarTank.AddChangeHook(ConVarChanged_Cvars); + g_hCvarText.AddChangeHook(ConVarChanged_Cvars); + g_hCvarSlow.AddChangeHook(ConVarChanged_Cvars); + g_hCvarTime.AddChangeHook(ConVarChanged_Cvars); + g_hCvarType.AddChangeHook(ConVarChanged_Cvars); + g_hCvarWarnLast.AddChangeHook(ConVarChanged_Cvars); + g_hCvarWarnLead.AddChangeHook(ConVarChanged_Cvars); + g_hCvarWarnTime.AddChangeHook(ConVarChanged_Cvars); + + g_hElevators = new ArrayList(); + + #if DEBUG_BENCHMARK + g_Prof = CreateProfiler(); + #endif + + g_OnRushForward = new GlobalForward("OnAntiRush", ET_Event, Param_Cell, Param_CellByRef, Param_Float); +} + + + +// ==================================================================================================== +// CVARS +// ==================================================================================================== +public void OnConfigsExecuted() +{ + IsAllowed(); +} + +public void ConVarChanged_Allow(Handle convar, const char[] oldValue, const char[] newValue) +{ + IsAllowed(); +} + +public void ConVarChanged_Cvars(Handle convar, const char[] oldValue, const char[] newValue) +{ + GetCvars(); +} + +void GetCvars() +{ + char sTemp[32]; + g_hCvarFlags.GetString(sTemp, sizeof(sTemp)); + g_iCvarFlags = ReadFlagString(sTemp); + g_iCvarIgnore = g_hCvarIgnore.IntValue; + + g_iCvarFinale = g_hCvarFinale.IntValue; + g_iCvarIncap = g_hCvarIncap.IntValue; + g_iCvarPlayers = g_hCvarPlayers.IntValue; + g_fCvarTime = g_hCvarTime.FloatValue; + g_iCvarTank = g_hCvarTank.IntValue; + g_iCvarText = g_hCvarText.IntValue; + g_fCvarSlow = g_hCvarSlow.FloatValue; + g_iCvarType = g_hCvarType.IntValue; + g_fCvarRangeLast = g_hCvarRangeLast.FloatValue; + g_fCvarRangeLead = g_hCvarRangeLead.FloatValue; + g_fCvarWarnLast = g_hCvarWarnLast.FloatValue; + g_fCvarWarnLead = g_hCvarWarnLead.FloatValue; + g_fCvarWarnTime = g_hCvarWarnTime.FloatValue; + + if( g_fCvarRangeLast && g_fCvarRangeLast < MINIMUM_RANGE ) g_fCvarRangeLast = MINIMUM_RANGE; + if( g_fCvarRangeLead && g_fCvarRangeLead < MINIMUM_RANGE ) g_fCvarRangeLead = MINIMUM_RANGE; + if( g_fCvarWarnLast && g_fCvarWarnLast < MINIMUM_RANGE ) g_fCvarWarnLast = MINIMUM_RANGE; + if( g_fCvarWarnLead && g_fCvarWarnLead < MINIMUM_RANGE ) g_fCvarWarnLead = MINIMUM_RANGE; +} + +void IsAllowed() +{ + bool bCvarAllow = g_hCvarAllow.BoolValue; + bool bAllowMode = IsAllowedGameMode(); + GetCvars(); + + if( g_bCvarAllow == false && bCvarAllow == true && bAllowMode == true ) + { + g_bCvarAllow = true; + + HookEvent("round_start", Event_RoundStart); + HookEvent("round_end", Event_RoundEnd); + HookEvent("player_death", Event_PlayerDeath); + HookEvent("player_team", Event_PlayerTeam); + + Event_RoundStart(null, "", false); + } + + else if( g_bCvarAllow == true && (bCvarAllow == false || bAllowMode == false) ) + { + g_bCvarAllow = false; + + UnhookEvent("round_start", Event_RoundStart); + UnhookEvent("round_end", Event_RoundEnd); + UnhookEvent("player_death", Event_PlayerDeath); + UnhookEvent("player_team", Event_PlayerTeam); + + ResetSlowdown(); + ResetPlugin(); + } +} + +int g_iCurrentMode; +bool IsAllowedGameMode() +{ + if( g_hCvarMPGameMode == null ) + return false; + + int iCvarModesTog = g_hCvarModesTog.IntValue; + if( iCvarModesTog != 0 ) + { + if( g_bMapStarted == false ) + return false; + + g_iCurrentMode = 0; + + int entity = CreateEntityByName("info_gamemode"); + if( IsValidEntity(entity) ) + { + DispatchSpawn(entity); + HookSingleEntityOutput(entity, "OnCoop", OnGamemode, true); + HookSingleEntityOutput(entity, "OnSurvival", OnGamemode, true); + HookSingleEntityOutput(entity, "OnVersus", OnGamemode, true); + HookSingleEntityOutput(entity, "OnScavenge", OnGamemode, true); + ActivateEntity(entity); + AcceptEntityInput(entity, "PostSpawnActivate"); + if( IsValidEntity(entity) ) // Because sometimes "PostSpawnActivate" seems to kill the ent. + RemoveEdict(entity); // Because multiple plugins creating at once, avoid too many duplicate ents in the same frame + } + + if( g_iCurrentMode == 0 ) + return false; + + if( !(iCvarModesTog & g_iCurrentMode) ) + return false; + } + + char sGameModes[64], sGameMode[64]; + g_hCvarMPGameMode.GetString(sGameMode, sizeof(sGameMode)); + Format(sGameMode, sizeof(sGameMode), ",%s,", sGameMode); + + g_hCvarModes.GetString(sGameModes, sizeof(sGameModes)); + if( sGameModes[0] ) + { + Format(sGameModes, sizeof(sGameModes), ",%s,", sGameModes); + if( StrContains(sGameModes, sGameMode, false) == -1 ) + return false; + } + + g_hCvarModesOff.GetString(sGameModes, sizeof(sGameModes)); + if( sGameModes[0] ) + { + Format(sGameModes, sizeof(sGameModes), ",%s,", sGameModes); + if( StrContains(sGameModes, sGameMode, false) != -1 ) + return false; + } + + return true; +} + +public void OnGamemode(const char[] output, int caller, int activator, float delay) +{ + if( strcmp(output, "OnCoop") == 0 ) + g_iCurrentMode = 1; + else if( strcmp(output, "OnSurvival") == 0 ) + g_iCurrentMode = 2; + else if( strcmp(output, "OnVersus") == 0 ) + g_iCurrentMode = 4; + else if( strcmp(output, "OnScavenge") == 0 ) + g_iCurrentMode = 8; +} + + + +// ==================================================================================================== +// HOOK CUSTOM ARLARM EVENTS +// ==================================================================================================== +int g_iSectionLevel; + +void LoadEventConfig() +{ + g_bFoundMap = false; + + char sPath[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, sPath, sizeof(sPath), EVENTS_CONFIG); + if( FileExists(sPath) ) + { + ParseConfigFile(sPath); + } +} + +bool ParseConfigFile(const char[] file) +{ + SMCParser parser = new SMCParser(); + SMC_SetReaders(parser, ColorConfig_NewSection, ColorConfig_KeyValue, ColorConfig_EndSection); + parser.OnEnd = ColorConfig_End; + + char error[128]; + int line = 0, col = 0; + SMCError result = parser.ParseFile(file, line, col); + + if( result != SMCError_Okay ) + { + parser.GetErrorString(result, error, sizeof(error)); + SetFailState("%s on line %d, col %d of %s [%d]", error, line, col, file, result); + } + + delete parser; + return (result == SMCError_Okay); +} + +public SMCResult ColorConfig_NewSection(Handle parser, const char[] section, bool quotes) +{ + g_iSectionLevel++; + + // Map + if( g_iSectionLevel == 2 && strcmp(section, g_sMap) == 0 ) + { + g_bFoundMap = true; + } else { + g_bFoundMap = false; + } + + return SMCParse_Continue; +} + +public SMCResult ColorConfig_KeyValue(Handle parser, const char[] key, const char[] value, bool key_quotes, bool value_quotes) +{ + // On / Off + if( g_iSectionLevel == 2 && g_bFoundMap ) + { + if( strcmp(key, "add") == 0 ) + { + g_fEventExtended = StringToFloat(value); + } else { + static char sSplit[3][64]; + + int len = ExplodeString(value, ":", sSplit, sizeof(sSplit), sizeof(sSplit[])); + if( len != 3 ) + { + LogError("Malformed string in l4d_anti_rush.cfg. Section [%s] key [%s] value [%s].", g_sMap, key, value); + } else { + int entity = FindByClassTargetName(sSplit[0], sSplit[1]); + if( entity != INVALID_ENT_REFERENCE ) + { + if( strcmp(key, "1") == 0 ) + { + HookSingleEntityOutput(entity, sSplit[2], OutputStart); + } + else if( strcmp(key, "0") == 0 ) + { + HookSingleEntityOutput(entity, sSplit[2], OutputStop); + } + } + } + } + } + + return SMCParse_Continue; +} + +public void OutputStart(const char[] output, int caller, int activator, float delay) +{ + g_bEventStarted = true; +} + +public void OutputStop(const char[] output, int caller, int activator, float delay) +{ + g_bEventStarted = false; +} + + +public SMCResult ColorConfig_EndSection(Handle parser) +{ + g_iSectionLevel--; + return SMCParse_Continue; +} + +public void ColorConfig_End(Handle parser, bool halted, bool failed) +{ + if( failed ) + SetFailState("Error: Cannot load the config file: \"%s\"", EVENTS_CONFIG); +} + +int FindByClassTargetName(const char[] sClass, const char[] sTarget) +{ + char sName[64]; + int entity = INVALID_ENT_REFERENCE; + + // Is targetname numeric? + bool numeric = true; + for( int i = 0; i < strlen(sTarget); i++ ) + { + if( IsCharNumeric(sTarget[i]) == false ) + { + numeric = false; + break; + } + } + + // Search by hammer ID or targetname + while( (entity = FindEntityByClassname(entity, sClass)) != INVALID_ENT_REFERENCE ) + { + if( numeric ) + { + if( GetEntProp(entity, Prop_Data, "m_iHammerID") == StringToInt(sTarget) ) return entity; + } else { + GetEntPropString(entity, Prop_Data, "m_iName", sName, sizeof(sName)); + if( strcmp(sTarget, sName) == 0 ) return entity; + } + } + return INVALID_ENT_REFERENCE; +} + + + +// ==================================================================================================== +// EVENTS +// ==================================================================================================== +public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast) +{ + delete g_hTimer; + + // Finales allowed, or not finale + if( g_iCvarFinale || (g_iCvarFinale == 0 && L4D_IsMissionFinalMap() == false) ) + { + // Gauntlet finale only + if( g_iCvarFinale == 2 && g_bLeft4Dead2 ) + { + int entity = FindEntityByClassname(-1, "trigger_finale"); + if( entity != -1 ) + { + if( GetEntProp(entity, Prop_Data, "m_type") != 1 ) return; + } + } + + g_hTimer = CreateTimer(1.0, TimerTest, _, TIMER_REPEAT); + + LoadEventConfig(); + } + + // Get elevators + g_hElevators.Clear(); + + int entity = -1; + while( (entity = FindEntityByClassname(entity, "func_elevator")) != INVALID_ENT_REFERENCE ) + { + g_hElevators.Push(EntIndexToEntRef(entity)); + } +} + +public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast) +{ + ResetSlowdown(); + ResetPlugin(); +} + +public void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + if( client ) + { + ResetClient(client); + } +} + +public void Event_PlayerTeam(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + if( client ) + { + ResetClient(client); + } +} + +public void OnMapStart() +{ + GetCurrentMap(g_sMap, sizeof(g_sMap)); + g_bMapStarted = true; +} + +public void OnMapEnd() +{ + g_bMapStarted = false; + ResetPlugin(); +} + +void ResetPlugin() +{ + for( int i = 1; i <= MAXPLAYERS; i++ ) + { + ResetClient(i); + } + + delete g_hTimer; + + g_hElevators.Clear(); + g_fEventExtended = 0.0; + g_bEventStarted = false; +} + +void ResetClient(int i) +{ + g_bInhibit[i] = false; + g_fHintLast[i] = 0.0; + g_fHintWarn[i] = 0.0; + g_fLastFlow[i] = 0.0; + + SDKUnhook(i, SDKHook_PreThinkPost, PreThinkPost); +} + +void ResetSlowdown() +{ + for( int i = 1; i <= MaxClients; i++ ) + { + if( g_bInhibit[i] && IsClientInGame(i) ) + { + SDKUnhook(i, SDKHook_PreThinkPost, PreThinkPost); + } + + g_bInhibit[i] = false; + } +} + + + +// ==================================================================================================== +// LOGIC +// ==================================================================================================== +public Action TimerTest(Handle timer) +{ + if( !g_bMapStarted ) return Plugin_Continue; + + #if DEBUG_BENCHMARK + StartProfiling(g_Prof); + #endif + + static bool bTanks; + if( g_iCvarTank == 0 ) + { + if( L4D2_GetTankCount() > 0 ) + { + if( !bTanks ) + { + bTanks = true; + ResetSlowdown(); + } + + return Plugin_Continue; + } else { + bTanks = false; + } + } + + float flow; + int count, countflow, index; + + // Get survivors flow distance + ArrayList aList = new ArrayList(2); + + // Account for incapped + int clients[MAXPLAYERS+1]; + int incapped, client; + + // Check valid survivors, count incapped + for( int i = 1; i <= MaxClients; i++ ) + { + if( IsClientInGame(i) && GetClientTeam(i) == 2 && IsPlayerAlive(i) ) + { + // Immune players - ignore from flow calculations + if( g_iCvarFlags != 0 && g_iCvarIgnore == 0 && CheckCommandAccess(client, "", g_iCvarFlags, true) ) + { + continue; + } + + // Count + clients[count++] = i; + + if( g_iCvarIncap ) + { + if( GetEntProp(i, Prop_Send, "m_isIncapacitated", 1) ) + incapped++; + } + } + } + + for( int i = 0; i < count; i++ ) + { + client = clients[i]; + + // Ignore incapped + if( g_iCvarIncap && incapped >= g_iCvarIncap && GetEntProp(client, Prop_Send, "m_isIncapacitated", 1) ) + continue; + + // Ignore healing / using stuff + if( GetEntPropEnt(client, Prop_Send, "m_useActionTarget") > 0 ) + continue; + + // Ignore reviving + if( GetEntPropEnt(client, Prop_Send, "m_reviveOwner") > 0 || GetEntPropEnt(client, Prop_Send, "m_reviveTarget") > 0 ) + continue; + + // Ignore pinned by Charger + if( g_bLeft4Dead2 && GetEntPropEnt(client, Prop_Send, "m_carryAttacker") != -1 ) + continue; + + // Ignore in Elevator + int lift = GetEntPropEnt(client, Prop_Send, "m_hGroundEntity"); + if( lift > MaxClients ) + { + lift = EntIndexToEntRef(lift); + if( g_hElevators.FindValue(lift) != -1 ) + continue; + } + + // Get flow + flow = L4D2Direct_GetFlowDistance(client); + if( flow && flow != -9999.0 ) // Invalid flows + { + countflow++; + index = aList.Push(flow); + aList.Set(index, client, 1); + } + // Reset slowdown if players flow is invalid + else if( g_bInhibit[client] == true ) + { + g_bInhibit[client] = false; + SDKUnhook(client, SDKHook_PreThinkPost, PreThinkPost); + } + } + + // In case not enough players or some have invalid flow distance, we still need an average. + if( countflow >= g_iCvarPlayers ) + { + aList.Sort(Sort_Descending, Sort_Float); + + int clientAvg; + float lastFlow; + float distance; + + + + // Detect rushers + if( g_fCvarRangeLead ) + { + // Loop through survivors from highest flow + for( int i = 0; i < countflow; i++ ) + { + client = aList.Get(i, 1); + + // Immune players + if( g_iCvarFlags != 0 && g_iCvarIgnore == 1 && CheckCommandAccess(client, "", g_iCvarFlags, true) ) + { + continue; + } + + bool flowBack = true; + + // Only check nearest half of survivor pack. + if( i < countflow / 2 ) + { + flow = aList.Get(i, 0); + + // Loop through from next survivor to mid-way through the pack. + for( int x = i + 1; x <= countflow / 2; x++ ) + { + lastFlow = aList.Get(x, 0); + distance = flow - lastFlow; + if( g_bEventStarted ) distance -= g_fEventExtended; + + // Warn ahead hint + if( g_iCvarText && g_fCvarWarnTime && g_fCvarWarnLead && distance > g_fCvarWarnLead && distance < g_fCvarRangeLead && g_fHintWarn[client] < GetGameTime() ) + { + g_fHintWarn[client] = GetGameTime() + g_fCvarWarnTime; + + if( g_iCvarType == 1 ) + ClientHintMessage(client, "Warn_Slowdown"); + else + ClientHintMessage(client, "Warn_Ahead"); + } + + // Compare higher flow with next survivor, they're rushing + if( distance > g_fCvarRangeLead ) + { + // PrintToServer("RUSH: %N %f", client, distance); + flowBack = false; + int punishType = g_iCvarType, result; + + Call_StartForward(g_OnRushForward); + Call_PushCell(client); + Call_PushCellRef(punishType); + Call_PushFloat(distance); + + if(Call_Finish(result) == SP_ERROR_NONE && result > 0) break; + + // Slowdown enabled? + if( punishType == 1 ) + { + // Inhibit moving forward + // Only check > or < because when == the same flow distance, they're either already being slowed or running back, so we don't want to change/affect them within the same flow NavMesh. + if( flow > g_fLastFlow[client] ) + { + g_fLastFlow[client] = flow; + + if( g_bInhibit[client] == false ) + { + g_bInhibit[client] = true; + SDKHook(client, SDKHook_PreThinkPost, PreThinkPost); + } + + // Hint + if( g_iCvarText && g_fHintLast[client] < GetGameTime() ) + { + g_fHintLast[client] = GetGameTime() + g_fCvarTime; + + ClientHintMessage(client, "Rush_Slowdown"); + } + } + else if( flow < g_fLastFlow[client] ) + { + flowBack = true; + g_fLastFlow[client] = flow; + } + } + + + + // Teleport enabled? + if( punishType == 2 && IsClientPinned(client) == false ) + { + clientAvg = aList.Get(x, 1); + float vPos[3]; + GetClientAbsOrigin(clientAvg, vPos); + + // Hint + if( g_iCvarText) + { + ClientHintMessage(client, "Rush_Ahead"); + } + + TeleportEntity(client, vPos, NULL_VECTOR, NULL_VECTOR); + } + + break; + } + } + } + + // Running back, allow full speed + if( flowBack && g_bInhibit[client] == true ) + { + g_bInhibit[client] = false; + SDKUnhook(client, SDKHook_PreThinkPost, PreThinkPost); + } + } + } + + + + // Teleport slacker + if( g_fCvarRangeLast ) + { + // Loop through survivors from lowest flow to mid-way through the pack. + for( int i = countflow - 1; i > countflow / 2; i-- ) + { + client = aList.Get(i, 1); + + // Immune players + if( g_iCvarFlags != 0 && g_iCvarIgnore == 1 && CheckCommandAccess(client, "", g_iCvarFlags, true) ) + { + continue; + } + + flow = aList.Get(i, 0); + + // Loop through from next survivor to mid-way through the pack. + for( int x = i - 1; x < countflow; x++ ) + { + lastFlow = aList.Get(x, 0); + distance = lastFlow - flow; + if( g_bEventStarted ) distance -= g_fEventExtended; + + // Warn behind hint + if( g_iCvarText && g_fCvarWarnTime && g_fCvarWarnLast && distance > g_fCvarWarnLast && distance < g_fCvarRangeLead && g_fHintWarn[client] < GetGameTime() ) + { + g_fHintWarn[client] = GetGameTime() + g_fCvarWarnTime; + + ClientHintMessage(client, "Warn_Behind"); + } + + // Compare lower flow with next survivor, they're behind + if( distance > g_fCvarRangeLast && IsClientPinned(client) == false ) + { + // PrintToServer("SLOW: %N %f", client, distance); + clientAvg = aList.Get(x, 1); + float vPos[3]; + GetClientAbsOrigin(clientAvg, vPos); + + // Hint + if( g_iCvarText ) + { + ClientHintMessage(client, "Rush_Behind"); + } + + TeleportEntity(client, vPos, NULL_VECTOR, NULL_VECTOR); + break; + } + } + } + } + } + else + { + ResetSlowdown(); + } + + delete aList; + + #if DEBUG_BENCHMARK + StopProfiling(g_Prof); + float speed = GetProfilerTime(g_Prof); + if( speed < g_fBenchMin ) g_fBenchMin = speed; + if( speed > g_fBenchMax ) g_fBenchMax = speed; + g_fBenchAvg += speed; + g_iBenchTicks++; + + PrintToServer("Anti Rush benchmark: %f (Min %f. Avg %f. Max %f)", speed, g_fBenchMin, g_fBenchAvg / g_iBenchTicks, g_fBenchMax); + #endif + + return Plugin_Continue; +} + +/* Remove this line to enable, if you want to limit speed (slower) than default when walking/crouched. +public Action L4D_OnGetCrouchTopSpeed(int target, float &retVal) +{ + if( g_bInhibit[target] ) + { + retVal = g_fCvarSlow; + return Plugin_Handled; + } + + return Plugin_Continue; +} + +public Action L4D_OnGetWalkTopSpeed(int target, float &retVal) +{ + if( g_bInhibit[target] ) + { + retVal = g_fCvarSlow; + return Plugin_Handled; + } + + return Plugin_Continue; +} +// */ + +public void PreThinkPost(int client) +{ + SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", g_fCvarSlow); +} + +void ClientHintMessage(int client, const char[] translation) +{ + static char sMessage[256]; + Format(sMessage, sizeof(sMessage), "%T", translation, client); + + if( g_iCvarText == 1 ) + { + ReplaceColors(sMessage, sizeof(sMessage), false); + PrintToChat(client, sMessage); + } else { + ReplaceColors(sMessage, sizeof(sMessage), true); + PrintHintText(client, sMessage); + } +} + +void ReplaceColors(char[] translation, int size, bool hint) +{ + ReplaceString(translation, size, "{white}", hint ? "" : "\x01"); + ReplaceString(translation, size, "{cyan}", hint ? "" : "\x03"); + ReplaceString(translation, size, "{orange}", hint ? "" : "\x04"); + ReplaceString(translation, size, "{green}", hint ? "" : "\x05"); +} + +bool IsClientPinned(int client) +{ + if( GetEntProp(client, Prop_Send, "m_isIncapacitated", 1) || + GetEntProp(client, Prop_Send, "m_isHangingFromLedge", 1) || + GetEntPropEnt(client, Prop_Send, "m_tongueOwner") > 0 || + GetEntPropEnt(client, Prop_Send, "m_pounceAttacker") > 0 + ) return true; + + if( g_bLeft4Dead2 && + ( + GetEntPropEnt(client, Prop_Send, "m_jockeyAttacker") > 0 || + GetEntPropEnt(client, Prop_Send, "m_carryAttacker") > 0 || + GetEntPropEnt(client, Prop_Send, "m_pummelAttacker") > 0 + )) return true; + + return false; +} \ No newline at end of file diff --git a/scripting/l4d_reservetheserver.sp b/scripting/l4d_reservetheserver.sp index c09c607..fbe3564 100644 --- a/scripting/l4d_reservetheserver.sp +++ b/scripting/l4d_reservetheserver.sp @@ -40,8 +40,9 @@ public void OnPluginStart() { PluginCvarTimeout = CreateConVar("l4d_rts_timeout", "30", "How long will the server stay disconnected from matchmaking? 0 - never restore matchmaking connection", 0, true, 0.0, true, 300.0); PluginCvarImmuneLevel = CreateConVar("l4d_rts_immunelevel", "1", "Any player >= to this level will cancel the lobby vote.", 0); - RegAdminCmd("sm_rts", Command_MakeReservation, ADMFLAG_BAN, "Free the server from all players, then reserve it."); - RegAdminCmd("sm_cr", Command_CancelReservation, ADMFLAG_BAN, "Cancel reservation and make server public again."); + RegAdminCmd("sm_rts", Command_MakeReservation, ADMFLAG_KICK, "Free the server from all players, then reserve it."); + RegAdminCmd("sm_cr", Command_CancelReservation, ADMFLAG_KICK, "Cancel reservation and make server public again."); + RegAdminCmd("sm_forcelobby", Command_ForceLobby, ADMFLAG_BAN, "Force call vote to return to lobby"); SteamGroupExclusiveCvar = FindConVar("sv_steamgroup_exclusive"); SearchKeyCvar = FindConVar("sv_search_key"); @@ -68,6 +69,17 @@ public void OnMapStart() { isMapChange = false; } +public Action Command_ForceLobby(int client, int args) { + Handle bf = StartMessageOne("VoteStart", client, USERMSG_RELIABLE); + BfWriteByte(bf, 0); + BfWriteByte(bf, client); + BfWriteString(bf, "returntolobby"); + BfWriteString(bf, ""); + BfWriteString(bf, ""); + EndMessage(); + PassVote(); +} + public Action Command_MakeReservation(int client, int args) { bool isAdminOnline, isServerEmpty = true; if(client > 0) { diff --git a/scripting/sm_player_recorder.sp b/scripting/sm_player_recorder.sp new file mode 100644 index 0000000..b961cf9 --- /dev/null +++ b/scripting/sm_player_recorder.sp @@ -0,0 +1,136 @@ +#pragma semicolon 1 + +#define DEBUG + +#define PLUGIN_AUTHOR "Jackz" +#define PLUGIN_VERSION "1.00" + +#define DATABASE_NAME "player-recorder" +#define RECORD_INTERVAL 60.0 + +#include +#include + +#pragma newdecls required + +public Plugin myinfo = { + name = "SRCDS Player Count Recorder", + author = PLUGIN_AUTHOR, + description = "", + version = PLUGIN_VERSION, + url = "" +}; + +static Database g_db; +static char playerID[32]; + +enum struct PlayerData { + int timestamp; + int playerCount; +} + +static ArrayList g_playerData; +static int iLastCount; +static bool active; + +public void OnPluginStart() { + if(!SQL_CheckConfig(DATABASE_NAME)) { + SetFailState("No database entry for '" ... DATABASE_NAME ... "'; no database to connect to."); + } else if(!ConnectDB()) { + SetFailState("Failed to connect to database."); + } + + g_playerData = new ArrayList(sizeof(PlayerData)); + + + ConVar hPlayerCountID = CreateConVar("sm_playercount_id", "", "The ID to use for player count recording. Will not record if not set", FCVAR_NONE); + hPlayerCountID.GetString(playerID, sizeof(playerID)); + hPlayerCountID.AddChangeHook(Change_ID); + + if(strlen(playerID) > 0) { + Init(); + } +} + +void Init() { + HookEvent("player_first_spawn", Event_Connection, EventHookMode_PostNoCopy); + HookEvent("player_disconnect", Event_Connection, EventHookMode_PostNoCopy); + + CreateTimer(RECORD_INTERVAL, Timer_PushCounts, _, TIMER_REPEAT); + active = true; + PlayerData data; + data.timestamp = GetTime(); + data.playerCount = GetPlayerCount(); + g_playerData.PushArray(data); + + +} + +public void Change_ID(ConVar convar, const char[] oldValue, const char[] newValue) { + convar.GetString(playerID, sizeof(playerID)); + if(!active && strlen(playerID) > 0) { + Init(); + } +} + +bool ConnectDB() { + char error[255]; + g_db = SQL_Connect(DATABASE_NAME, true, error, sizeof(error)); + if (g_db == null) { + LogError("Database error %s", error); + delete g_db; + return false; + } else { + PrintToServer("[SPR] Connected to database \"" ... DATABASE_NAME ... "\""); + SQL_LockDatabase(g_db); + SQL_FastQuery(g_db, "SET NAMES \"UTF8mb4\""); + SQL_UnlockDatabase(g_db); + g_db.SetCharset("utf8mb4"); + + return true; + } +} + +public void Event_Connection(Event event, const char[] name, bool dontBroadcast) { + int count = GetPlayerCount(); + if(count != iLastCount) { + PlayerData data; + data.timestamp = GetTime(); + data.playerCount = count; + g_playerData.PushArray(data); + iLastCount = count; + } +} + +public Action Timer_PushCounts(Handle h) { + Transaction transact = new Transaction(); + static char query[255]; + static PlayerData data; + int length = g_playerData.Length; + for(int i = 0; i < length; i++) { + g_playerData.GetArray(i, data, sizeof(data)); + g_db.Format(query, sizeof(query), "INSERT INTO player_count (server_name, timestamp, count) VALUES ('%s', %d, %d)", + playerID, + data.timestamp, + data.playerCount + ); + transact.AddQuery(query); + } + g_playerData.Resize(g_playerData.Length - length); + g_db.Execute(transact, _, SQL_TransactionFailed, length, DBPrio_Low); + return Plugin_Continue; +} + +public void SQL_TransactionFailed(Database db, any data, int numQueries, const char[] error, int failIndex, any[] queryData) { + PrintToServer("[PlayerRecorder] Push failure: %s at query %d/%d", error, failIndex, numQueries); +} + +int GetPlayerCount() { + int count; + for(int i = 1; i <= MaxClients; i++) { + if(IsClientConnected(i) && !IsFakeClient(i)) { + count++; + } + } + return count; +} \ No newline at end of file diff --git a/scripting/spray_control.sp b/scripting/spray_control.sp new file mode 100644 index 0000000..509068e --- /dev/null +++ b/scripting/spray_control.sp @@ -0,0 +1,194 @@ +#pragma semicolon 1 + +#define DEBUG + +#define PLUGIN_VERSION "0.00" + +#define DB_NAME "sprayfiltercontrol" +#define DB_TABLE "spray_results" //not used + +// The minimum value for detection, number is related to value of RESULT_TEXT +#define ADULT_THRES 2 +#define RACY_THRES 2 + +#include +#include +#include + +#pragma newdecls required + +public Plugin myinfo = { + name = "Spay Filter Control", + author = "jackzmc", + description = "", + version = PLUGIN_VERSION, + url = "" +}; + +static Database g_db; +static char apikey[64]; +/* TODO: +1. Plugin start, fetch API key from keyvalue +2. On client connect, check database for spray result +3. Run system2 if no result +*/ +char RESULT_TEXT[6][] = { + "UNKNOWN", + "VERY UNLIKELY", + "UNLIKELY", + "POSSIBLE", + "LIKELY", + "VERY LIKELY" +}; + +enum Result { + UNKNOWN = -1, + VERY_UNLIKELY = 0, + UNLIKELY, + POSSIBLE, + LIKELY, + VERY_LIKELY +} + +enum struct SprayResult { + Result adult; + Result racy; +} + +public void OnPluginStart() { + if(!SQL_CheckConfig(DB_NAME)) { + SetFailState("No database entry for " ... DB_NAME ... "; no database to connect to."); + } + if(!ConnectDB()) { + SetFailState("Failed to connect to database."); + } + + KeyValues kv = new KeyValues("Config"); + kv.ImportFromFile("spraycontrol.cfg"); + + if (!kv.JumpToKey("apikey")) { + delete kv; + SetFailState("No 'apikey' provided in spraycontrol.cfg"); + } + + kv.GetString("apikey", apikey, sizeof(apikey)); + + RegAdminCmd("sm_checkspray", Command_CheckSpray, ADMFLAG_GENERIC, "Gets the spray results of a user"); +} + +bool ConnectDB() { + char error[255]; + g_db = SQL_Connect(DB_NAME, true, error, sizeof(error)); + if (g_db == null) { + LogError("[SFC] Database error %s", error); + delete g_db; + return false; + } else { + SQL_LockDatabase(g_db); + SQL_FastQuery(g_db, "SET NAMES \"UTF8mb4\""); + SQL_UnlockDatabase(g_db); + g_db.SetCharset("utf8mb4"); + return true; + } +} + +// Events + +public void OnClientAuthorized(int client, const char[] auth) { + if(!StrEqual(auth, "BOT", true)) { + char filename[64], query[128]; + if(!GetPlayerDecalFile(client, filename, sizeof(filename))) { + return; //They don't have a spray + } + Format(query, sizeof(query), "SELECT adult,racy FROM spray_results WHERE steamid = '%s' AND sprayid = '%s'", auth, filename); + g_db.Query(DB_OnConnectCheck, query, GetClientUserId(client)); + } +} + +public void DB_OnConnectCheck(Database db, DBResultSet results, const char[] error, int user) { + int client = GetClientOfUserId(user); + if(db == INVALID_HANDLE || results == null) { + LogError("DB_OnConnectCheck returned error: %s", error); + }else{ + if(results.RowCount > 0 && client) { + int adult = results.FetchInt(0) + 1; + int racy = results.FetchInt(1) + 1; + + CheckUser(client, adult, racy); + } else { + char filename[64]; + if(!GetPlayerDecalFile(client, filename, sizeof(filename))) { + return; //They don't have a spray + } + System2_ExecuteFormattedThreaded(ExecuteCallback, GetClientUserId(client), "test-spray %s %s", filename, apikey); + } + } +} + +public void ExecuteCallback(bool success, const char[] command, System2ExecuteOutput output, int user) { + int client = GetClientOfUserId(user); + if(client <= 0) return; //Client disconnected, void result + if (!success || output.ExitStatus != 0) { + PrintToServer("[SFC] Could not get the spray result for %N", client); + } else { + char outputString[128]; + output.GetOutput(outputString, sizeof(outputString)); + + char results[2][64]; + char bit[3][16]; + ExplodeString(outputString, "\n", results, 2, 64); + + int adult = -1; + int racy = -1; + + ExplodeString(results[0], "=", bit, 3, 16); + adult = StringToInt(bit[2]); + ExplodeString(results[1], "=", bit, 3, 16); + racy = StringToInt(bit[2]); + + PrintToServer("[SFC] %N Spray Results | adult=%s racy=%s", RESULT_TEXT[adult], RESULT_TEXT[racy]); + + CheckUser(client, adult, racy); + } +} + +public Action Command_CheckSpray(int client, int args) { + if(args < 1) + ReplyToCommand(client, "Usage: sm_checkspray "); + else { + char arg1[64]; + GetCmdArg(1, arg1, sizeof(arg1)); + int target = FindTarget(client, arg1, true, false); + if(target > 0) { + char filename[64]; + if(!GetPlayerDecalFile(client, filename, sizeof(filename))) { + ReplyToCommand(client, "%N does not have a spray", target); + return Plugin_Handled; + } + System2_ExecuteFormattedThreaded(ExecuteCallback, GetClientUserId(client), "test-spray %s %s", filename, apikey); + } else { + ReplyToCommand(client, "Could not find target."); + } + } + return Plugin_Handled; +} + +void CheckUser(int client, int adult, int racy) { + if(adult > 3 || (adult > ADULT_THRES && racy > RACY_THRES)) { + PrintToAdmins("%N 's spray has a questionable spray. Adult=%s Racy=%s", + client, + RESULT_TEXT[adult], + RESULT_TEXT[racy] + ); + } +} + +stock void PrintToAdmins(const char[] format, any ...) { + char message[100]; + VFormat(message, sizeof(message), format, 2); + for (int x = 1; x <= MaxClients; x++){ + if (IsClientConnected(x) && IsClientInGame(x) && GetUserAdmin(x) != INVALID_ADMIN_ID) { + PrintToChat(x, message); + } + } +} \ No newline at end of file