From 54c35f46e41e34fed0ea793a8f21c705cca70d21 Mon Sep 17 00:00:00 2001 From: Jackz Date: Wed, 18 Oct 2023 08:50:05 -0500 Subject: [PATCH] Update director --- scripting/include/epi/director.sp | 96 +++++--- scripting/l4d2_extraplayeritems.sp | 372 ++++++++++++++++++----------- 2 files changed, 289 insertions(+), 179 deletions(-) diff --git a/scripting/include/epi/director.sp b/scripting/include/epi/director.sp index 0318942..feb5fb3 100644 --- a/scripting/include/epi/director.sp +++ b/scripting/include/epi/director.sp @@ -5,21 +5,20 @@ #define DIRECTOR_WITCH_CHECK_TIME 30.0 // How often to check if a witch should be spawned #define DIRECTOR_WITCH_MAX_WITCHES 5 // The maximum amount of extra witches to spawn #define DIRECTOR_WITCH_ROLLS 4 // The number of dice rolls, increase if you want to increase freq -#define DIRECTOR_MIN_SPAWN_TIME 12.0 // Possibly randomized, per-special -#define DIRECTOR_SPAWN_CHANCE 0.05 // The raw chance of a spawn +#define DIRECTOR_MIN_SPAWN_TIME 13.0 // Possibly randomized, per-special +#define DIRECTOR_SPAWN_CHANCE 0.04 // The raw chance of a spawn #define DIRECTOR_CHANGE_LIMIT_CHANCE 0.05 // The chance that the maximum amount per-special is changed #define DIRECTOR_SPECIAL_TANK_CHANCE 0.05 // The chance that specials can spawn when a tank is active #define DIRECTOR_STRESS_CUTOFF 0.75 // The minimum chance a random cut off stress value is chosen [this, 1.0] -#define DIRECTOR_REST_CHANCE 0.03 // The chance the director ceases spawning -#define DIRECTOR_REST_MAX_COUNT 10 // The maximum amount of rest given (this * DIRECTOR_TIMER_INTERVAL) +#define DIRECTOR_REST_CHANCE 0.04 // The chance the director ceases spawning +#define DIRECTOR_REST_MAX_COUNT 8 // The maximum amount of rest given (this * DIRECTOR_TIMER_INTERVAL) #define DIRECTOR_DEBUG_SPAWN 1 // Dont actually spawn /// DEFINITIONS #define NUM_SPECIALS 6 #define TOTAL_NUM_SPECIALS 8 -char SPECIAL_IDS[TOTAL_NUM_SPECIALS+1][] = { - "invalid", +char SPECIAL_IDS[TOTAL_NUM_SPECIALS][] = { "smoker", "boomer", "hunter", @@ -30,14 +29,14 @@ char SPECIAL_IDS[TOTAL_NUM_SPECIALS+1][] = { "tank" }; enum specialType { - Special_Smoker = 1, - Special_Boomer = 2, - Special_Hunter = 3, - Special_Spitter = 4, - Special_Jockey = 5, - Special_Charger = 6, - Special_Witch = 7, - Special_Tank = 8, + Special_Smoker = 0, + Special_Boomer = 1, + Special_Hunter = 2, + Special_Spitter = 3, + Special_Jockey = 4, + Special_Charger = 5, + Special_Witch = 6, + Special_Tank = 7, }; enum directorState { DState_Normal, @@ -84,7 +83,7 @@ void Director_OnMapStart() { InitExtraWitches(); } float time = GetGameTime(); - for(int i = 1; i <= TOTAL_NUM_SPECIALS; i++) { + for(int i = 0; i < TOTAL_NUM_SPECIALS; i++) { g_lastSpawnTime[i] = time; g_spawnLimit[i] = 1; g_spawnCount[i] = 0; @@ -123,10 +122,18 @@ void Director_CheckClient(int client) { if(IsClientConnected(client) && GetClientTeam(client) == 3) { // To bypass director limits many plugins spawn an infected "bot" that immediately gets kicked, which allows a window to spawn a special // The fake bot's class is usually 9, an invalid - int class = GetEntProp(client, Prop_Send, "m_zombieClass"); + int class = GetEntProp(client, Prop_Send, "m_zombieClass") - 1; if(class > view_as(Special_Tank)) { return; + } else if(IsFakeClient(client)) { + // Sometimes the bot class is _not_ invalid, but usually has BOT in its name. Ignore those players. + char name[32]; + GetClientName(client, name, sizeof(name)); + if(StrContains(name, "bot", false) != -1) { + return; + } } + if(IsFakeClient(client) && class == view_as(Special_Tank)) { OnTankBotSpawn(client); } @@ -143,6 +150,8 @@ void Director_CheckClient(int client) { static int g_newTankHealth = 0; void OnTankBotSpawn(int client) { if(!IsEPIActive() || !(cvEPISpecialSpawning.IntValue & 4)) return; + // Only run on 6+ survivors + if(g_realSurvivorCount < 6) return; if(g_finaleStage == Stage_FinaleTank2) { if(hExtraFinaleTank.IntValue > 0 && g_extraFinaleTankEnabled) { float duration = GetRandomFloat(EXTRA_TANK_MIN_SEC, EXTRA_TANK_MAX_SEC); @@ -151,7 +160,7 @@ void OnTankBotSpawn(int client) { } } else if(g_newTankHealth > 0) { // A split tank has spawned, set its health - PrintDebug(DEBUG_SPAWNLOGIC, "OnTankBotSpawn: split tank spawned, setting health", g_newTankHealth); + PrintDebug(DEBUG_SPAWNLOGIC, "OnTankBotSpawn: split tank spawned, setting health to %d", g_newTankHealth); SetEntProp(client, Prop_Send, "m_iHealth", g_newTankHealth); g_newTankHealth = 0; } else { @@ -159,15 +168,15 @@ void OnTankBotSpawn(int client) { int health = GetEntProp(client, Prop_Send, "m_iHealth"); float additionalHealth = float(g_survivorCount - 4) * cvEPITankHealth.FloatValue; health += RoundFloat(additionalHealth); - PrintDebug(DEBUG_SPAWNLOGIC, "OnTankBotSpawn: Setting tank health to %d", health); if(hExtraFinaleTank.IntValue & 1 && GetURandomFloat() <= hSplitTankChance.FloatValue) { float duration = GetRandomFloat(EXTRA_TANK_MIN_SEC, EXTRA_TANK_MAX_SEC); int splitHealth = health / 2; - PrintDebug(DEBUG_SPAWNLOGIC, "OnTankBotSpawn: split tank in %.1fs, health=%d", duration, g_newTankHealth); + PrintDebug(DEBUG_SPAWNLOGIC, "OnTankBotSpawn: split tank in %.1fs, health=%d", duration, splitHealth); CreateTimer(duration, Timer_SpawnSplitTank, splitHealth); SetEntProp(client, Prop_Send, "m_iHealth", splitHealth); } else { + PrintDebug(DEBUG_SPAWNLOGIC, "OnTankBotSpawn: Setting tank health to %d", health); SetEntProp(client, Prop_Send, "m_iHealth", health); } } @@ -184,7 +193,7 @@ void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast) { if(client > 0) { int team = GetClientTeam(client); if(team == 3) { - int class = GetEntProp(client, Prop_Send, "m_zombieClass"); + int class = GetEntProp(client, Prop_Send, "m_zombieClass") - 1; if(class > view_as(Special_Tank)) return; g_spawnCount[class]--; if(g_spawnCount[class] < 0) { @@ -208,6 +217,17 @@ void Event_PlayerIncapped(Event event, const char[] name, bool dontBroadcast) { } /// METHODS + +/* +Extra Witch Algo: +On map start, knowing # of total players, compute a random number of witches. +The random number calculated by DiceRoll with 2 rolls and biased to the left. [min, 6] +The minimum number in the dice is shifted to the right by the # of players (abmExtraCount-4)/4 (1 extra=0, 10 extra=2) + +Then, with the # of witches, as N, calculate N different flow values between [0, L4D2Direct_GetMapMaxFlowDistance()] +Timer_Director then checks if highest flow achieved (never decreases) is >= each flow value, if one found, a witch is spawned +(the witch herself is not spawned at the flow, just her spawning is triggered) +*/ void InitExtraWitches() { float flowMax = L4D2Direct_GetMapMaxFlowDistance() - FLOW_CUTOFF; // Just in case we don't have max flow or the map is extremely tiny, don't run: @@ -220,9 +240,10 @@ void InitExtraWitches() { // TODO: max based on count int max = RoundToFloor(float(count) / 4.0); + // TODO: inc chance based on map max flow g_extraWitchCount = DiceRoll(min, DIRECTOR_WITCH_MAX_WITCHES, DIRECTOR_WITCH_ROLLS, BIAS_LEFT); PrintDebug(DEBUG_SPAWNLOGIC, "InitExtraWitches: %d witches (min=%d, max=%d, rolls=%d) checkInterval=%f", g_extraWitchCount, min, max, DIRECTOR_WITCH_ROLLS, DIRECTOR_WITCH_CHECK_TIME); - for(int i = 0; i <= g_extraWitchCount; i++) { + for(int i = 0; i < g_extraWitchCount; i++) { g_extraWitchFlowPositions[i] = GetURandomFloat() * (flowMax-FLOW_CUTOFF) + FLOW_CUTOFF; PrintDebug(DEBUG_SPAWNLOGIC, "Witch position #%d: flow %.2f (%.0f%%)", i, g_extraWitchFlowPositions[i], g_extraWitchFlowPositions[i] / flowMax); } @@ -244,17 +265,17 @@ void Director_PrintDebug(int client) { char buffer[128]; float time = GetGameTime(); PrintToConsole(client, "Last Spawn Deltas: (%.1f s) (min %f)", time - g_lastSpecialSpawnTime, DIRECTOR_MIN_SPAWN_TIME); - for(int i = 1; i <= TOTAL_NUM_SPECIALS; i++) { + for(int i = 0; i < TOTAL_NUM_SPECIALS; i++) { Format(buffer, sizeof(buffer), "%s %s=%.1f", buffer, SPECIAL_IDS[i], time-g_lastSpawnTime[i]); } PrintToConsole(client, "\t%s", buffer); buffer[0] = '\0'; PrintToConsole(client, "Spawn Counts: (%d/%d)", g_infectedCount, g_survivorCount - 4); - for(int i = 1; i <= TOTAL_NUM_SPECIALS; i++) { + for(int i = 0; i < TOTAL_NUM_SPECIALS; i++) { Format(buffer, sizeof(buffer), "%s %s=%d/%d", buffer, SPECIAL_IDS[i], g_spawnCount[i], g_spawnLimit[i]); } PrintToConsole(client, "\t%s", buffer); - PrintToConsole(client, "timer interval=%.0f, rest count=%d", DIRECTOR_TIMER_INTERVAL, g_restCount); + PrintToConsole(client, "timer interval=%.0f, rest count=%d, rest time left=%.0fs", DIRECTOR_TIMER_INTERVAL, g_restCount, float(g_restCount) * DIRECTOR_TIMER_INTERVAL); } void Director_RandomizeLimits() { @@ -268,7 +289,7 @@ void Director_RandomizeLimits() { void Director_RandomizeThings() { g_maxStressIntensity = GetRandomFloat(DIRECTOR_STRESS_CUTOFF, 1.0); g_minFlowSpawn = GetRandomFloat(FLOW_CUTOFF, FLOW_CUTOFF * 2); - + Director_RandomizeLimits(); } bool Director_ShouldRest() { @@ -291,14 +312,14 @@ void TryGrantRest() { // Little hacky, need to track when one leaves instead void Director_CheckSpawnCounts() { if(!IsEPIActive()) return; - for(int i = 1; i <= TOTAL_NUM_SPECIALS; i++) { + for(int i = 0; i < TOTAL_NUM_SPECIALS; i++) { g_spawnCount[i] = 0; } g_infectedCount = 0; for(int i = 1; i <= MaxClients; i++) { if(IsClientInGame(i) && GetClientTeam(i) == 3) { int class = GetEntProp(i, Prop_Send, "m_zombieClass") - 1; // make it 0-based - if(class == 8) continue; + if(class > view_as(Special_Tank)) continue; g_spawnCount[class]++; g_infectedCount++; } @@ -329,7 +350,13 @@ directorState Director_Think() { // TODO: scaling chance, low chance when hitting g_infectedCount, higher on 0 if(g_highestFlowAchieved < g_minFlowSpawn || ~cvEPISpecialSpawning.IntValue & 1) return DState_PendingMinFlowOrDisabled; + // Check if a rest period is given + if(Director_ShouldRest()) { + return DState_Resting; + } + // Only spawn more than one special within 2s at 10% + // TODO: randomized time between spawns? 0, ?? instead of repeat timer? if(time - g_lastSpecialSpawnTime < 2.0 && GetURandomFloat() > 0.5) return DState_MaxSpecialTime; if(GetURandomFloat() < DIRECTOR_CHANGE_LIMIT_CHANCE) { @@ -340,15 +367,12 @@ directorState Director_Think() { // abmExtraCount=6 g_infectedCount=0 chance=1.0 ((abmExtraCount-g_infectedCount)/abmExtraCount) // abmExtraCount=6 g_infectedCount=1 chance=0.9 ((6-1)/6)) = (5/6) // abmExtraCount=6 g_infectedCount=6 chance=0.2 + // TODO: in debug calculate this float eCount = float(g_survivorCount - 3); float chance = (eCount - float(g_infectedCount)) / eCount; // TODO: verify (abmExtraCount-4) if(GetURandomFloat() > chance) return DState_PlayerChance; - // Check if a rest period is given - if(Director_ShouldRest()) { - return DState_Resting; - } float curAvgStress = L4D_GetAvgSurvivorIntensity(); // Don't spawn specials when tanks active, but have a small chance (DIRECTOR_SPECIAL_TANK_CHANCE) to bypass @@ -409,12 +433,14 @@ void DirectorSpawn(specialType special, int player = -1) { CreateTimer(0.1, Timer_Kick, bot); } } - // TODO: dont use z_spawn_old, spawns too close!! float pos[3]; - if(L4D_GetRandomPZSpawnPosition(player, view_as(special), 10, pos)) { + if(L4D_GetRandomPZSpawnPosition(player, view_as(special) + 1, 10, pos)) { // They use 1-index - L4D2_SpawnSpecial(view_as(special) + 1, pos, NULL_VECTOR); - g_lastSpawnTime[view_as(special)] = GetGameTime(); + if(special == Special_Tank) { + L4D2_SpawnTank(pos, NULL_VECTOR); + } else { + L4D2_SpawnSpecial(view_as(special) + 1, pos, NULL_VECTOR); + } } } diff --git a/scripting/l4d2_extraplayeritems.sp b/scripting/l4d2_extraplayeritems.sp index c106ab8..8e89d46 100644 --- a/scripting/l4d2_extraplayeritems.sp +++ b/scripting/l4d2_extraplayeritems.sp @@ -23,10 +23,13 @@ #define DEBUG_SPAWNLOGIC 2 #define DEBUG_ANY 3 +#define INV_SAVE_TIME 5.0 // How long after a save request do we actually save. Seconds. +#define MIN_JOIN_TIME 30 // The minimum amount of time after player joins where we can start saving + //Set the debug level -#define DEBUG_LEVEL DEBUG_ANY +#define DEBUG_LEVEL DEBUG_SPAWNLOGIC #define EXTRA_PLAYER_HUD_UPDATE_INTERVAL 0.8 -//Sets abmExtraCount to this value if set +//Sets g_survivorCount to this value if set // #define DEBUG_FORCE_PLAYERS 7 #define FLOW_CUTOFF 500.0 // The cutoff of flow, so that witches / tanks don't spawn in saferooms / starting areas, [0 + FLOW_CUTOFF, MapMaxFlow - FLOW_CUTOFF] @@ -80,8 +83,10 @@ public Plugin myinfo = url = "https://github.com/Jackzmc/sourcemod-plugins" }; -ConVar hExtraItemBasePercentage, hAddExtraKits, hMinPlayers, hUpdateMinPlayers, hMinPlayersSaferoomDoor, hSaferoomDoorWaitSeconds, hSaferoomDoorAutoOpen, hEPIHudState, hExtraFinaleTank, cvDropDisconnectTime, hSplitTankChance, cvFFDecreaseRate, cvZDifficulty, cvEPIHudFlags, cvEPISpecialSpawning, cvEPIGamemodes, hGamemode, cvEPITankHealth; +ConVar hExtraItemBasePercentage, hAddExtraKits, hMinPlayers, hUpdateMinPlayers, hMinPlayersSaferoomDoor, hSaferoomDoorWaitSeconds, hSaferoomDoorAutoOpen, hEPIHudState, hExtraFinaleTank, cvDropDisconnectTime, hSplitTankChance, cvFFDecreaseRate, cvZDifficulty, cvEPIHudFlags, cvEPISpecialSpawning, cvEPIGamemodes, hGamemode, cvEPITankHealth, cvEPIEnabledMode; +ConVar g_ffFactorCvar; int g_extraKitsAmount, g_extraKitsStart, g_saferoomDoorEnt, g_prevPlayerCount; +bool g_forcedSurvivorCount; static int g_currentChapter; bool g_isCheckpointReached, g_isLateLoaded, g_startCampaignGiven, g_isFailureRound, g_areItemsPopulated; static ArrayList g_ammoPacks; @@ -93,6 +98,7 @@ static bool g_isGamemodeAllowed; int g_survivorCount, g_realSurvivorCount; bool g_isFinaleEnding; static bool g_epiEnabled; +bool g_isOfficialMap; bool g_isSpeaking[MAXPLAYERS+1]; @@ -114,9 +120,10 @@ enum State { State_Active } #if defined DEBUG_LEVEL -char StateNames[3][] = { +char StateNames[4][] = { "Empty", "PendingEmpty", + "Pending", "Active" }; #endif @@ -140,6 +147,7 @@ enum struct PlayerData { bool isUnderAttack; //Is the player under attack (by any special) State state; bool hasJoined; + int joinTime; char nameCache[64]; int scrollIndex; @@ -216,7 +224,8 @@ Restore from saved inventory static StringMap weaponMaxClipSizes; static StringMap pInv; - +static int g_lastInvSave[MAXPLAYERS+1]; +static Handle g_saveTimer[MAXPLAYERS+1]; static char HUD_SCRIPT_DATA[] = "eph <- { Fields = { players = { slot = g_ModeScript.HUD_RIGHT_BOT, dataval = \"%s\", flags = g_ModeScript.HUD_FLAG_ALIGN_LEFT | g_ModeScript.HUD_FLAG_TEAM_SURVIVORS | g_ModeScript.HUD_FLAG_NOBG } } }\nHUDSetLayout(eph)\nHUDPlace(g_ModeScript.HUD_RIGHT_BOT,0.78,0.77,0.3,0.3)\ng_ModeScript;"; @@ -304,6 +313,8 @@ public void OnPluginStart() { HookEvent("witch_spawn", Event_WitchSpawn); HookEvent("finale_vehicle_incoming", Event_FinaleVehicleIncoming); + HookEvent("player_bot_replace", Event_PlayerToIdle); + HookEvent("item_pickup", Event_ItemPickup); @@ -322,6 +333,7 @@ public void OnPluginStart() { cvEPISpecialSpawning = CreateConVar("epi_sp_spawning", "2", "Determines what specials are spawned. Add bits together.\n1 = Normal specials\n2 = Witches\n4 = Tanks", FCVAR_NONE, true, 0.0); cvEPITankHealth = CreateConVar("epi_tank_chunkhp", "2500", "The amount of health added to tank, for each extra player", FCVAR_NONE, true, 0.0); cvEPIGamemodes = CreateConVar("epi_gamemodes", "coop,realism,versus", "Gamemodes where plugin is active. Comma-separated", FCVAR_NONE); + cvEPIEnabledMode = CreateConVar("epi_enabled", "1", "Is EPI 5+ spawning (if epi_sp_spawning enabled as well) enabled?\n0=OFF\n1=Auto (Official Maps Only)(5+)\n2=Auto (Any map) (5+)\n3=Forced on", FCVAR_NONE, true, 0.0, true, 3.0); // TODO: hook flags, reset name index / ping mode cvEPIHudFlags.AddChangeHook(Cvar_HudStateChange); cvEPISpecialSpawning.AddChangeHook(Cvar_SpecialSpawningChange); @@ -331,20 +343,6 @@ public void OnPluginStart() { if(hMinPlayers != null) PrintDebug(DEBUG_INFO, "Found convar abm_minplayers"); } - if(g_isLateLoaded) { - for(int i = 1; i <= MaxClients; i++) { - if(IsClientConnected(i) && IsClientInGame(i)) { - if(GetClientTeam(i) == 2) { - SaveInventory(i); - SDKHook(i, SDKHook_WeaponEquip, Event_Pickup); - } - playerData[i].Setup(i); - } - } - UpdateSurvivorCount(); - TryStartHud(); - } - char buffer[16]; cvZDifficulty = FindConVar("z_difficulty"); cvZDifficulty.GetString(buffer, sizeof(buffer)); @@ -356,6 +354,20 @@ public void OnPluginStart() { hGamemode.AddChangeHook(Event_GamemodeChange); Event_GamemodeChange(hGamemode, g_currentGamemode, g_currentGamemode); + + if(g_isLateLoaded) { + for(int i = 1; i <= MaxClients; i++) { + if(IsClientConnected(i) && IsClientInGame(i)) { + if(GetClientTeam(i) == 2) { + SaveInventory(i, true); + SDKHook(i, SDKHook_WeaponEquip, Event_Pickup); + } + playerData[i].Setup(i); + } + } + TryStartHud(); + } + AutoExecConfig(true, "l4d2_extraplayeritems"); @@ -409,8 +421,12 @@ public void OnClientPutInServer(int client) { } public void OnClientDisconnect(int client) { - if(!IsFakeClient(client) && IsClientInGame(client)) - SaveInventory(client); + // For when bots disconnect in saferoom transitions, empty: + if(playerData[client].state == State_PendingEmpty) + playerData[client].state = State_Empty; + + if(!IsFakeClient(client) && IsClientInGame(client) && GetClientTeam(client) == 2) + SaveInventory(client, true); g_isSpeaking[client] = false; } @@ -516,7 +532,8 @@ public void Event_DifficultyChange(ConVar cvar, const char[] oldValue, const cha } else if(StrEqual(newValue, "impossible", false)) { zDifficulty = Difficulty_Expert; } - // Unknown difficulty, silently ignore + g_ffFactorCvar = GetActiveFriendlyFireFactor(); + SetFFFactor(false); } ///////////////////////////////////// @@ -550,11 +567,17 @@ Action Command_EpiVal(int client, int args) { if(args == 0) { PrintToConsole(client, "epiEnabled = %b", g_epiEnabled); PrintToConsole(client, "isGamemodeAllowed = %b", g_isGamemodeAllowed); + PrintToConsole(client, "isOfficialMap = %b", g_isOfficialMap); PrintToConsole(client, "extraKitsAmount = %d", g_extraKitsAmount); PrintToConsole(client, "extraKitsStart = %d", g_extraKitsStart); PrintToConsole(client, "currentChapter = %d", g_currentChapter); PrintToConsole(client, "extraWitchCount = %d", g_extraWitchCount); + PrintToConsole(client, "forcedSurvivorCount = %b", g_forcedSurvivorCount); + PrintToConsole(client, "survivorCount = %d %s", g_survivorCount, g_forcedSurvivorCount ? "(forced)" : ""); + PrintToConsole(client, "realSurvivorCount = %d", g_realSurvivorCount); PrintToConsole(client, "restCount = %d", g_restCount); + PrintToConsole(client, "extraFinaleTankEnabled = %b", g_extraFinaleTankEnabled); + ReplyToCommand(client, "Values printed to console"); return Plugin_Handled; } char arg[32], value[32]; @@ -574,6 +597,14 @@ Action Command_EpiVal(int client, int args) { ValInt(client, "g_extraWitchCount", g_extraWitchCount, value); } else if(StrEqual(arg, "restCount")) { ValInt(client, "g_restCount", g_restCount, value); + } else if(StrEqual(arg, "survivorCount")) { + ValInt(client, "g_survivorCount", g_survivorCount, value); + } else if(StrEqual(arg, "realSurvivorCount")) { + ValInt(client, "g_survivorCount", g_survivorCount, value); + } else if(StrEqual(arg, "forcedSurvivorCount")) { + ValBool(client, "g_forcedSurvivorCount", g_forcedSurvivorCount, value); + } else if(StrEqual(arg, "forcedSurvivorCount")) { + ValBool(client, "g_extraFinaleTankEnabled", g_extraFinaleTankEnabled, value); } else { ReplyToCommand(client, "Unknown value"); } @@ -602,7 +633,7 @@ Action Command_SaveInventory(int client, int args) { ReplyToCommand(client, "No player found"); return Plugin_Handled; } - SaveInventory(player); + SaveInventory(player, true); ReplyToCommand(client, "Saved inventory for %N", player); return Plugin_Handled; } @@ -649,6 +680,12 @@ Action Command_SetSurvivorCount(int client, int args) { if(args > 0) { char arg[8]; GetCmdArg(1, arg, sizeof(arg)); + if(arg[0] == 'c') { + g_forcedSurvivorCount = false; + ReplyToCommand(client, "Cleared forced survivor count."); + UpdateSurvivorCount(); + return Plugin_Handled; + } int survivorCount = parseSurvivorCount(arg); int oldSurvivorCount = g_survivorCount; if(survivorCount == -1) { @@ -669,7 +706,8 @@ Action Command_SetSurvivorCount(int client, int args) { g_realSurvivorCount = survivorCount; } g_survivorCount = survivorCount; - ReplyToCommand(client, "Changed survivor count %d -> %d", oldSurvivorCount, survivorCount); + g_forcedSurvivorCount = true; + ReplyToCommand(client, "Forced survivor count %d -> %d", oldSurvivorCount, survivorCount); } else { ReplyToCommand(client, "Survivor Count = %d | Real Survivor Count = %d", g_survivorCount, g_realSurvivorCount); } @@ -702,7 +740,7 @@ Action Command_ToggleDoorLocks(int client, int args) { } Action Command_GetKitAmount(int client, int args) { - ReplyToCommand(client, "Extra kits available: %d (%d) | Survivors: %d", g_extraKitsAmount, g_extraKitsStart, GetSurvivorsCount()); + ReplyToCommand(client, "Extra kits available: %d (%d) | Survivors: %d", g_extraKitsAmount, g_extraKitsStart, g_survivorCount); ReplyToCommand(client, "isCheckpointReached %b, g_isLateLoaded %b, firstGiven %b", g_isCheckpointReached, g_isLateLoaded, g_startCampaignGiven); return Plugin_Handled; } @@ -764,7 +802,16 @@ Action Command_DebugStats(int client, int args) { ///////////////////////////////////// /// EVENTS //////////////////////////////////// - +void OnTakeDamageAlivePost(int victim, int attacker, int inflictor, float damage, int damagetype) { + if(GetClientTeam(victim) == 2 && !IsFakeClient(victim)) + SaveInventory(victim); +} +void Event_PlayerToIdle(Event event, const char[] name, bool dontBroadcast) { + int bot = GetClientOfUserId(event.GetInt("bot")); + int client = GetClientOfUserId(event.GetInt("player")); + if(GetClientTeam(client) != 2) return; + PrintToServer("%N -> idle %N", client, bot); +} public Action L4D2_OnChangeFinaleStage(int &finaleType, const char[] arg) { if(finaleType == FINALE_STARTED && g_realSurvivorCount > 4) { g_finaleStage = Stage_FinaleActive; @@ -796,7 +843,7 @@ void Event_TankSpawn(Event event, const char[] name, bool dontBroadcast) { } else if(g_finaleStage == Stage_FinaleDuplicatePending) { PrintToConsoleAll("[EPI] Third & final tank spawned"); RequestFrame(Frame_SetExtraTankHealth, user); - } else if(g_finaleStage == Stage_Inactive && g_extraFinaleTankEnabled && hExtraFinaleTank.IntValue & 1 && GetSurvivorsCount() > 6) { + } else if(g_finaleStage == Stage_Inactive && g_extraFinaleTankEnabled && hExtraFinaleTank.IntValue & 1 && g_survivorCount > 6) { g_finaleStage = Stage_TankSplit; if(GetRandomFloat() <= hSplitTankChance.FloatValue) { // Half their HP, assign half to self and for next tank @@ -872,6 +919,7 @@ void Event_PlayerFirstSpawn(Event event, const char[] name, bool dontBroadcast) int userid = event.GetInt("userid"); int client = GetClientOfUserId(userid); if(GetClientTeam(client) != 2) return; + UpdateSurvivorCount(); if(IsFakeClient(client)) { // Ignore any 'BOT' bots (ABMBot, etc), they are temporarily char classname[32]; @@ -893,15 +941,17 @@ void Event_PlayerFirstSpawn(Event event, const char[] name, bool dontBroadcast) // Make the (real) player invincible as well: CreateTimer(1.5, Timer_RemoveInvincibility, userid); SDKHook(client, SDKHook_OnTakeDamage, OnInvincibleDamageTaken); + SDKHook(client, SDKHook_OnTakeDamageAlivePost, OnTakeDamageAlivePost); playerData[client].state = State_Active; + playerData[client].joinTime = GetTime(); + if(L4D_IsFirstMapInScenario() && !g_startCampaignGiven) { // Players are joining the campaign, but not all clients are ready yet. Once a client is ready, we will give the extra players their items if(AreAllClientsReady()) { - UpdateSurvivorCount(); + g_startCampaignGiven = true; if(g_realSurvivorCount > 4) { PrintToServer("[EPI] First chapter kits given"); - g_startCampaignGiven = true; //Set the initial value ofhMinPlayers PopulateItems(); CreateTimer(1.0, Timer_GiveKits); @@ -910,7 +960,6 @@ void Event_PlayerFirstSpawn(Event event, const char[] name, bool dontBroadcast) } } else { // New client has connected, late on the first chapter or on any other chapter - UpdateSurvivorCount(); // If 5 survivors, then set them up, TP them. if(g_realSurvivorCount > 4) { CreateTimer(0.1, Timer_SetupNewClient, userid); @@ -924,18 +973,21 @@ void Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast) { int userid = event.GetInt("userid"); int client = GetClientOfUserId(userid); + if(GetClientTeam(client) != 2) return; UpdateSurvivorCount(); - if(GetClientTeam(client) == 2 && !IsFakeClient(client) && !L4D_IsFirstMapInScenario()) { + if(!IsFakeClient(client) && !L4D_IsFirstMapInScenario()) { // Start door timeout: - CreateTimer(hSaferoomDoorWaitSeconds.FloatValue, Timer_OpenSaferoomDoor, _, TIMER_FLAG_NO_MAPCHANGE); + if(g_saferoomDoorEnt != INVALID_ENT_REFERENCE) { + CreateTimer(hSaferoomDoorWaitSeconds.FloatValue, Timer_OpenSaferoomDoor, _, TIMER_FLAG_NO_MAPCHANGE); - if(g_prevPlayerCount > 0) { - // Open the door if we hit % percent - float percentIn = float(g_realSurvivorCount) / float(g_prevPlayerCount); - if(percentIn > hMinPlayersSaferoomDoor.FloatValue) + if(g_prevPlayerCount > 0) { + // Open the door if we hit % percent + float percentIn = float(g_realSurvivorCount) / float(g_prevPlayerCount); + if(percentIn > hMinPlayersSaferoomDoor.FloatValue) + UnlockDoor(2); + } else{ UnlockDoor(2); - } else{ - UnlockDoor(2); + } } CreateTimer(0.5, Timer_GiveClientKit, userid); SDKHook(client, SDKHook_WeaponEquip, Event_Pickup); @@ -947,12 +999,14 @@ void Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast) { void Event_PlayerDisconnect(Event event, const char[] name, bool dontBroadcast) { int userid = event.GetInt("userid"); int client = GetClientOfUserId(userid); - if(client > 0 && GetClientTeam(client) == 2) { + if(client > 0 && IsClientInGame(client) && GetClientTeam(client) == 2 && playerData[client].state == State_Active) { playerData[client].hasJoined = false; playerData[client].state = State_PendingEmpty; playerData[client].nameCache[0] = '\0'; PrintToServer("debug: Player (index %d, uid %d) now pending empty", client, client, userid); CreateTimer(cvDropDisconnectTime.FloatValue, Timer_DropSurvivor, client); + if(g_saveTimer[client] != null) + delete g_saveTimer[client]; } } @@ -964,6 +1018,7 @@ void Event_PlayerInfo(Event event, const char[] name, bool dontBroadcast) { } Action Timer_DropSurvivor(Handle h, int client) { + // Check that they are still pending empty (no one replaced them) if(playerData[client].state == State_PendingEmpty) { playerData[client].state = State_Empty; if(hMinPlayers != null) { @@ -985,8 +1040,9 @@ Action Timer_DropSurvivor(Handle h, int client) { void Event_ItemPickup(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); - if(client > 0) { + if(client > 0 && GetClientTeam(client) == 2 && !IsFakeClient(client)) { UpdatePlayerInventory(client); + SaveInventory(client); } } @@ -1015,6 +1071,12 @@ char TIER2_WEAPONS[9][] = { Action Timer_SetupNewClient(Handle h, int userid) { int client = GetClientOfUserId(userid); if(client == 0) return Plugin_Handled; + if(HasSavedInventory(client)) { + PrintDebug(DEBUG_GENERIC, "%N has existing inventory", client); + // TODO: restore + } + + // Incase their bot snagged a kit before we could give them one: if(!DoesClientHaveKit(client)) { int item = GivePlayerItem(client, "weapon_first_aid_kit"); @@ -1049,6 +1111,7 @@ Action Timer_SetupNewClient(Handle h, int userid) { // playerWeapons.PushString(weaponName); } } + wpn = GetPlayerWeaponSlot(i, 1); if(wpn > 0) { GetEdictClassname(wpn, weaponName, sizeof(weaponName)); @@ -1162,6 +1225,12 @@ Action Timer_GiveKits(Handle timer) { } public void OnMapStart() { + char map[5]; + GetCurrentMap(map, sizeof(map)); + // If map starts with c#m#, 98% an official map + if(map[0] == 'c' && IsCharNumeric(map[1]) && (map[2] == 'm' || map[3] == 'm')) { + g_isOfficialMap = true; + } g_isCheckpointReached = false; //If previous round was a failure, restore the amount of kits that were left directly after map transition if(g_isFailureRound) { @@ -1184,7 +1253,7 @@ public void OnMapStart() { g_extraFinaleTankEnabled = false; } - int extraKits = GetSurvivorsCount() - 4; + int extraKits = g_survivorCount - 4; if(extraKits > 0) { // Keep how many extra kits were left after we loaded in, for resetting on failure rounds g_extraKitsAmount += extraKits; @@ -1224,20 +1293,11 @@ public void OnMapStart() { L4D2_RunScript(HUD_SCRIPT_CLEAR); Director_OnMapStart(); if(g_isLateLoaded) { + UpdateSurvivorCount(); g_isLateLoaded = false; } } -/* -Extra Witch Algo: -On map start, knowing # of total players, compute a random number of witches. -The random number calculated by DiceRoll with 2 rolls and biased to the left. [min, 6] -The minimum number in the dice is shifted to the right by the # of players (abmExtraCount-4)/4 (1 extra=0, 10 extra=2) - -Then, with the # of witches, as N, calculate N different flow values between [0, L4D2Direct_GetMapMaxFlowDistance()] -Timer_Director then checks if highest flow achieved (never decreases) is >= each flow value, if one found, a witch is spawned -(the witch herself is not spawned at the flow, just her spawning is triggered) -*/ public void OnConfigsExecuted() { if(hUpdateMinPlayers.BoolValue && hMinPlayers != null) { @@ -1429,7 +1489,7 @@ public Action OnUpgradePackUse(int entity, int activator, int caller, UseType ty clients.Push(activator); ClientCommand(activator, "play player/orch_hit_csharp_short.wav"); - if(clients.Length >= GetSurvivorsCount()) { + if(clients.Length >= g_survivorCount) { AcceptEntityInput(entity, "kill"); delete clients; g_ammoPacks.Erase(index); @@ -1485,13 +1545,13 @@ void UnlockDoor(int flag) { SDKUnhook(entity, SDKHook_Use, Hook_Use); if(hSaferoomDoorAutoOpen.IntValue & flag) { AcceptEntityInput(entity, "Open"); - g_saferoomDoorEnt = INVALID_ENT_REFERENCE; - if(!g_areItemsPopulated) - PopulateItems(); } - + SetVariantString("Unlock"); + AcceptEntityInput(entity, "SetAnimation"); + g_saferoomDoorEnt = INVALID_ENT_REFERENCE; + if(!g_areItemsPopulated) + PopulateItems(); } - } Action Timer_UpdateHud(Handle h) { @@ -1674,11 +1734,63 @@ void DropDroppedInventories() { } } } -void SaveInventory(int client) { - PrintDebug(DEBUG_GENERIC, "Saving inventory for %N", client); +// Used for EPI hud +void UpdatePlayerInventory(int client) { + static char item[16]; + if(GetClientWeaponName(client, 2, item, sizeof(item))) { + items[client].throwable[0] = CharToUpper(item[7]); + if(items[client].throwable[0] == 'V') { + items[client].throwable[0] = 'B'; //Replace [V]omitjar with [B]ile + } + items[client].throwable[1] = '\0'; + } else { + items[client].throwable[0] = '\0'; + } + + if(GetClientWeaponName(client, 3, item, sizeof(item))) { + items[client].usable[0] = CharToUpper(item[7]); + items[client].usable[1] = '\0'; + if(items[client].throwable[0] == 'F') { + items[client].throwable[0] = '+'; //Replace [V]omitjar with [B]ile + } + } else { + items[client].usable[0] = '-'; + items[client].usable[1] = '\0'; + } + + if(GetClientWeaponName(client, 4, item, sizeof(item))) { + items[client].consumable[0] = CharToUpper(item[7]); + items[client].consumable[1] = '\0'; + } else { + items[client].consumable[0] = '\0'; + } +} + +Action Timer_SaveInventory(Handle h, int userid) { + int client = GetClientOfUserId(userid); + if(client > 0) { + // Force save to bypass our timeout + g_saveTimer[client] = null; + SaveInventory(client, true); + } + return Plugin_Stop; +} + +void SaveInventory(int client, bool force = false) { + // TODO: dont save during join time + int time = GetTime(); + if(!force) { + if(time - playerData[client].joinTime < MIN_JOIN_TIME) return; + // Queue their inventory to be saved after a timeout. + // Any time a save happens between prev save and timeout will delay the timeout. + // This should ensure that the saved inventory is most of the time up-to-date + if(g_saveTimer[client] != null) + delete g_saveTimer[client]; + g_saveTimer[client] = CreateTimer(INV_SAVE_TIME, Timer_SaveInventory, GetClientUserId(client)); + } PlayerInventory inventory; - inventory.timestamp = GetTime(); - inventory.isAlive = IsClientInGame(client) && IsPlayerAlive(client); + inventory.timestamp = time; + inventory.isAlive = IsPlayerAlive(client); playerData[client].state = State_Active; GetClientAbsOrigin(client, inventory.location); @@ -1697,6 +1809,7 @@ void SaveInventory(int client) { GetClientAuthId(client, AuthId_Steam3, buffer, sizeof(buffer)); pInv.SetArray(buffer, inventory, sizeof(inventory)); + g_lastInvSave[client] = GetTime(); } void RestoreInventory(int client, PlayerInventory inventory) { @@ -1737,6 +1850,12 @@ bool GetInventory(const char[] steamid, PlayerInventory inventory) { return pInv.GetArray(steamid, inventory, sizeof(inventory)); } +bool HasSavedInventory(int client) { + char buffer[32]; + GetClientAuthId(client, AuthId_Steam3, buffer, sizeof(buffer)); + return pInv.ContainsKey(buffer); +} + bool DoesInventoryDiffer(int client) { static char buffer[32]; GetClientAuthId(client, AuthId_Steam3, buffer, sizeof(buffer)); @@ -1753,47 +1872,69 @@ bool DoesInventoryDiffer(int client) { return currentPrimary != storedPrimary || currentSecondary != storedSecondary; } +// TODO: disable by cvar as well (replace abm_autohard) bool IsEPIActive() { return g_epiEnabled; } - +/* +[Debug] UpdateSurvivorCount: total=4 real=4 active=4 +[Debug] UpdateSurvivorCount: total=4 real=4 active=4 +Player no longer idle +[Debug] UpdateSurvivorCount: total=5 real=4 active=5 +[Debug] UpdateSurvivorCount: total=4 real=4 active=4 +Player no longer idle +*/ void UpdateSurvivorCount() { + #if defined DEBUG_FORCE_PLAYERS + g_survivorCount = DEBUG_FORCE_PLAYERS; + g_realSurvivorCount = DEBUG_FORCE_PLAYERS; + g_epiEnabled = g_realSurvivorCount > 4 && g_isGamemodeAllowed; + return; + #endif + if(g_forcedSurvivorCount) return; // Don't update if forced int countTotal = 0, countReal = 0, countActive = 0; - #if !defined DEBUG_FORCE_PLAYERS for(int i = 1; i <= MaxClients; i++) { if(IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) == 2) { - if(!IsFakeClient(i)) { + // Count idle player's bots as well + if(!IsFakeClient(i) || L4D_GetIdlePlayerOfBot(i) > 0) { countReal++; } + // FIXME: counting idle players for a brief tick countTotal++; if(playerData[i].state == State_Active) { countActive++; } } } - g_survivorCount = countTotal; + g_survivorCount = countTotal; g_realSurvivorCount = countReal; PrintDebug(DEBUG_GENERIC, "UpdateSurvivorCount: total=%d real=%d active=%d", countTotal, countReal, countActive); - #endif - #if defined DEBUG_FORCE_PLAYERS - g_survivorCount = DEBUG_FORCE_PLAYERS; - g_realSurvivorCount = DEBUG_FORCE_PLAYERS; - #endif - - if(g_survivorCount > 4) { - // Update friendly fire values to reduce accidental FF in crowded corridors - ConVar friendlyFireFactor = GetActiveFriendlyFireFactor(); - // TODO: Get previous default - friendlyFireFactor.FloatValue = friendlyFireFactor.FloatValue - ((g_realSurvivorCount - 4) * cvFFDecreaseRate.FloatValue); - if(friendlyFireFactor.FloatValue < 0.0) { - friendlyFireFactor.FloatValue = 0.01; - } - g_epiEnabled = g_isGamemodeAllowed; - } else { - g_epiEnabled = false; + // Temporarily for now use g_realSurvivorCount, as players joining have a brief second where they are 5 players + + // 1 = 5+ official + // 2 = 5+ any map + // 3 = always on + bool isActive = g_isGamemodeAllowed; + if(isActive && cvEPIEnabledMode.IntValue != 3) { + // Enable only if mode is 2 or is official map AND 5+ + isActive = (g_isOfficialMap || cvEPIEnabledMode.IntValue == 2) && g_realSurvivorCount > 4; } + g_epiEnabled = isActive; + SetFFFactor(g_epiEnabled); +} - // TODO: update hMinPlayers +void SetFFFactor(bool enabled) { + static float prevValue; + // Restore the previous value (we use the value for the calculations of new value) + if(g_ffFactorCvar == null) return; // Ignore invalid difficulties + g_ffFactorCvar.FloatValue = prevValue; + if(enabled) { + prevValue = g_ffFactorCvar.FloatValue; + g_ffFactorCvar.FloatValue = g_ffFactorCvar.FloatValue - ((g_realSurvivorCount - 4) * cvFFDecreaseRate.FloatValue); + if(g_ffFactorCvar.FloatValue < 0.01) { + g_ffFactorCvar.FloatValue = 0.01; + } + } } stock int FindFirstSurvivor() { @@ -1822,33 +1963,6 @@ stock void GiveStartingKits() { } } -stock int GetSurvivorsCount() { - #if defined DEBUG_FORCE_PLAYERS - return DEBUG_FORCE_PLAYERS; - #endif - int count = 0; - for(int i = 1; i <= MaxClients; i++) { - if(IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) == 2) { - ++count; - } - } - return count; -} - -stock int GetRealSurvivorsCount() { - #if defined DEBUG_FORCE_PLAYERS - return DEBUG_FORCE_PLAYERS; - #endif - int count = 0; - for(int i = 1; i <= MaxClients; i++) { - if(IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) == 2) { - if(IsFakeClient(i) && HasEntProp(i, Prop_Send, "m_humanSpectatorUserID") && GetEntProp(i, Prop_Send, "m_humanSpectatorUserID") == 0) continue; - ++count; - } - } - return count; -} - stock bool AreAllClientsReady() { for(int i = 1; i <= MaxClients; i++) { if(IsClientConnected(i) && !IsClientInGame(i)) { @@ -1859,9 +1973,10 @@ stock bool AreAllClientsReady() { } stock bool DoesClientHaveKit(int client) { - char wpn[32]; - if(IsClientConnected(client) && IsClientInGame(client) && GetClientWeaponName(client, 3, wpn, sizeof(wpn))) { - return StrEqual(wpn, "weapon_first_aid_kit"); + if(IsClientConnected(client) && IsClientInGame(client)) { + char wpn[32]; + if(GetClientWeaponName(client, 3, wpn, sizeof(wpn))) + return StrEqual(wpn, "weapon_first_aid_kit"); } return false; } @@ -1958,37 +2073,6 @@ int FindCabinetIndex(int cabinetId) { return -1; } -void UpdatePlayerInventory(int client) { - static char item[16]; - if(GetClientWeaponName(client, 2, item, sizeof(item))) { - items[client].throwable[0] = CharToUpper(item[7]); - if(items[client].throwable[0] == 'V') { - items[client].throwable[0] = 'B'; //Replace [V]omitjar with [B]ile - } - items[client].throwable[1] = '\0'; - } else { - items[client].throwable[0] = '\0'; - } - - if(GetClientWeaponName(client, 3, item, sizeof(item))) { - items[client].usable[0] = CharToUpper(item[7]); - items[client].usable[1] = '\0'; - if(items[client].throwable[0] == 'F') { - items[client].throwable[0] = '+'; //Replace [V]omitjar with [B]ile - } - } else { - items[client].usable[0] = '-'; - items[client].usable[1] = '\0'; - } - - if(GetClientWeaponName(client, 4, item, sizeof(item))) { - items[client].consumable[0] = CharToUpper(item[7]); - items[client].consumable[1] = '\0'; - } else { - items[client].consumable[0] = '\0'; - } -} - stock void RunVScriptLong(const char[] sCode, any ...) { static int iScriptLogic = INVALID_ENT_REFERENCE; if(iScriptLogic == INVALID_ENT_REFERENCE || !IsValidEntity(iScriptLogic)) {