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_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<int>(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<int>(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<int>(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<int>(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<int>(special), 10, pos)) {
if(L4D_GetRandomPZSpawnPosition(player, view_as<int>(special) + 1, 10, pos)) {
// They use 1-index
L4D2_SpawnSpecial(view_as<int>(special) + 1, pos, NULL_VECTOR);
g_lastSpawnTime[view_as<int>(special)] = GetGameTime();
if(special == Special_Tank) {
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_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)) {