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 0000000..1db970e Binary files /dev/null and b/plugins/l4d2_rollback.smx differ 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