Update director

This commit is contained in:
Jackz 2023-10-18 08:50:05 -05:00
parent b0405a36d3
commit 54c35f46e4
No known key found for this signature in database
GPG key ID: E0BBD94CF657F603
2 changed files with 289 additions and 179 deletions

View file

@ -5,21 +5,20 @@
#define DIRECTOR_WITCH_CHECK_TIME 30.0 // How often to check if a witch should be spawned #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_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_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_MIN_SPAWN_TIME 13.0 // Possibly randomized, per-special
#define DIRECTOR_SPAWN_CHANCE 0.05 // The raw chance of a spawn #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_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_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_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_CHANCE 0.04 // 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_MAX_COUNT 8 // The maximum amount of rest given (this * DIRECTOR_TIMER_INTERVAL)
#define DIRECTOR_DEBUG_SPAWN 1 // Dont actually spawn #define DIRECTOR_DEBUG_SPAWN 1 // Dont actually spawn
/// DEFINITIONS /// DEFINITIONS
#define NUM_SPECIALS 6 #define NUM_SPECIALS 6
#define TOTAL_NUM_SPECIALS 8 #define TOTAL_NUM_SPECIALS 8
char SPECIAL_IDS[TOTAL_NUM_SPECIALS+1][] = { char SPECIAL_IDS[TOTAL_NUM_SPECIALS][] = {
"invalid",
"smoker", "smoker",
"boomer", "boomer",
"hunter", "hunter",
@ -30,14 +29,14 @@ char SPECIAL_IDS[TOTAL_NUM_SPECIALS+1][] = {
"tank" "tank"
}; };
enum specialType { enum specialType {
Special_Smoker = 1, Special_Smoker = 0,
Special_Boomer = 2, Special_Boomer = 1,
Special_Hunter = 3, Special_Hunter = 2,
Special_Spitter = 4, Special_Spitter = 3,
Special_Jockey = 5, Special_Jockey = 4,
Special_Charger = 6, Special_Charger = 5,
Special_Witch = 7, Special_Witch = 6,
Special_Tank = 8, Special_Tank = 7,
}; };
enum directorState { enum directorState {
DState_Normal, DState_Normal,
@ -84,7 +83,7 @@ void Director_OnMapStart() {
InitExtraWitches(); InitExtraWitches();
} }
float time = GetGameTime(); 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_lastSpawnTime[i] = time;
g_spawnLimit[i] = 1; g_spawnLimit[i] = 1;
g_spawnCount[i] = 0; g_spawnCount[i] = 0;
@ -123,10 +122,18 @@ void Director_CheckClient(int client) {
if(IsClientConnected(client) && GetClientTeam(client) == 3) { 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 // 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 // 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<int>(Special_Tank)) { if(class > view_as<int>(Special_Tank)) {
return; 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<int>(Special_Tank)) { if(IsFakeClient(client) && class == view_as<int>(Special_Tank)) {
OnTankBotSpawn(client); OnTankBotSpawn(client);
} }
@ -143,6 +150,8 @@ void Director_CheckClient(int client) {
static int g_newTankHealth = 0; static int g_newTankHealth = 0;
void OnTankBotSpawn(int client) { void OnTankBotSpawn(int client) {
if(!IsEPIActive() || !(cvEPISpecialSpawning.IntValue & 4)) return; if(!IsEPIActive() || !(cvEPISpecialSpawning.IntValue & 4)) return;
// Only run on 6+ survivors
if(g_realSurvivorCount < 6) return;
if(g_finaleStage == Stage_FinaleTank2) { if(g_finaleStage == Stage_FinaleTank2) {
if(hExtraFinaleTank.IntValue > 0 && g_extraFinaleTankEnabled) { if(hExtraFinaleTank.IntValue > 0 && g_extraFinaleTankEnabled) {
float duration = GetRandomFloat(EXTRA_TANK_MIN_SEC, EXTRA_TANK_MAX_SEC); float duration = GetRandomFloat(EXTRA_TANK_MIN_SEC, EXTRA_TANK_MAX_SEC);
@ -151,7 +160,7 @@ void OnTankBotSpawn(int client) {
} }
} else if(g_newTankHealth > 0) { } else if(g_newTankHealth > 0) {
// A split tank has spawned, set its health // 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); SetEntProp(client, Prop_Send, "m_iHealth", g_newTankHealth);
g_newTankHealth = 0; g_newTankHealth = 0;
} else { } else {
@ -159,15 +168,15 @@ void OnTankBotSpawn(int client) {
int health = GetEntProp(client, Prop_Send, "m_iHealth"); int health = GetEntProp(client, Prop_Send, "m_iHealth");
float additionalHealth = float(g_survivorCount - 4) * cvEPITankHealth.FloatValue; float additionalHealth = float(g_survivorCount - 4) * cvEPITankHealth.FloatValue;
health += RoundFloat(additionalHealth); health += RoundFloat(additionalHealth);
PrintDebug(DEBUG_SPAWNLOGIC, "OnTankBotSpawn: Setting tank health to %d", health);
if(hExtraFinaleTank.IntValue & 1 && GetURandomFloat() <= hSplitTankChance.FloatValue) { if(hExtraFinaleTank.IntValue & 1 && GetURandomFloat() <= hSplitTankChance.FloatValue) {
float duration = GetRandomFloat(EXTRA_TANK_MIN_SEC, EXTRA_TANK_MAX_SEC); float duration = GetRandomFloat(EXTRA_TANK_MIN_SEC, EXTRA_TANK_MAX_SEC);
int splitHealth = health / 2; 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); CreateTimer(duration, Timer_SpawnSplitTank, splitHealth);
SetEntProp(client, Prop_Send, "m_iHealth", splitHealth); SetEntProp(client, Prop_Send, "m_iHealth", splitHealth);
} else { } else {
PrintDebug(DEBUG_SPAWNLOGIC, "OnTankBotSpawn: Setting tank health to %d", health);
SetEntProp(client, Prop_Send, "m_iHealth", 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) { if(client > 0) {
int team = GetClientTeam(client); int team = GetClientTeam(client);
if(team == 3) { if(team == 3) {
int class = GetEntProp(client, Prop_Send, "m_zombieClass"); int class = GetEntProp(client, Prop_Send, "m_zombieClass") - 1;
if(class > view_as<int>(Special_Tank)) return; if(class > view_as<int>(Special_Tank)) return;
g_spawnCount[class]--; g_spawnCount[class]--;
if(g_spawnCount[class] < 0) { if(g_spawnCount[class] < 0) {
@ -208,6 +217,17 @@ void Event_PlayerIncapped(Event event, const char[] name, bool dontBroadcast) {
} }
/// METHODS /// 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() { void InitExtraWitches() {
float flowMax = L4D2Direct_GetMapMaxFlowDistance() - FLOW_CUTOFF; float flowMax = L4D2Direct_GetMapMaxFlowDistance() - FLOW_CUTOFF;
// Just in case we don't have max flow or the map is extremely tiny, don't run: // 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 // TODO: max based on count
int max = RoundToFloor(float(count) / 4.0); 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); 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); 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; 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); 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]; char buffer[128];
float time = GetGameTime(); float time = GetGameTime();
PrintToConsole(client, "Last Spawn Deltas: (%.1f s) (min %f)", time - g_lastSpecialSpawnTime, DIRECTOR_MIN_SPAWN_TIME); 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]); Format(buffer, sizeof(buffer), "%s %s=%.1f", buffer, SPECIAL_IDS[i], time-g_lastSpawnTime[i]);
} }
PrintToConsole(client, "\t%s", buffer); PrintToConsole(client, "\t%s", buffer);
buffer[0] = '\0'; buffer[0] = '\0';
PrintToConsole(client, "Spawn Counts: (%d/%d)", g_infectedCount, g_survivorCount - 4); 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]); 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, "\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() { void Director_RandomizeLimits() {
@ -268,7 +289,7 @@ void Director_RandomizeLimits() {
void Director_RandomizeThings() { void Director_RandomizeThings() {
g_maxStressIntensity = GetRandomFloat(DIRECTOR_STRESS_CUTOFF, 1.0); g_maxStressIntensity = GetRandomFloat(DIRECTOR_STRESS_CUTOFF, 1.0);
g_minFlowSpawn = GetRandomFloat(FLOW_CUTOFF, FLOW_CUTOFF * 2); g_minFlowSpawn = GetRandomFloat(FLOW_CUTOFF, FLOW_CUTOFF * 2);
Director_RandomizeLimits();
} }
bool Director_ShouldRest() { bool Director_ShouldRest() {
@ -291,14 +312,14 @@ void TryGrantRest() {
// Little hacky, need to track when one leaves instead // Little hacky, need to track when one leaves instead
void Director_CheckSpawnCounts() { void Director_CheckSpawnCounts() {
if(!IsEPIActive()) return; 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_spawnCount[i] = 0;
} }
g_infectedCount = 0; g_infectedCount = 0;
for(int i = 1; i <= MaxClients; i++) { for(int i = 1; i <= MaxClients; i++) {
if(IsClientInGame(i) && GetClientTeam(i) == 3) { if(IsClientInGame(i) && GetClientTeam(i) == 3) {
int class = GetEntProp(i, Prop_Send, "m_zombieClass") - 1; // make it 0-based int class = GetEntProp(i, Prop_Send, "m_zombieClass") - 1; // make it 0-based
if(class == 8) continue; if(class > view_as<int>(Special_Tank)) continue;
g_spawnCount[class]++; g_spawnCount[class]++;
g_infectedCount++; g_infectedCount++;
} }
@ -329,7 +350,13 @@ directorState Director_Think() {
// TODO: scaling chance, low chance when hitting g_infectedCount, higher on 0 // TODO: scaling chance, low chance when hitting g_infectedCount, higher on 0
if(g_highestFlowAchieved < g_minFlowSpawn || ~cvEPISpecialSpawning.IntValue & 1) return DState_PendingMinFlowOrDisabled; 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% // 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(time - g_lastSpecialSpawnTime < 2.0 && GetURandomFloat() > 0.5) return DState_MaxSpecialTime;
if(GetURandomFloat() < DIRECTOR_CHANGE_LIMIT_CHANCE) { 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=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=1 chance=0.9 ((6-1)/6)) = (5/6)
// abmExtraCount=6 g_infectedCount=6 chance=0.2 // abmExtraCount=6 g_infectedCount=6 chance=0.2
// TODO: in debug calculate this
float eCount = float(g_survivorCount - 3); float eCount = float(g_survivorCount - 3);
float chance = (eCount - float(g_infectedCount)) / eCount; float chance = (eCount - float(g_infectedCount)) / eCount;
// TODO: verify (abmExtraCount-4) // TODO: verify (abmExtraCount-4)
if(GetURandomFloat() > chance) return DState_PlayerChance; if(GetURandomFloat() > chance) return DState_PlayerChance;
// Check if a rest period is given
if(Director_ShouldRest()) {
return DState_Resting;
}
float curAvgStress = L4D_GetAvgSurvivorIntensity(); float curAvgStress = L4D_GetAvgSurvivorIntensity();
// Don't spawn specials when tanks active, but have a small chance (DIRECTOR_SPECIAL_TANK_CHANCE) to bypass // 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); CreateTimer(0.1, Timer_Kick, bot);
} }
} }
// TODO: dont use z_spawn_old, spawns too close!!
float pos[3]; float pos[3];
if(L4D_GetRandomPZSpawnPosition(player, view_as<int>(special), 10, pos)) { if(L4D_GetRandomPZSpawnPosition(player, view_as<int>(special) + 1, 10, pos)) {
// They use 1-index // They use 1-index
L4D2_SpawnSpecial(view_as<int>(special) + 1, pos, NULL_VECTOR); if(special == Special_Tank) {
g_lastSpawnTime[view_as<int>(special)] = GetGameTime(); L4D2_SpawnTank(pos, NULL_VECTOR);
} else {
L4D2_SpawnSpecial(view_as<int>(special) + 1, pos, NULL_VECTOR);
}
} }
} }

View file

@ -23,10 +23,13 @@
#define DEBUG_SPAWNLOGIC 2 #define DEBUG_SPAWNLOGIC 2
#define DEBUG_ANY 3 #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 //Set the debug level
#define DEBUG_LEVEL DEBUG_ANY #define DEBUG_LEVEL DEBUG_SPAWNLOGIC
#define EXTRA_PLAYER_HUD_UPDATE_INTERVAL 0.8 #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 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] #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" 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; int g_extraKitsAmount, g_extraKitsStart, g_saferoomDoorEnt, g_prevPlayerCount;
bool g_forcedSurvivorCount;
static int g_currentChapter; static int g_currentChapter;
bool g_isCheckpointReached, g_isLateLoaded, g_startCampaignGiven, g_isFailureRound, g_areItemsPopulated; bool g_isCheckpointReached, g_isLateLoaded, g_startCampaignGiven, g_isFailureRound, g_areItemsPopulated;
static ArrayList g_ammoPacks; static ArrayList g_ammoPacks;
@ -93,6 +98,7 @@ static bool g_isGamemodeAllowed;
int g_survivorCount, g_realSurvivorCount; int g_survivorCount, g_realSurvivorCount;
bool g_isFinaleEnding; bool g_isFinaleEnding;
static bool g_epiEnabled; static bool g_epiEnabled;
bool g_isOfficialMap;
bool g_isSpeaking[MAXPLAYERS+1]; bool g_isSpeaking[MAXPLAYERS+1];
@ -114,9 +120,10 @@ enum State {
State_Active State_Active
} }
#if defined DEBUG_LEVEL #if defined DEBUG_LEVEL
char StateNames[3][] = { char StateNames[4][] = {
"Empty", "Empty",
"PendingEmpty", "PendingEmpty",
"Pending",
"Active" "Active"
}; };
#endif #endif
@ -140,6 +147,7 @@ enum struct PlayerData {
bool isUnderAttack; //Is the player under attack (by any special) bool isUnderAttack; //Is the player under attack (by any special)
State state; State state;
bool hasJoined; bool hasJoined;
int joinTime;
char nameCache[64]; char nameCache[64];
int scrollIndex; int scrollIndex;
@ -216,7 +224,8 @@ Restore from saved inventory
static StringMap weaponMaxClipSizes; static StringMap weaponMaxClipSizes;
static StringMap pInv; 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;"; 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("witch_spawn", Event_WitchSpawn);
HookEvent("finale_vehicle_incoming", Event_FinaleVehicleIncoming); 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); 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); 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); 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 // TODO: hook flags, reset name index / ping mode
cvEPIHudFlags.AddChangeHook(Cvar_HudStateChange); cvEPIHudFlags.AddChangeHook(Cvar_HudStateChange);
cvEPISpecialSpawning.AddChangeHook(Cvar_SpecialSpawningChange); cvEPISpecialSpawning.AddChangeHook(Cvar_SpecialSpawningChange);
@ -331,20 +343,6 @@ public void OnPluginStart() {
if(hMinPlayers != null) PrintDebug(DEBUG_INFO, "Found convar abm_minplayers"); 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]; char buffer[16];
cvZDifficulty = FindConVar("z_difficulty"); cvZDifficulty = FindConVar("z_difficulty");
cvZDifficulty.GetString(buffer, sizeof(buffer)); cvZDifficulty.GetString(buffer, sizeof(buffer));
@ -356,6 +354,20 @@ public void OnPluginStart() {
hGamemode.AddChangeHook(Event_GamemodeChange); hGamemode.AddChangeHook(Event_GamemodeChange);
Event_GamemodeChange(hGamemode, g_currentGamemode, g_currentGamemode); 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"); AutoExecConfig(true, "l4d2_extraplayeritems");
@ -409,8 +421,12 @@ public void OnClientPutInServer(int client) {
} }
public void OnClientDisconnect(int client) { public void OnClientDisconnect(int client) {
if(!IsFakeClient(client) && IsClientInGame(client)) // For when bots disconnect in saferoom transitions, empty:
SaveInventory(client); 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; 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)) { } else if(StrEqual(newValue, "impossible", false)) {
zDifficulty = Difficulty_Expert; 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) { if(args == 0) {
PrintToConsole(client, "epiEnabled = %b", g_epiEnabled); PrintToConsole(client, "epiEnabled = %b", g_epiEnabled);
PrintToConsole(client, "isGamemodeAllowed = %b", g_isGamemodeAllowed); PrintToConsole(client, "isGamemodeAllowed = %b", g_isGamemodeAllowed);
PrintToConsole(client, "isOfficialMap = %b", g_isOfficialMap);
PrintToConsole(client, "extraKitsAmount = %d", g_extraKitsAmount); PrintToConsole(client, "extraKitsAmount = %d", g_extraKitsAmount);
PrintToConsole(client, "extraKitsStart = %d", g_extraKitsStart); PrintToConsole(client, "extraKitsStart = %d", g_extraKitsStart);
PrintToConsole(client, "currentChapter = %d", g_currentChapter); PrintToConsole(client, "currentChapter = %d", g_currentChapter);
PrintToConsole(client, "extraWitchCount = %d", g_extraWitchCount); 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, "restCount = %d", g_restCount);
PrintToConsole(client, "extraFinaleTankEnabled = %b", g_extraFinaleTankEnabled);
ReplyToCommand(client, "Values printed to console");
return Plugin_Handled; return Plugin_Handled;
} }
char arg[32], value[32]; char arg[32], value[32];
@ -574,6 +597,14 @@ Action Command_EpiVal(int client, int args) {
ValInt(client, "g_extraWitchCount", g_extraWitchCount, value); ValInt(client, "g_extraWitchCount", g_extraWitchCount, value);
} else if(StrEqual(arg, "restCount")) { } else if(StrEqual(arg, "restCount")) {
ValInt(client, "g_restCount", g_restCount, value); 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 { } else {
ReplyToCommand(client, "Unknown value"); ReplyToCommand(client, "Unknown value");
} }
@ -602,7 +633,7 @@ Action Command_SaveInventory(int client, int args) {
ReplyToCommand(client, "No player found"); ReplyToCommand(client, "No player found");
return Plugin_Handled; return Plugin_Handled;
} }
SaveInventory(player); SaveInventory(player, true);
ReplyToCommand(client, "Saved inventory for %N", player); ReplyToCommand(client, "Saved inventory for %N", player);
return Plugin_Handled; return Plugin_Handled;
} }
@ -649,6 +680,12 @@ Action Command_SetSurvivorCount(int client, int args) {
if(args > 0) { if(args > 0) {
char arg[8]; char arg[8];
GetCmdArg(1, arg, sizeof(arg)); 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 survivorCount = parseSurvivorCount(arg);
int oldSurvivorCount = g_survivorCount; int oldSurvivorCount = g_survivorCount;
if(survivorCount == -1) { if(survivorCount == -1) {
@ -669,7 +706,8 @@ Action Command_SetSurvivorCount(int client, int args) {
g_realSurvivorCount = survivorCount; g_realSurvivorCount = survivorCount;
} }
g_survivorCount = 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 { } else {
ReplyToCommand(client, "Survivor Count = %d | Real Survivor Count = %d", g_survivorCount, g_realSurvivorCount); 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) { 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); ReplyToCommand(client, "isCheckpointReached %b, g_isLateLoaded %b, firstGiven %b", g_isCheckpointReached, g_isLateLoaded, g_startCampaignGiven);
return Plugin_Handled; return Plugin_Handled;
} }
@ -764,7 +802,16 @@ Action Command_DebugStats(int client, int args) {
///////////////////////////////////// /////////////////////////////////////
/// EVENTS /// 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) { public Action L4D2_OnChangeFinaleStage(int &finaleType, const char[] arg) {
if(finaleType == FINALE_STARTED && g_realSurvivorCount > 4) { if(finaleType == FINALE_STARTED && g_realSurvivorCount > 4) {
g_finaleStage = Stage_FinaleActive; g_finaleStage = Stage_FinaleActive;
@ -796,7 +843,7 @@ void Event_TankSpawn(Event event, const char[] name, bool dontBroadcast) {
} else if(g_finaleStage == Stage_FinaleDuplicatePending) { } else if(g_finaleStage == Stage_FinaleDuplicatePending) {
PrintToConsoleAll("[EPI] Third & final tank spawned"); PrintToConsoleAll("[EPI] Third & final tank spawned");
RequestFrame(Frame_SetExtraTankHealth, user); 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; g_finaleStage = Stage_TankSplit;
if(GetRandomFloat() <= hSplitTankChance.FloatValue) { if(GetRandomFloat() <= hSplitTankChance.FloatValue) {
// Half their HP, assign half to self and for next tank // 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 userid = event.GetInt("userid");
int client = GetClientOfUserId(userid); int client = GetClientOfUserId(userid);
if(GetClientTeam(client) != 2) return; if(GetClientTeam(client) != 2) return;
UpdateSurvivorCount();
if(IsFakeClient(client)) { if(IsFakeClient(client)) {
// Ignore any 'BOT' bots (ABMBot, etc), they are temporarily // Ignore any 'BOT' bots (ABMBot, etc), they are temporarily
char classname[32]; char classname[32];
@ -893,15 +941,17 @@ void Event_PlayerFirstSpawn(Event event, const char[] name, bool dontBroadcast)
// Make the (real) player invincible as well: // Make the (real) player invincible as well:
CreateTimer(1.5, Timer_RemoveInvincibility, userid); CreateTimer(1.5, Timer_RemoveInvincibility, userid);
SDKHook(client, SDKHook_OnTakeDamage, OnInvincibleDamageTaken); SDKHook(client, SDKHook_OnTakeDamage, OnInvincibleDamageTaken);
SDKHook(client, SDKHook_OnTakeDamageAlivePost, OnTakeDamageAlivePost);
playerData[client].state = State_Active; playerData[client].state = State_Active;
playerData[client].joinTime = GetTime();
if(L4D_IsFirstMapInScenario() && !g_startCampaignGiven) { 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 // 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()) { if(AreAllClientsReady()) {
UpdateSurvivorCount(); g_startCampaignGiven = true;
if(g_realSurvivorCount > 4) { if(g_realSurvivorCount > 4) {
PrintToServer("[EPI] First chapter kits given"); PrintToServer("[EPI] First chapter kits given");
g_startCampaignGiven = true;
//Set the initial value ofhMinPlayers //Set the initial value ofhMinPlayers
PopulateItems(); PopulateItems();
CreateTimer(1.0, Timer_GiveKits); CreateTimer(1.0, Timer_GiveKits);
@ -910,7 +960,6 @@ void Event_PlayerFirstSpawn(Event event, const char[] name, bool dontBroadcast)
} }
} else { } else {
// New client has connected, late on the first chapter or on any other chapter // 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 5 survivors, then set them up, TP them.
if(g_realSurvivorCount > 4) { if(g_realSurvivorCount > 4) {
CreateTimer(0.1, Timer_SetupNewClient, userid); 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 userid = event.GetInt("userid");
int client = GetClientOfUserId(userid); int client = GetClientOfUserId(userid);
if(GetClientTeam(client) != 2) return;
UpdateSurvivorCount(); UpdateSurvivorCount();
if(GetClientTeam(client) == 2 && !IsFakeClient(client) && !L4D_IsFirstMapInScenario()) { if(!IsFakeClient(client) && !L4D_IsFirstMapInScenario()) {
// Start door timeout: // 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) { if(g_prevPlayerCount > 0) {
// Open the door if we hit % percent // Open the door if we hit % percent
float percentIn = float(g_realSurvivorCount) / float(g_prevPlayerCount); float percentIn = float(g_realSurvivorCount) / float(g_prevPlayerCount);
if(percentIn > hMinPlayersSaferoomDoor.FloatValue) if(percentIn > hMinPlayersSaferoomDoor.FloatValue)
UnlockDoor(2);
} else{
UnlockDoor(2); UnlockDoor(2);
} else{ }
UnlockDoor(2);
} }
CreateTimer(0.5, Timer_GiveClientKit, userid); CreateTimer(0.5, Timer_GiveClientKit, userid);
SDKHook(client, SDKHook_WeaponEquip, Event_Pickup); 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) { void Event_PlayerDisconnect(Event event, const char[] name, bool dontBroadcast) {
int userid = event.GetInt("userid"); int userid = event.GetInt("userid");
int client = GetClientOfUserId(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].hasJoined = false;
playerData[client].state = State_PendingEmpty; playerData[client].state = State_PendingEmpty;
playerData[client].nameCache[0] = '\0'; playerData[client].nameCache[0] = '\0';
PrintToServer("debug: Player (index %d, uid %d) now pending empty", client, client, userid); PrintToServer("debug: Player (index %d, uid %d) now pending empty", client, client, userid);
CreateTimer(cvDropDisconnectTime.FloatValue, Timer_DropSurvivor, client); 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) { Action Timer_DropSurvivor(Handle h, int client) {
// Check that they are still pending empty (no one replaced them)
if(playerData[client].state == State_PendingEmpty) { if(playerData[client].state == State_PendingEmpty) {
playerData[client].state = State_Empty; playerData[client].state = State_Empty;
if(hMinPlayers != null) { if(hMinPlayers != null) {
@ -985,8 +1040,9 @@ Action Timer_DropSurvivor(Handle h, int client) {
void Event_ItemPickup(Event event, const char[] name, bool dontBroadcast) { void Event_ItemPickup(Event event, const char[] name, bool dontBroadcast) {
int client = GetClientOfUserId(event.GetInt("userid")); int client = GetClientOfUserId(event.GetInt("userid"));
if(client > 0) { if(client > 0 && GetClientTeam(client) == 2 && !IsFakeClient(client)) {
UpdatePlayerInventory(client); UpdatePlayerInventory(client);
SaveInventory(client);
} }
} }
@ -1015,6 +1071,12 @@ char TIER2_WEAPONS[9][] = {
Action Timer_SetupNewClient(Handle h, int userid) { Action Timer_SetupNewClient(Handle h, int userid) {
int client = GetClientOfUserId(userid); int client = GetClientOfUserId(userid);
if(client == 0) return Plugin_Handled; 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: // Incase their bot snagged a kit before we could give them one:
if(!DoesClientHaveKit(client)) { if(!DoesClientHaveKit(client)) {
int item = GivePlayerItem(client, "weapon_first_aid_kit"); int item = GivePlayerItem(client, "weapon_first_aid_kit");
@ -1049,6 +1111,7 @@ Action Timer_SetupNewClient(Handle h, int userid) {
// playerWeapons.PushString(weaponName); // playerWeapons.PushString(weaponName);
} }
} }
wpn = GetPlayerWeaponSlot(i, 1); wpn = GetPlayerWeaponSlot(i, 1);
if(wpn > 0) { if(wpn > 0) {
GetEdictClassname(wpn, weaponName, sizeof(weaponName)); GetEdictClassname(wpn, weaponName, sizeof(weaponName));
@ -1162,6 +1225,12 @@ Action Timer_GiveKits(Handle timer) {
} }
public void OnMapStart() { 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; g_isCheckpointReached = false;
//If previous round was a failure, restore the amount of kits that were left directly after map transition //If previous round was a failure, restore the amount of kits that were left directly after map transition
if(g_isFailureRound) { if(g_isFailureRound) {
@ -1184,7 +1253,7 @@ public void OnMapStart() {
g_extraFinaleTankEnabled = false; g_extraFinaleTankEnabled = false;
} }
int extraKits = GetSurvivorsCount() - 4; int extraKits = g_survivorCount - 4;
if(extraKits > 0) { if(extraKits > 0) {
// Keep how many extra kits were left after we loaded in, for resetting on failure rounds // Keep how many extra kits were left after we loaded in, for resetting on failure rounds
g_extraKitsAmount += extraKits; g_extraKitsAmount += extraKits;
@ -1224,20 +1293,11 @@ public void OnMapStart() {
L4D2_RunScript(HUD_SCRIPT_CLEAR); L4D2_RunScript(HUD_SCRIPT_CLEAR);
Director_OnMapStart(); Director_OnMapStart();
if(g_isLateLoaded) { if(g_isLateLoaded) {
UpdateSurvivorCount();
g_isLateLoaded = false; 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() { public void OnConfigsExecuted() {
if(hUpdateMinPlayers.BoolValue && hMinPlayers != null) { if(hUpdateMinPlayers.BoolValue && hMinPlayers != null) {
@ -1429,7 +1489,7 @@ public Action OnUpgradePackUse(int entity, int activator, int caller, UseType ty
clients.Push(activator); clients.Push(activator);
ClientCommand(activator, "play player/orch_hit_csharp_short.wav"); ClientCommand(activator, "play player/orch_hit_csharp_short.wav");
if(clients.Length >= GetSurvivorsCount()) { if(clients.Length >= g_survivorCount) {
AcceptEntityInput(entity, "kill"); AcceptEntityInput(entity, "kill");
delete clients; delete clients;
g_ammoPacks.Erase(index); g_ammoPacks.Erase(index);
@ -1485,13 +1545,13 @@ void UnlockDoor(int flag) {
SDKUnhook(entity, SDKHook_Use, Hook_Use); SDKUnhook(entity, SDKHook_Use, Hook_Use);
if(hSaferoomDoorAutoOpen.IntValue & flag) { if(hSaferoomDoorAutoOpen.IntValue & flag) {
AcceptEntityInput(entity, "Open"); 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) { Action Timer_UpdateHud(Handle h) {
@ -1674,11 +1734,63 @@ void DropDroppedInventories() {
} }
} }
} }
void SaveInventory(int client) { // Used for EPI hud
PrintDebug(DEBUG_GENERIC, "Saving inventory for %N", client); 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; PlayerInventory inventory;
inventory.timestamp = GetTime(); inventory.timestamp = time;
inventory.isAlive = IsClientInGame(client) && IsPlayerAlive(client); inventory.isAlive = IsPlayerAlive(client);
playerData[client].state = State_Active; playerData[client].state = State_Active;
GetClientAbsOrigin(client, inventory.location); GetClientAbsOrigin(client, inventory.location);
@ -1697,6 +1809,7 @@ void SaveInventory(int client) {
GetClientAuthId(client, AuthId_Steam3, buffer, sizeof(buffer)); GetClientAuthId(client, AuthId_Steam3, buffer, sizeof(buffer));
pInv.SetArray(buffer, inventory, sizeof(inventory)); pInv.SetArray(buffer, inventory, sizeof(inventory));
g_lastInvSave[client] = GetTime();
} }
void RestoreInventory(int client, PlayerInventory inventory) { void RestoreInventory(int client, PlayerInventory inventory) {
@ -1737,6 +1850,12 @@ bool GetInventory(const char[] steamid, PlayerInventory inventory) {
return pInv.GetArray(steamid, inventory, sizeof(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) { bool DoesInventoryDiffer(int client) {
static char buffer[32]; static char buffer[32];
GetClientAuthId(client, AuthId_Steam3, buffer, sizeof(buffer)); GetClientAuthId(client, AuthId_Steam3, buffer, sizeof(buffer));
@ -1753,47 +1872,69 @@ bool DoesInventoryDiffer(int client) {
return currentPrimary != storedPrimary || currentSecondary != storedSecondary; return currentPrimary != storedPrimary || currentSecondary != storedSecondary;
} }
// TODO: disable by cvar as well (replace abm_autohard)
bool IsEPIActive() { bool IsEPIActive() {
return g_epiEnabled; 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() { 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; int countTotal = 0, countReal = 0, countActive = 0;
#if !defined DEBUG_FORCE_PLAYERS
for(int i = 1; i <= MaxClients; i++) { for(int i = 1; i <= MaxClients; i++) {
if(IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) == 2) { 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++; countReal++;
} }
// FIXME: counting idle players for a brief tick
countTotal++; countTotal++;
if(playerData[i].state == State_Active) { if(playerData[i].state == State_Active) {
countActive++; countActive++;
} }
} }
} }
g_survivorCount = countTotal; g_survivorCount = countTotal;
g_realSurvivorCount = countReal; g_realSurvivorCount = countReal;
PrintDebug(DEBUG_GENERIC, "UpdateSurvivorCount: total=%d real=%d active=%d", countTotal, countReal, countActive); PrintDebug(DEBUG_GENERIC, "UpdateSurvivorCount: total=%d real=%d active=%d", countTotal, countReal, countActive);
#endif // Temporarily for now use g_realSurvivorCount, as players joining have a brief second where they are 5 players
#if defined DEBUG_FORCE_PLAYERS
g_survivorCount = DEBUG_FORCE_PLAYERS; // 1 = 5+ official
g_realSurvivorCount = DEBUG_FORCE_PLAYERS; // 2 = 5+ any map
#endif // 3 = always on
bool isActive = g_isGamemodeAllowed;
if(g_survivorCount > 4) { if(isActive && cvEPIEnabledMode.IntValue != 3) {
// Update friendly fire values to reduce accidental FF in crowded corridors // Enable only if mode is 2 or is official map AND 5+
ConVar friendlyFireFactor = GetActiveFriendlyFireFactor(); isActive = (g_isOfficialMap || cvEPIEnabledMode.IntValue == 2) && g_realSurvivorCount > 4;
// 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;
} }
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() { 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() { stock bool AreAllClientsReady() {
for(int i = 1; i <= MaxClients; i++) { for(int i = 1; i <= MaxClients; i++) {
if(IsClientConnected(i) && !IsClientInGame(i)) { if(IsClientConnected(i) && !IsClientInGame(i)) {
@ -1859,9 +1973,10 @@ stock bool AreAllClientsReady() {
} }
stock bool DoesClientHaveKit(int client) { stock bool DoesClientHaveKit(int client) {
char wpn[32]; if(IsClientConnected(client) && IsClientInGame(client)) {
if(IsClientConnected(client) && IsClientInGame(client) && GetClientWeaponName(client, 3, wpn, sizeof(wpn))) { char wpn[32];
return StrEqual(wpn, "weapon_first_aid_kit"); if(GetClientWeaponName(client, 3, wpn, sizeof(wpn)))
return StrEqual(wpn, "weapon_first_aid_kit");
} }
return false; return false;
} }
@ -1958,37 +2073,6 @@ int FindCabinetIndex(int cabinetId) {
return -1; 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 ...) { stock void RunVScriptLong(const char[] sCode, any ...) {
static int iScriptLogic = INVALID_ENT_REFERENCE; static int iScriptLogic = INVALID_ENT_REFERENCE;
if(iScriptLogic == INVALID_ENT_REFERENCE || !IsValidEntity(iScriptLogic)) { if(iScriptLogic == INVALID_ENT_REFERENCE || !IsValidEntity(iScriptLogic)) {