From ac18fc7a1033b42347ae2fbc0c89de5800b7be78 Mon Sep 17 00:00:00 2001 From: Jackz Date: Sun, 25 Apr 2021 15:58:36 -0500 Subject: [PATCH] Add l4d2_rollback plugin --- README.md | 16 ++- plugins/l4d2_rollback.smx | Bin 0 -> 8671 bytes scripting/l4d2_rollback.sp | 243 +++++++++++++++++++++++++++++++++++++ 3 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 plugins/l4d2_rollback.smx create mode 100644 scripting/l4d2_rollback.sp diff --git a/README.md b/README.md index ec72480..b4f977d 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Useful things: * [l4d2_population_control](#l4d2_population_control) * [l4d2_extrafinaletanks](#l4d2_extrafinaletanks) * [globalbans](#globalbans) +* [l4d2_rollback](#l4d2_rollback) ### Modified Others * [200IQBots_FlyYouFools](#200IQBots_FlyYouFools) @@ -250,4 +251,17 @@ This plugin will automatically spawn an extra amount of tanks (determined by `l4 This plugin will store bans in a database and read from it on connect. This allows you to easily have bans global between servers. It will automatically intercept any ban that calls OnBanIdentity or OnBanClient (so sm_ban will work normally) * **Convars:** - * `sm_hKickOnDBFailure <0/1>` - Should the plugin kick players if it cannot connect to the database? \ No newline at end of file + * `sm_hKickOnDBFailure <0/1>` - Should the plugin kick players if it cannot connect to the database? + +### l4d2_rollback +An idea that you can either manually or have events (friendly fire, new player joining) trigger saving all the player's states. Then if say, a troll comes and kills you and/or incaps your team, you can just quick restore to exactly the point you were at with the same items, health, etc. + +Currently **in development.** + +Currently auto triggers: +1. On any recent friendly fire (only triggers once per 100 game ticks) +2. Any new player joins (only triggers once per 100 game ticks) + +* **Commands:** + * `sm_sstate` - Initiates a manual save of all player's states + * `sm_rstate ` - Restores the selected player's state. @all for all \ No newline at end of file diff --git a/plugins/l4d2_rollback.smx b/plugins/l4d2_rollback.smx new file mode 100644 index 0000000000000000000000000000000000000000..1db970e2e8d232b050883159e4178213cbedafce GIT binary patch literal 8671 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 z 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 literal 0 HcmV?d00001 diff --git a/scripting/l4d2_rollback.sp b/scripting/l4d2_rollback.sp new file mode 100644 index 0000000..0e5033d --- /dev/null +++ b/scripting/l4d2_rollback.sp @@ -0,0 +1,243 @@ +#pragma semicolon 1 +#pragma newdecls required + +//#define DEBUG + +#define PLUGIN_VERSION "1.0" +#define LAST_FF_TIME_THRESHOLD 100.0 +#define LAST_PLAYER_JOIN_THRESHOLD 120.0 + +#include +#include +#include +#include + +static Handle hRoundRespawn; + +public Plugin myinfo = +{ + name = "L4D2 Rollback", + author = "jackzmc", + description = "", + version = PLUGIN_VERSION, + url = "" +}; + +/* +Allows you to rollback to state, +auto recorded at: player join, or FF event + +*/ +enum struct PlayerState { + int incapState; //0 -> Not incapped, # -> # of incap + bool isAlive; + bool hasKit; + char pillSlotItem[32]; + + int permHealth; + float tempHealth; + + float position[3]; + float angles[3]; +} + +static PlayerState[MAXPLAYERS+1] playerStates; +static bool isHealing[MAXPLAYERS+1]; //Is player healing (self, or other) +static ConVar hMaxIncapCount, hDecayRate; + +static float ZERO_VECTOR[3] = {0.0, 0.0, 0.0}, lastDamageTime, lastSpawnTime; + +public void OnPluginStart() { + EngineVersion g_Game = GetEngineVersion(); + if(g_Game != Engine_Left4Dead2) { + SetFailState("This plugin is for L4D/L4D2 only."); + } + Handle hGameConf = LoadGameConfigFile("left4dhooks.l4d2"); + if (hGameConf != INVALID_HANDLE) { + StartPrepSDKCall(SDKCall_Player); + PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "RoundRespawn"); + hRoundRespawn = EndPrepSDKCall(); + if (hRoundRespawn == INVALID_HANDLE) SetFailState("L4D2_Rollback: RoundRespawn Signature broken"); + + } else { + SetFailState("Could not find gamedata: l4d2_rollback.txt."); + } + + hMaxIncapCount = FindConVar("survivor_max_incapacitated_count"); + hDecayRate = FindConVar("pain_pills_decay_rate"); + + HookEvent("heal_begin", Event_HealBegin); + HookEvent("heal_end", Event_HealStop); + + HookEvent("player_first_spawn", Event_PlayerFirstSpawn); + HookEvent("player_hurt", Event_PlayerHurt); + + RegAdminCmd("sm_sstate", Command_SaveGlobalState, ADMFLAG_ROOT, "Saves all players state"); + RegAdminCmd("sm_rstate", Command_RestoreState, ADMFLAG_ROOT, "Restores a certain player's state"); +} + +// ///////////////////////////////////////////////////////////////////////////// +// COMMANDS +// ///////////////////////////////////////////////////////////////////////////// + +public Action Command_SaveGlobalState(int client, int args) { + RecordGlobalState(); + ReplyToCommand(client, "Saved global state."); +} +public Action Command_RestoreState(int client, int args) { + if(args < 1) { + ReplyToCommand(client, "Usage: sm_srestore "); + }else{ + char arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + char target_name[MAX_TARGET_LENGTH]; + int target_list[MAXPLAYERS], target_count; + bool tn_is_ml; + if ((target_count = ProcessTargetString( + arg1, + client, + target_list, + MAXPLAYERS, + COMMAND_FILTER_CONNECTED, + target_name, + sizeof(target_name), + tn_is_ml)) <= 0) + { + /* This function replies to the admin with a failure message */ + ReplyToTargetError(client, target_count); + return Plugin_Handled; + } + for (int i = 0; i < target_count; i++) { + int target = target_list[i]; + if(IsClientConnected(target) && IsClientInGame(target) && GetClientTeam(target) == 2) { + RestoreState(target); + //ReplyToCommand(client, "Restored %N's state", target); + } + } + } + return Plugin_Handled; +} + +// ///////////////////////////////////////////////////////////////////////////// +// EVENTS +// ///////////////////////////////////////////////////////////////////////////// + +public Action Event_PlayerFirstSpawn(Event event, const char[] name, bool dontBroadcast) { + float time = GetGameTime(); + if(time - lastSpawnTime >= LAST_PLAYER_JOIN_THRESHOLD) { + RecordGlobalState(); + PrintToConsoleAll("[Rollback] Saving global state."); + lastSpawnTime = time; + } +} + +void OnClientDisconnect(int client) { + for(int i = 1; i <= MaxClients; i++) { + if(IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) == 2) { + playerStates[i].incapState = 0;//TODO: get incap state + playerStates[i].wasKilled = false; + players[i].pillSlotItem[0] = '\0'; + playerStates[i].hasKit = false; + playerStates[i].prePermHealth = 0; + //TODO: record temp health + } + } +} + +public Action Event_PlayerHurt(Event event, const char[] name, bool dontBroadcast) { + float currentTime = GetGameTime(); + if(currentTime - lastDamageTime >= LAST_FF_TIME_THRESHOLD) { + int client = GetClientOfUserId(event.GetInt("userid")); + int attackerID = event.GetInt("attacker"); + int damage = event.GetInt("dmg_health"); + PrintToChatAll("PLAYER_HURT | V %N | A #%d | DMG %d", client, attackerID, damage); + if(client && GetClientTeam(client) == 2 && attackerID > 0 && damage > 0) { + int attacker = GetClientOfUserId(attackerID); + if(GetClientTeam(attacker) == 2) { + lastDamageTime = GetGameTime(); + RecordGlobalState(); + PrintToConsoleAll("[Rollback] Saving global state due to FF damage"); + } + } + + } +} + +public Action Event_HealBegin(Event event, const char[] name, bool dontBroadcast) { + int client = GetClientOfUserId(event.GetInt("userid")); + isHealing[client] = true; +} +public Action Event_HealStop(Event event, const char[] name, bool dontBroadcast) { + int client = GetClientOfUserId(event.GetInt("userid")); + isHealing[client] = false; +} + +// ///////////////////////////////////////////////////////////////////////////// +// METHODS +// ///////////////////////////////////////////////////////////////////////////// +void RecordGlobalState() { + char item[32]; + for(int i = 1; i <= MaxClients; i++) { + if(IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) == 2) { + playerStates[i].incapState = GetEntProp(i, Prop_Send, "m_currentReviveCount"); + playerStates[i].isAlive = IsPlayerAlive(i); + GetClientWeaponName(i, 3, item, sizeof(item)); + playerStates[i].hasKit = StrEqual(item, "weapon_first_aid_kit"); + GetClientWeaponName(i, 4, playerStates[i].pillSlotItem, 32); + + playerStates[i].permHealth = GetClientHealth(i); + playerStates[i].tempHealth = GetClientHealthBuffer(i); + + + GetClientAbsOrigin(i, playerStates[i].position); + GetClientAbsAngles(i, playerStates[i].angles); + + } + } +} + +void RestoreState(int client) { + char item[32]; + bool isIncapped = GetEntProp(client, Prop_Send, "m_isIncapacitated") == 1; + + bool respawned = false; + if(!IsPlayerAlive(client) && playerStates[client].isAlive) { + SDKCall(hRoundRespawn, client); + RequestFrame(Frame_Teleport, client); + respawned = true; + }else if(isIncapped) { + CheatCommand(client, "give", "health", ""); + TeleportEntity(client, playerStates[client].position, playerStates[client].angles, ZERO_VECTOR); + } + SetEntProp(client, Prop_Send, "m_currentReviveCount", playerStates[client].incapState); + SetEntProp(client, Prop_Send, "m_bIsOnThirdStrike", playerStates[client].incapState >= hMaxIncapCount.IntValue); + SetEntProp(client, Prop_Send, "m_isGoingToDie", playerStates[client].incapState >= hMaxIncapCount.IntValue); + + if(!respawned) { + GetClientWeaponName(client, 3, item, sizeof(item)); + if(playerStates[client].hasKit && !StrEqual(item, "weapon_first_aid_kit") && !isHealing[client]) { + CheatCommand(client, "give", "first_aid_kit", ""); + } + GetClientWeaponName(client, 4, item, sizeof(item)); + if(!StrEqual(playerStates[client].pillSlotItem, item)) { + CheatCommand(client, "give", item, ""); + } + } + SetEntProp(client, Prop_Send, "m_iHealth", playerStates[client].permHealth); + SetEntPropFloat(client, Prop_Send, "m_healthBuffer", playerStates[client].tempHealth); + SetEntPropFloat(client, Prop_Send, "m_healthBufferTime", GetGameTime()); +} + +float GetClientHealthBuffer(int client, float defaultVal=0.0) { + // https://forums.alliedmods.net/showpost.php?p=1365630&postcount=1 + static float healthBuffer, healthBufferTime, tempHealth; + healthBuffer = GetEntPropFloat(client, Prop_Send, "m_healthBuffer"); + healthBufferTime = GetGameTime() - GetEntPropFloat(client, Prop_Send, "m_healthBufferTime"); + tempHealth = healthBuffer - (healthBufferTime / (1.0 / hDecayRate.FloatValue)); + return tempHealth < 0.0 ? defaultVal : tempHealth; +} + +public void Frame_Teleport(int client) { + TeleportEntity(client, playerStates[client].position, playerStates[client].angles, ZERO_VECTOR); +} \ No newline at end of file