diff --git a/plugins/L4D2Tools.smx b/plugins/L4D2Tools.smx index da5aaec..a15440c 100644 Binary files a/plugins/L4D2Tools.smx and b/plugins/L4D2Tools.smx differ diff --git a/plugins/adminpanel.smx b/plugins/adminpanel.smx index 073dbc8..109fedf 100644 Binary files a/plugins/adminpanel.smx and b/plugins/adminpanel.smx differ diff --git a/plugins/l4d2_extraplayeritems.smx b/plugins/l4d2_extraplayeritems.smx index 9a73968..854f163 100644 Binary files a/plugins/l4d2_extraplayeritems.smx and b/plugins/l4d2_extraplayeritems.smx differ diff --git a/plugins/l4d2_feedthetrolls.smx b/plugins/l4d2_feedthetrolls.smx index 71e0749..8455350 100644 Binary files a/plugins/l4d2_feedthetrolls.smx and b/plugins/l4d2_feedthetrolls.smx differ diff --git a/plugins/l4d2_guesswho.smx b/plugins/l4d2_guesswho.smx index 267d711..fbe50ff 100644 Binary files a/plugins/l4d2_guesswho.smx and b/plugins/l4d2_guesswho.smx differ diff --git a/plugins/l4d2_hats.smx b/plugins/l4d2_hats.smx index 55eba8b..7e60e60 100644 Binary files a/plugins/l4d2_hats.smx and b/plugins/l4d2_hats.smx differ diff --git a/plugins/l4d2_hideandseek.smx b/plugins/l4d2_hideandseek.smx index f4a3a9e..7c3f18b 100644 Binary files a/plugins/l4d2_hideandseek.smx and b/plugins/l4d2_hideandseek.smx differ diff --git a/plugins/l4d2_mimic.smx b/plugins/l4d2_mimic.smx new file mode 100644 index 0000000..1ff27e4 Binary files /dev/null and b/plugins/l4d2_mimic.smx differ diff --git a/plugins/l4d2_population_control.smx b/plugins/l4d2_population_control.smx index 87c9c97..6b1d7d0 100644 Binary files a/plugins/l4d2_population_control.smx and b/plugins/l4d2_population_control.smx differ diff --git a/plugins/l4d2_prophunt.smx b/plugins/l4d2_prophunt.smx index d6f4a1a..fc72696 100644 Binary files a/plugins/l4d2_prophunt.smx and b/plugins/l4d2_prophunt.smx differ diff --git a/plugins/l4d_survivor_identity_fix.smx b/plugins/l4d_survivor_identity_fix.smx index 5f8b259..6acce75 100644 Binary files a/plugins/l4d_survivor_identity_fix.smx and b/plugins/l4d_survivor_identity_fix.smx differ diff --git a/plugins/sm_player_notes.smx b/plugins/sm_player_notes.smx index 5ac1d3d..6296987 100644 Binary files a/plugins/sm_player_notes.smx and b/plugins/sm_player_notes.smx differ diff --git a/scripting/L4D2Tools.sp b/scripting/L4D2Tools.sp index 96b6560..b55b332 100644 --- a/scripting/L4D2Tools.sp +++ b/scripting/L4D2Tools.sp @@ -21,6 +21,7 @@ char PRECACHE_SOUNDS[PRECACHE_SOUNDS_COUNT][] = { #tryinclude #include #include "l4d_survivor_identity_fix.inc" +#include char MODELS[8][] = { "models/survivors/survivor_gambler.mdl", @@ -51,6 +52,7 @@ static bool isHighPingIdle[MAXPLAYERS+1], isL4D1Survivors; static Handle hGoAwayFromKeyboard; static StringMap SteamIDs; static char lastSound[MAXPLAYERS+1][64], gamemode[32]; +AnyMap disabledItems; static float OUT_OF_BOUNDS[3] = {0.0, -1000.0, 0.0}; @@ -108,6 +110,7 @@ public void OnPluginStart() { } LasersUsed = new ArrayList(1, 0); + disabledItems = new AnyMap(); SteamIDs = new StringMap(); hGamemode = FindConVar("mp_gamemode"); @@ -462,7 +465,7 @@ void SetCharacter(int target, int survivorIndex, L4DModelId modelIndex, bool kee if (IsFakeClient(target)) { char name[32]; GetSurvivorName(target, name, sizeof(name)); - // SetClientInfo(target, "name", name); + SetClientInfo(target, "name", name); } UpdatePlayerIdentity(target, view_as(survivorIndex), keepModel); @@ -595,7 +598,6 @@ public void Event_PlayerDisconnect(Event event, const char[] name, bool dontBroa } } -int disabledItem[2048]; //Can also probably prevent kit drop to pick them up public void Event_WeaponDrop(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); @@ -604,16 +606,18 @@ public void Event_WeaponDrop(Event event, const char[] name, bool dontBroadcast) GetEntityClassname(client, newWpn, sizeof(newWpn)); if(StrEqual(newWpn, "weapon_ammo_pack")) { // prevent weapon from being picked up? - disabledItem[weapon] = client; + disabledItems.SetValue(weapon, GetClientUserId(client)); CreateTimer(10.0, Timer_AllowKitPickup, weapon); } } public Action Event_OnWeaponEquip(int client, int weapon) { - if(disabledItem[weapon] > 0 && disabledItem[weapon] != client) return Plugin_Handled; - return Plugin_Continue; + int userid; + if(disabledItems.GetValue(weapon, userid) && GetClientUserId(client) == userid) + return Plugin_Handled; + else return Plugin_Continue; } public Action Timer_AllowKitPickup(Handle h, int entity) { - disabledItem[entity] = 0; + disabledItems.Remove(entity); return Plugin_Handled; } public void OnMapStart() { @@ -629,6 +633,9 @@ public void OnMapStart() { HookEntityOutput("info_changelevel", "OnStartTouch", EntityOutput_OnStartTouchSaferoom); HookEntityOutput("trigger_changelevel", "OnStartTouch", EntityOutput_OnStartTouchSaferoom); } +public void OnMapEnd() { + disabledItems.Clear(); +} public void OnConfigsExecuted() { isL4D1Survivors = L4D2_GetSurvivorSetMap() == 1; if(hSVMaxPlayers != null && hPlayerLimit.IntValue > 0) { @@ -638,14 +645,19 @@ public void OnConfigsExecuted() { #if defined _sceneprocessor_included public void OnSceneStageChanged(int scene, SceneStages stage) { - if(stage == SceneStage_Started) { + if(stage == SceneStage_SpawnedPost) { int activator = GetSceneInitiator(scene); + // int actor = GetActorFromScene(scene); + + // PrintToServer("activator=%N actor=%N %s", activator, actor, sceneFile); if(activator == 0) { static char sceneFile[64]; GetSceneFile(scene, sceneFile, sizeof(sceneFile)); if(StrContains(sceneFile, "scenes/mechanic/dlc1_c6m1_initialmeeting") > -1 || StrEqual(sceneFile, "scenes/teengirl/dlc1_c6m1_initialmeeting07.vcd")) { CancelScene(scene); - }else if(StrEqual(sceneFile, "scenes/teengirl/dlc1_c6m1_initialmeeting13.vcd") && activator == 0) { + } else if(StrEqual(sceneFile, "scenes/teengirl/dlc1_c6m1_initialmeeting13.vcd")) { + CancelScene(scene); + } else if(StrEqual(sceneFile, "scenes/coach/worldc1m3b04.vcd")) { CancelScene(scene); } } diff --git a/scripting/adminpanel.sp b/scripting/adminpanel.sp index 6e867de..2185a25 100644 --- a/scripting/adminpanel.sp +++ b/scripting/adminpanel.sp @@ -52,6 +52,7 @@ int playerJoinTime[MAXPLAYERS+1]; Handle updateHealthTimer[MAXPLAYERS+1]; Handle updateItemTimer[MAXPLAYERS+1]; Handle receiveTimeoutTimer = null; +int pendingTries = 3; bool lateLoaded; @@ -72,16 +73,12 @@ GameState g_gameState; Buffer sendBuffer; Buffer receiveBuffer; // Unfortunately there's no easy way to have this not be the same as BUFFER_SIZE -public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) -{ +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) { lateLoaded = late; return APLRes_Success; } -public void OnPluginStart() -{ - - // TODO: periodic reconnect +public void OnPluginStart() { g_socket = new Socket(SOCKET_TCP, OnSocketError); g_socket.SetOption(SocketKeepAlive, 1); g_socket.SetOption(SocketReuseAddr, 1); @@ -118,17 +115,20 @@ public void OnPluginStart() HookEvent("pills_used", Event_ItemUsed); HookEvent("adrenaline_used", Event_ItemUsed); HookEvent("weapon_drop", Event_WeaponDrop); - HookEvent("player_first_spawn", Event_PlayerFirstSpawn); + HookEvent("player_spawn", Event_PlayerSpawn); HookEvent("map_transition", Event_MapTransition); HookEvent("player_death", Event_PlayerDeath); HookEvent("player_bot_replace", Event_PlayerToBot); HookEvent("bot_player_replace", Event_BotToPlayer); campaignStartTime = GetTime(); + char auth[32]; for(int i = 1; i <= MaxClients; i++) { - if(IsClientConnected(i) && IsClientInGame(i)) { - playerJoinTime[i] = GetTime(); - OnClientPutInServer(i); + if(IsClientInGame(i)) { + if(GetClientAuthId(i, AuthId_Steam2, auth, sizeof(auth))) { + OnClientAuthorized(i, auth); + OnClientPutInServer(i); + } } } @@ -138,6 +138,15 @@ public void OnPluginStart() RegAdminCmd("sm_panel_request_stop", Command_RequestStop, ADMFLAG_GENERIC); CommandArgRegex = new Regex("(?:[^\\s\"]+|\"[^\"]*\")+", 0); + + CreateTimer(300.0, Timer_FullSync, _, TIMER_REPEAT); +} + +Action Timer_FullSync(Handle h) { + if(CanSendPayload(true)) { + SendFullSync(); + } + return Plugin_Continue; } void TriggerHealthUpdate(int client, bool instant = false) { @@ -187,6 +196,7 @@ void OnSocketReceive(Socket socket, const char[] receiveData, int dataSize, int if(authState == Auth_Pending) { if(response == Live_OK) { authState = Auth_Success; + pendingTries = 0; PrintToServer("[AdminPanel] Authenticated with server successfully."); SendFullSync(); } else if(response == Live_Error) { @@ -203,7 +213,7 @@ void OnSocketReceive(Socket socket, const char[] receiveData, int dataSize, int lastReceiveTime = GetTime(); switch(response) { - case Live_RunComand: { + case Live_RunCommand: { char command[128]; char cmdNamespace[32]; int id = receiveBuffer.ReadByte(); @@ -223,8 +233,18 @@ void OnSocketReceive(Socket socket, const char[] receiveData, int dataSize, int case Live_Reconnect: CreateTimer(5.0, Timer_Reconnect); case Live_Refresh: { - PrintToServer("[AdminPanel] Sync requested, performing"); - // SendFullSync(); + int userid = receiveBuffer.ReadByte(); + if(userid > 0) { + int client = GetClientOfUserId(userid); + if(client > 0 && StartPayload(true)) { + PrintToServer("[AdminPanel] Sync requested for #%d, performing", userid); + AddPlayerRecord(client); + SendPayload(); + } + } else { + PrintToServer("[AdminPanel] Sync requested, performing"); + SendFullSync(); + } } } if(receiveTimeoutTimer != null) { @@ -481,6 +501,7 @@ void ConnectSocket() { if(authToken[0] == '\0') return; // Do not try to reconnect on auth failure, until token has changed if(authState == Auth_Fail) return; + authState = Auth_Pending; g_socket.Connect(OnSocketConnect, OnSocketReceive, OnSocketDisconnect, serverIp, serverPort); } @@ -499,6 +520,11 @@ Action Command_PanelDebug(int client, int args) { ReplyToCommand(client, "Target Host: %s:%d", serverIp, serverPort); ReplyToCommand(client, "Buffer Size: %d", BUFFER_SIZE); ReplyToCommand(client, "Can Send: %b\tCan Force-Send: %b", CanSendPayload(), CanSendPayload(true)); + } else if(StrEqual(arg, "cansend")) { + if(!g_socket.Connected) ReplyToCommand(client, "Socket Not Connected"); + else if(authState != Auth_Success) ReplyToCommand(client, "Socket Not Authenticated (State=%d)", authState); + else if(numberOfViewers == 0 || numberOfPlayers == 0) ReplyToCommand(client, "Can send forefully, but no players(%d)/viewers(%d)", numberOfPlayers, numberOfViewers); + ReplyToCommand(client, "Can Send!"); } else if(StrEqual(arg, "builtin")) { if(args < 2) { ReplyToCommand(client, "Usage: builtin "); @@ -561,6 +587,7 @@ void Event_GameStart(Event event, const char[] name, bool dontBroadcast) { } void Event_GameEnd(Event event, const char[] name, bool dontBroadcast) { campaignStartTime = 0; + CreateTimer(10.0, Timer_FullSync); } void Event_MapTransition(Event event, const char[] name, bool dontBroadcast) { @@ -571,6 +598,12 @@ void Event_MapTransition(Event event, const char[] name, bool dontBroadcast) { } } +public void L4D_OnFirstSurvivorLeftSafeArea_Post(int client) { + if(CanSendPayload()) { + SendPlayers(); + } +} + void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); if(client > 0) { @@ -590,6 +623,8 @@ public void Event_BotToPlayer(Handle event, char[] name, bool dontBroadcast) { int player = GetClientOfUserId(GetEventInt(event, "player")); int bot = GetClientOfUserId(GetEventInt(event, "bot")); if(player > 0 && !IsFakeClient(player) && StartPayload(true)) { + // Bot is going away, remove it: (prob unnecessary OnClientDisconnect happens) + AddPlayerRecord(bot, false); AddPlayerRecord(player); SendPayload(); } @@ -616,10 +651,13 @@ void Event_HealInterrupted(Event event, const char[] name, bool dontBroadcast) { g_icBeingHealed[subject] = false; } -void Event_PlayerFirstSpawn(Event event, const char[] name, bool dontBroadcast) { - int client = GetClientOfUserId(event.GetInt("userid")); - playerJoinTime[client] = GetTime(); +void Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast) { RecalculatePlayerCount(); + int client = GetClientOfUserId(event.GetInt("userid")); + if(client > 0 && StartPayload()) { + AddPlayerRecord(client); + SendPayload(); + } } void RecalculatePlayerCount() { @@ -662,6 +700,21 @@ public void OnMapStart() { public void OnConfigsExecuted() { isL4D1Survivors = L4D2_GetSurvivorSetMap() == 1; } +public void OnClientAuthorized(int client, const char[] auth) { + if(!IsFakeClient(client)) { + strcopy(steamidCache[client], 32, auth); + numberOfPlayers++; + } else { + // Check if they are not a real survivor bot, such as ABMBot or EPIBot, etc + if(StrContains(nameCache[client], "bot", false) > -1) { + return; + } + strcopy(steamidCache[client], 32, "BOT"); + } + GetClientName(client, nameCache[client], MAX_NAME_LENGTH); + playerJoinTime[client] = GetTime(); + RequestFrame(SendNewClient, client); +} // Player counts public void OnClientPutInServer(int client) { if(g_gameState == State_Transitioning) { @@ -671,21 +724,9 @@ public void OnClientPutInServer(int client) { SendPayload(); } } - GetClientName(client, nameCache[client], MAX_NAME_LENGTH); - if(!IsFakeClient(client)) { - GetClientAuthId(client, AuthId_SteamID64, steamidCache[client], 32); - numberOfPlayers++; - } else { - // Check if they are not a bot, such as ABMBot or EPIBot, etc - if(StrContains(nameCache[client], "bot", false) > -1) { - return; - } - strcopy(steamidCache[client], 32, "BOT"); - } SDKHook(client, SDKHook_WeaponEquipPost, OnWeaponPickUp); SDKHook(client, SDKHook_OnTakeDamageAlivePost, OnTakeDamagePost); // We wait a frame because Event_PlayerFirstSpawn sets their join time - RequestFrame(SendNewClient, client); } void OnWeaponPickUp(int client, int weapon) { @@ -780,6 +821,7 @@ public void OnClientDisconnect(int client) { // Incase somehow we lost track if(numberOfPlayers < 0) { numberOfPlayers = 0; + CreateTimer(1.0, Timer_FullSync); } } if(updateHealthTimer[client] != null) { @@ -806,7 +848,6 @@ void OnCvarChanged(ConVar convar, const char[] oldValue, const char[] newValue) if(serverPort == 0) serverPort = DEFAULT_SERVER_PORT; } PrintToServer("[AdminPanel] Sending data to %s:%d", serverIp, serverPort); - authState = Auth_Pending; ConnectSocket(); } } else if(cvar_gamemode == convar) { @@ -823,7 +864,6 @@ void OnCvarChanged(ConVar convar, const char[] oldValue, const char[] newValue) } } else if(cvar_authToken == convar) { strcopy(authToken, sizeof(authToken), newValue); - authState = Auth_Pending; // Token changed, re-try authentication ConnectSocket(); } @@ -931,15 +971,15 @@ enum CommandResultType { } enum LiveRecordType { - Live_Game, - Live_Player, - Live_Survivor, - Live_Infected, - Live_Finale, - Live_SurvivorItems, - Live_CommandResponse, - Live_Auth, - Live_Meta + Live_Game = 0, + Live_Player = 1, + Live_Survivor = 2, + Live_Infected = 3, + Live_Finale = 4, + Live_SurvivorItems = 5, + Live_CommandResponse = 6, + Live_Auth = 7, + Live_Meta = 8 } char LIVE_RECORD_NAMES[view_as(Live_Meta)+1][] = { "Game", @@ -958,13 +998,21 @@ enum LiveRecordResponse { Live_Reconnect, Live_Error, Live_Refresh, - Live_RunComand + Live_RunCommand } char pendingRecords[64]; bool CanSendPayload(bool ignorePause = false) { - if(!g_socket.Connected || authState != Auth_Success) return false; + if(!g_socket.Connected) return false; + if(authState != Auth_Success) { + if(authState == Auth_Pending && pendingTries > 0) { + pendingTries--; + PrintToServer("[AdminPanel] Auth state is pending. Too early?"); + ConnectSocket(); + } + return false; + } if(!ignorePause && (numberOfViewers == 0 || numberOfPlayers == 0)) return false; return true; } @@ -978,34 +1026,45 @@ bool StartPayload(bool ignorePause = false) { /// Starts payload, ignoring if the payload can even be sent bool hasRecord; int recordStart; +bool pendingRecord; void StartPayloadEx() { + if(pendingRecord) { + LogError("StartPayloadEx called before EndRecord()"); + } sendBuffer.Reset(); hasRecord = false; pendingRecords[0] = '\0'; recordStart = 0; + pendingRecord = false; + } void StartRecord(LiveRecordType type) { + if(pendingRecord) { + LogError("StartRecord called before EndRecord()"); + } if(hasRecord) { sendBuffer.WriteChar('\x1e'); } if(cvar_debug.BoolValue) Format(pendingRecords, sizeof(pendingRecords), "%s%s ", pendingRecords, LIVE_RECORD_NAMES[view_as(type)]); recordStart = sendBuffer.offset; - sendBuffer.WriteByte(0); // write temp NULL to be replaced + sendBuffer.WriteShort(-1); // write temp value to be replaced when record ends sendBuffer.WriteByte(view_as(type)); - hasRecord = true; + pendingRecord = true; } void EndRecord() { - int length = sendBuffer.offset - recordStart - 1; // subtract 1, as don't count length inside - sendBuffer.WriteByteAt(length, recordStart); + int length = sendBuffer.offset - recordStart - 2; // subtract 1, as don't count length inside + sendBuffer.WriteShortAt(length, recordStart); // if(cvar_debug.BoolValue) { - // int type = sendBuffer.ReadByteAt(recordStart + 1); + // int type = sendBuffer.ReadByteAt(recordStart + 2); // PrintToServer("End record %s(%d) (start: %d, end: %d) length: %d", LIVE_RECORD_NAMES[view_as(type)], type, recordStart, sendBuffer.offset, length); // } + hasRecord = true; + pendingRecord = false; } void AddGameRecord() { @@ -1020,13 +1079,6 @@ void AddGameRecord() { EndRecord(); } - -int GetMaxPlayers() { - if(cvar_visibleMaxPlayers != null && cvar_visibleMaxPlayers.IntValue > 0) return cvar_visibleMaxPlayers.IntValue; - if(cvar_maxplayers != null) return cvar_maxplayers.IntValue; - return L4D_IsVersusMode() ? 8 : 4; -} - void AddFinaleRecord(int stage) { StartRecord(Live_Finale); sendBuffer.WriteByte(stage); // finale stage @@ -1127,6 +1179,9 @@ void AddCommandResponseRecord(int id, CommandResultType resultType = Result_None } void AddAuthRecord() { + if(authToken[0] == '\0') { + LogError("AddAuthRecord called with missing auth token"); + } StartRecord(Live_Auth); sendBuffer.WriteByte(LIVESTATUS_VERSION); sendBuffer.WriteString(authToken); @@ -1141,10 +1196,11 @@ void AddMetaRecord(bool state) { void SendPayload() { if(sendBuffer.offset == 0) return; + int len = sendBuffer.Finish(); if(cvar_debug.BoolValue) { - PrintToServer("[AdminPanel] Sending %d bytes of data (records = %s)", sendBuffer.offset, pendingRecords); + PrintToServer("[AdminPanel] Sending %d bytes of data (records = %s)", len, pendingRecords); } - g_socket.Send(sendBuffer.buffer, sendBuffer.offset); + g_socket.Send(sendBuffer.buffer, len); } enum struct Buffer { @@ -1194,19 +1250,30 @@ enum struct Buffer { this.buffer[this.offset++] = (value >> 8) & 0xFF; } - void WriteInt(int value, int bytes = 4) { - this.buffer[this.offset++] = value & 0xFF; - this.buffer[this.offset++] = (value >> 8) & 0xFF; - this.buffer[this.offset++] = (value >> 16) & 0xFF; - this.buffer[this.offset++] = (value >> 24) & 0xFF; + void WriteShortAt(int value, int offset) { + this.buffer[offset] = value & 0xFF; + this.buffer[offset+1] = (value >> 8) & 0xFF; + } + + void WriteInt(int value) { + this.WriteIntAt(value, this.offset); + this.offset += 4; } + void WriteIntAt(int value, int offset) { + this.buffer[offset] = value & 0xFF; + this.buffer[offset+1] = (value >> 8) & 0xFF; + this.buffer[offset+2] = (value >> 16) & 0xFF; + this.buffer[offset+3] = (value >> 24) & 0xFF; + } + void WriteFloat(float value) { this.WriteInt(view_as(value)); } // Writes a null-terminated length string, strlen > size is truncated. void WriteString(const char[] string) { + this.buffer[this.offset] = '\0'; int written = strcopy(this.buffer[this.offset], BUFFER_SIZE, string); this.offset += written + 1; } @@ -1250,4 +1317,15 @@ enum struct Buffer { bool EOF() { return this.offset >= BUFFER_SIZE; } + + int Finish() { + this.buffer[this.offset++] = '\x0A'; + return this.offset; + } +} + +int GetMaxPlayers() { + if(cvar_visibleMaxPlayers != null && cvar_visibleMaxPlayers.IntValue > 0) return cvar_visibleMaxPlayers.IntValue; + if(cvar_maxplayers != null) return cvar_maxplayers.IntValue; + return L4D_IsVersusMode() ? 8 : 4; } \ No newline at end of file diff --git a/scripting/include/epi/director.sp b/scripting/include/epi/director.sp index 393eea7..b95fb9a 100644 --- a/scripting/include/epi/director.sp +++ b/scripting/include/epi/director.sp @@ -169,8 +169,7 @@ void OnTankBotSpawn(int client) { SetEntProp(client, Prop_Send, "m_iHealth", health); g_finaleStage = Stage_FirstTankSpawned; return; - } else if(g_realSurvivorCount < 6 && g_finaleStage == Stage_FirstTankSpawned) { - // 2nd tank spawned + } else if(g_realSurvivorCount >= 6 && g_finaleStage == Stage_FirstTankSpawned) { PrintDebug(DEBUG_SPAWNLOGIC, "OnTankBotSpawn: [FINALE] 2nd tank spawned"); float duration = GetRandomFloat(EXTRA_TANK_MIN_SEC, EXTRA_TANK_MAX_SEC); // Pass it 0, which doesnt make it a split tank, has default health @@ -215,6 +214,7 @@ int CalculateExtraTankHealth(int client) { int health = GetEntProp(client, Prop_Send, "m_iHealth"); float additionalHealth = float(g_survivorCount - 4) * cvEPITankHealth.FloatValue; health += RoundFloat(additionalHealth); + if(health <= 0) PrintToServer("CalculateExtraTankHealth: returning 0?"); return health; } diff --git a/scripting/include/feedthetrolls/commands.inc b/scripting/include/feedthetrolls/commands.inc index 1beac8b..1a5f0db 100644 --- a/scripting/include/feedthetrolls/commands.inc +++ b/scripting/include/feedthetrolls/commands.inc @@ -707,15 +707,15 @@ Action Command_SetReverseFF(int client, int args) { } else if(StrEqual(arg, "1")) { flag = 1; } else { - ReplyToCommand(client, "Unsupported amount. Possible values: 0, 2, 0.5, 3, 1"); + ReplyToCommand(client, "Unsupported amount. Possible values: 0.5, 1, 2, 3"); return Plugin_Handled; } // args are 1-indexed so <= for(int i = 3; i <= args; i++) { GetCmdArg(i, arg, sizeof(arg)); - if(arg[0] == 'f') { + if(arg[0] == 'f') { // [f]ire flag |= 32; - } else if(arg[0] == 'e') { + } else if(arg[0] == 'e' || arg[0] == 'b') { //[]blast or [e]xplode flag |= 64; } else { ReplyToCommand(client, "Unknown arg: %s", arg); diff --git a/scripting/l4d2_extraplayeritems.sp b/scripting/l4d2_extraplayeritems.sp index e487f3f..58b0248 100644 --- a/scripting/l4d2_extraplayeritems.sp +++ b/scripting/l4d2_extraplayeritems.sp @@ -86,9 +86,14 @@ public Plugin myinfo = }; ConVar hExtraItemBasePercentage, hExtraSpawnBasePercentage, hAddExtraKits, hMinPlayers, hUpdateMinPlayers, hMinPlayersSaferoomDoor, hSaferoomDoorWaitSeconds, hSaferoomDoorAutoOpen, hEPIHudState, hExtraFinaleTank, cvDropDisconnectTime, hSplitTankChance, cvFFDecreaseRate, cvZDifficulty, cvEPIHudFlags, cvEPISpecialSpawning, cvEPIGamemodes, hGamemode, cvEPITankHealth, cvEPIEnabledMode; +ConVar cvEPICommonCountScale, cvEPICommonCountScaleMax; ConVar g_ffFactorCvar, hExtraTankThreshold; + + +ConVar cvZCommonLimit; int zCommonLimitPrevValue; + int g_extraKitsAmount, g_extraKitsStart, g_saferoomDoorEnt, g_prevPlayerCount; -bool g_forcedSurvivorCount; +bool g_forcedSurvivorCount, g_extraKitsSpawnedFinale; static int g_currentChapter; bool g_isCheckpointReached, g_isLateLoaded, g_startCampaignGiven, g_isFailureRound, g_areItemsPopulated; static ArrayList g_ammoPacks; @@ -317,6 +322,10 @@ public void OnPluginStart() { 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 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); + cvEPICommonCountScale = CreateConVar("epi_commons_scale_multiplier", "0", "This value is multiplied by the number of extra players playing. It's then added to z_common_limit. 5 players with value 5 would be z_common_limit + ", FCVAR_NONE, true, 0.0); + cvEPICommonCountScaleMax = CreateConVar("epi_commons_scale_max", "60", "The maximum amount that z_common_limit can be scaled to.", FCVAR_NONE, true, 0.0); + cvZCommonLimit = FindConVar("z_common_limit"); + // TODO: hook flags, reset name index / ping mode cvEPIHudFlags.AddChangeHook(Cvar_HudStateChange); cvEPISpecialSpawning.AddChangeHook(Cvar_SpecialSpawningChange); @@ -1001,6 +1010,19 @@ Action Event_Pickup(int client, int weapon) { void Event_ItemPickup(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); if(client > 0 && GetClientTeam(client) == 2 && !IsFakeClient(client)) { + if(!g_extraKitsSpawnedFinale && L4D_IsMissionFinalMap(true)) { + float pos[3]; + GetAbsOrigin(client, pos); + Address address = L4D_GetNearestNavArea(pos); + if(address != Address_Null) { + int attributes = L4D_GetNavArea_SpawnAttributes(address); + if(attributes & NAV_SPAWN_FINALE) { + IncreaseKits(true); + } + } + } + // TODO: trigger increase kits finale on kit pickup + UpdatePlayerInventory(client); QueueSaveInventory(client); } @@ -1197,6 +1219,11 @@ int SpawnItem(const char[] itemName, float pos[3], float ang[3] = NULL_VECTOR) { } void IncreaseKits(bool inFinale) { + if(inFinale) { + if(g_extraKitsSpawnedFinale) return; + g_extraKitsSpawnedFinale = true; + g_extraKitsAmount = g_realSurvivorCount - 4; + } float pos[3]; int entity = FindEntityByClassname(-1, "weapon_first_aid_kit_spawn"); if(entity == INVALID_ENT_REFERENCE) { @@ -1289,12 +1316,13 @@ void Debug_GetAttributes(int attributes, char[] output, int maxlen) { } public void L4D2_OnChangeFinaleStage_Post(int stage) { - if(stage == 1) { + if(stage == 1 && IsEPIActive()) { IncreaseKits(true); } } public void OnMapStart() { + g_extraKitsSpawnedFinale = false; char map[32]; GetCurrentMap(map, sizeof(map)); // If map starts with c#m#, 98% an official map @@ -2031,14 +2059,7 @@ bool DoesInventoryDiffer(int client) { 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 -*/ +bool wasActive; void UpdateSurvivorCount() { #if defined DEBUG_FORCE_PLAYERS g_survivorCount = DEBUG_FORCE_PLAYERS; @@ -2075,9 +2096,35 @@ void UpdateSurvivorCount() { isActive = (g_isOfficialMap || cvEPIEnabledMode.IntValue == 2) && g_realSurvivorCount > 4; } g_epiEnabled = isActive; + if(g_epiEnabled && !wasActive) { + OnEPIActive(); + wasActive = true; + } else if(wasActive) { + OnEPIInactive(); + } + + if(isActive) SetFFFactor(g_epiEnabled); } + +void OnEPIActive() { + zCommonLimitPrevValue = cvZCommonLimit.IntValue; + // TODO: lag check for common limit + if(cvEPICommonCountScale.IntValue > 0) { + int newLimit = zCommonLimitPrevValue + RoundFloat(cvEPICommonCountScale.FloatValue * float(g_realSurvivorCount)); + if(newLimit > 0) { + if(newLimit > cvEPICommonCountScaleMax.IntValue) { + newLimit = cvEPICommonCountScaleMax.IntValue; + } + } + cvZCommonLimit.IntValue = newLimit; + } +} + +void OnEPIInactive() { + cvZCommonLimit.IntValue = zCommonLimitPrevValue; +} void SetFFFactor(bool enabled) { static float prevValue; // Restore the previous value (we use the value for the calculations of new value) diff --git a/scripting/l4d2_guesswho.sp b/scripting/l4d2_guesswho.sp index 2c9eb75..548780f 100644 --- a/scripting/l4d2_guesswho.sp +++ b/scripting/l4d2_guesswho.sp @@ -343,6 +343,9 @@ public void OnMapStart() { } Game.State = State_Unknown; } +public void OnMapEnd() { + Game.Cleanup(); +} public void ThinkPost(int entity) { static int iTeamNum[MAXPLAYERS+1]; diff --git a/scripting/l4d2_hideandseek.sp b/scripting/l4d2_hideandseek.sp index 23b34ae..8e5c4e2 100644 --- a/scripting/l4d2_hideandseek.sp +++ b/scripting/l4d2_hideandseek.sp @@ -248,6 +248,9 @@ public void OnMapStart() { lateLoaded = false; } } +public void OnMapEnd() { + Game.Cleanup(); +} public Action L4D2_OnChangeFinaleStage(int &finaleType, const char[] arg) { if(isEnabled) { diff --git a/scripting/l4d2_mimic.sp b/scripting/l4d2_mimic.sp new file mode 100644 index 0000000..cf43c99 --- /dev/null +++ b/scripting/l4d2_mimic.sp @@ -0,0 +1,225 @@ +#pragma semicolon 1 +#pragma newdecls required + +//#define DEBUG + +#define PLUGIN_VERSION "1.0" + +#include +#include +#include +#include +#include + +int g_mimicBot; +int g_mimicController; +int g_mimicCamera; + +public Plugin myinfo = +{ + name = "L4D2 Mimic", + author = "jackzmc", + description = "", + version = PLUGIN_VERSION, + url = "https://github.com/Jackzmc/sourcemod-plugins" +}; + +public void OnPluginStart() { + EngineVersion g_Game = GetEngineVersion(); + if(g_Game != Engine_Left4Dead && g_Game != Engine_Left4Dead2) { + SetFailState("This plugin is for L4D/L4D2 only."); + } + + RegAdminCmd("sm_mimic", Command_Mimic, ADMFLAG_CHEATS); + HookEvent("player_bot_replace", Event_BotToIdle); + AddCommandListener(OnCommand); +} + +public void OnPluginEnd() { + StopMimic(); +} + +#define PASSTHROUGH_COMMANDS_MAX 10 +char PASSTHROUGH_COMMANDS[PASSTHROUGH_COMMANDS_MAX][] = { + "say", + "vocalize", + "sm_give", + "give", + "sm_say", + "sm_chat", + "use", + "sm_slay", + "sm_model", + "sm_surv", +}; + +Action OnCommand(int client, const char[] command, int argc) { + if(g_mimicController == 0 || client != g_mimicController) return Plugin_Continue; + for(int i = 0; i < PASSTHROUGH_COMMANDS_MAX; i++) { + if(StrEqual(command, PASSTHROUGH_COMMANDS[i])) { + char args[256]; + GetCmdArgString(args, sizeof(args)); + // PrintToServer("pass: %s %s", command, args); + FakeClientCommandEx(g_mimicBot, "%s %s", command, args); + return Plugin_Handled; + } + } + // PrintToServer("ignore: %s", command); + return Plugin_Continue; +} + +void Event_BotToIdle(Event event, const char[] name, bool dontBroadcast) { + int player = GetClientOfUserId(event.GetInt("player")); + int bot = GetClientOfUserId(event.GetInt("bot")); + if(g_mimicBot == player) { + KickClient(bot); + RequestFrame(SetupMimic); + } +} +public void OnClientDisconnect(int client) { + if(client == g_mimicBot || client == g_mimicController) { + StopMimic(); + } +} + +Action Command_Mimic(int client, int args) { + if(g_mimicController != 0) { + if(g_mimicController == client) { + StopMimic(); + } else { + ReplyToCommand(client, "Mimic is currently active by another player."); + } + } else if(args > 0) { + char name[32], id[4]; + GetCmdArg(1, name, sizeof(name)); + int survivorId = -1; + if(args > 1) { + GetCmdArg(2, id, sizeof(id)); + survivorId = GetSurvivorId(id, L4D2_GetSurvivorSetMap() == 0); + } + StartMimic(client, name, survivorId); + } else { + ReplyToCommand(client, "Enter name"); + } + return Plugin_Handled; +} + +void StartMimic(int controller, const char[] name, int survivorId = -1) { + int bot = CreateFakeClient(name); + if(bot == -1) { + PrintToChat(controller, "Could not spawn fake client"); + return; + } + DispatchKeyValue(bot, "classname", "SurvivorBot"); + + ChangeClientTeam(bot, 2); + if(!DispatchSpawn(bot)) { + PrintToChat(controller, "Could not dispatch spawn"); + return; + } + L4D_RespawnPlayer(bot); + + char model[128]; + GetEntPropString(controller, Prop_Data, "m_ModelName", model, sizeof(model)); + SetEntityModel(bot, model); + + int camera = CreateEntityByName("point_viewcontrol_survivor"); + DispatchKeyValue(camera, "targetname", "mimic_cam"); + DispatchSpawn(camera); + + g_mimicBot = bot; + g_mimicController = controller; + g_mimicCamera = camera; + + SDKHook(controller, SDKHook_WeaponSwitchPost, OnWeaponSwitchPost); + PrintToServer("controller: %N | bot: %N(#%d) | camera: %d", controller, bot, GetClientUserId(bot), camera); + + RequestFrame(SetupMimic, survivorId); +} + +void SetupMimic(int survivorId = -1) { + if(g_mimicController == 0) return; + float pos[3], ang[3]; + GetClientAbsOrigin(g_mimicController, pos); + int nav = L4D_GetNearestNavArea(pos); + if(nav > 0) { + L4D_FindRandomSpot(nav, pos); + } + TeleportEntity(g_mimicBot, pos, NULL_VECTOR, NULL_VECTOR); + CheatCommand(g_mimicBot, "give", "rifle_ak47", ""); + CheatCommand(g_mimicBot, "give", "first_aid_kit", ""); + + if(survivorId == -1) survivorId = GetEntProp(g_mimicController, Prop_Send, "m_survivorCharacter"); + SetEntProp(g_mimicBot, Prop_Send, "m_survivorCharacter", survivorId); + + + AcceptEntityInput(g_mimicCamera, "Disable", g_mimicController); + GetClientEyePosition(g_mimicBot, pos); + GetClientEyeAngles(g_mimicBot, ang); + ClearParent(g_mimicCamera); + TeleportEntity(g_mimicCamera, pos, NULL_VECTOR, NULL_VECTOR); + SetParent(g_mimicCamera, g_mimicBot); + AcceptEntityInput(g_mimicCamera, "Enable", g_mimicController); +} + +void OnWeaponSwitchPost(int client, int weapon) { + if(g_mimicBot == 0) return; + for(int slot = 0; slot < 5; slot++) { + int slotWpn = GetPlayerWeaponSlot(client, slot); + if(slotWpn == weapon) { + ClientCommand(g_mimicBot, "slot%d", slot); + return; + } + } +} + + + +void StopMimic() { + if(g_mimicBot > 0 && IsClientInGame(g_mimicBot)) { + if(L4D_GoAwayFromKeyboard(g_mimicBot)) { + int bot = L4D_GetBotOfIdlePlayer(g_mimicBot); + if(bot > 0) { + KickClient(bot); + } + } + KickClient(g_mimicBot); + } + if(g_mimicCamera > 0 && IsValidEntity(g_mimicCamera)) { + AcceptEntityInput(g_mimicCamera, "Disable"); + RemoveEntity(g_mimicCamera); + } + if(g_mimicController > 0 && IsClientConnected(g_mimicController)) { + SDKUnhook(g_mimicController, SDKHook_WeaponSwitchPost, OnWeaponSwitchPost); + } + + g_mimicBot = 0; + g_mimicController = 0; + g_mimicCamera = 0; +} + +int prevButtons; +float prevAngles[3], prevVel[3]; +public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon) { + if(client == g_mimicController) { + if(!IsPlayerAlive(client)) { + StopMimic(); + } + prevAngles = angles; + prevButtons = buttons; + prevVel = vel; + SetEntPropFloat(client, Prop_Send, "m_flNextAttack", GetGameTime() + 1.0); + // TeleportEntity(client, NULL_VECTOR, angles, NULL_VECTOR); + return Plugin_Handled; + } else if(client == g_mimicBot) { + if(!IsPlayerAlive(client)) { + StopMimic(); + } + buttons = prevButtons | IN_BULLRUSH; + angles = prevAngles; + vel = prevVel; + TeleportEntity(client, NULL_VECTOR, angles, NULL_VECTOR); + return Plugin_Changed; + } + return Plugin_Continue; +} \ No newline at end of file diff --git a/scripting/l4d2_population_control.sp b/scripting/l4d2_population_control.sp index 663db88..bddbf59 100644 --- a/scripting/l4d2_population_control.sp +++ b/scripting/l4d2_population_control.sp @@ -10,6 +10,7 @@ #include #include #include +#include public Plugin myinfo = { @@ -33,7 +34,7 @@ static ConVar hZCommonLimit; static bool IsDoneLoading, clownMusicPlayed; static int iCurrentCommons, commonLimit, clownCommonsSpawned; -static int commonType[2048]; +static AnyMap commonType; #define COMMON_MODELS_COUNT 6 static char INFECTED_MODELS[COMMON_MODELS_COUNT][] = { @@ -50,13 +51,15 @@ static char WORKER_MODELS[3][] = { "models/infected/common_male_roadcrew.mdl", "models/infected/common_male_roadcrew_rain.mdl" }; -enum CommonTypes { +enum CommonType { + Common_Worker = -2, + Common_Any = -1, Common_Clown, Common_Mud, Common_Ceda, Common_Riot, Common_Jimmy, - Common_Worker = -1, + Common_Fallen, }; //TODO: Add back survivor zombie, inc z_fallen_max_count @@ -67,6 +70,8 @@ public void OnPluginStart() { SetFailState("This plugin is for L4D2 only."); } + commonType = new AnyMap(); + HookEvent("game_start", OnGameStart); hPercentTotal = CreateConVar("l4d2_population_global_chance", "1.0", "The % chance that any the below chances occur.\n0.0 = NEVER, 1.0: ALWAYS"); @@ -114,6 +119,7 @@ public void OnMapEnd() { IsDoneLoading = false; iCurrentCommons = 0; clownCommonsSpawned = 0; + commonType.Clear(); } public void CVAR_hTotalZombiesChanged(ConVar convar, const char[] oldValue, const char[] newValue) { @@ -137,28 +143,28 @@ public void OnEntityCreated(int entity, const char[] classname) { if(GetRandomFloat() <= hPercentTotal.FloatValue) { if(GetRandomFloat() <= hPercentClown.FloatValue) { SetEntityModel(entity, INFECTED_MODELS[Common_Clown]); - commonType[entity] = 2; + commonType.SetValue(entity, Common_Clown); }else if(GetRandomFloat() <= hPercentMud.FloatValue) { SetEntityModel(entity, INFECTED_MODELS[Common_Mud]); - commonType[entity] = 3; + commonType.SetValue(entity, Common_Mud); }else if(GetRandomFloat() <= hPercentCeda.FloatValue) { SetEntityModel(entity, INFECTED_MODELS[Common_Ceda]); - commonType[entity] = 4; + commonType.SetValue(entity, Common_Ceda); }else if(GetRandomFloat() <= hPercentWorker.FloatValue) { //worker has multiple models: SetEntityModel(entity, WORKER_MODELS[GetRandomInt(0,2)]); - commonType[entity] = 5; + commonType.SetValue(entity, Common_Worker); }else if(GetRandomFloat() <= hPercentRiot.FloatValue) { SetEntityModel(entity, INFECTED_MODELS[Common_Riot]); - commonType[entity] = 6; + commonType.SetValue(entity, Common_Riot); }else if(GetRandomFloat() <= hPercentJimmy.FloatValue) { SetEntityModel(entity, INFECTED_MODELS[Common_Jimmy]); - commonType[entity] = 7; + commonType.SetValue(entity, Common_Jimmy); }else{ - commonType[entity] = 1; + commonType.SetValue(entity, Common_Any); } }else{ - commonType[entity] = 1; + commonType.SetValue(entity, Common_Any); } } } @@ -171,7 +177,8 @@ public Action Hook_SpawnPost(int entity) { } } ++iCurrentCommons; - if(commonType[entity] == 2) { + CommonType type; + if(commonType.GetValue(entity, type) && type == Common_Clown) { if(++clownCommonsSpawned > CLOWN_MUSIC_THRESHOLD && !clownMusicPlayed) { clownMusicPlayed = true; EmitSoundToAll("custom/clown.mp3"); @@ -189,11 +196,12 @@ public Action Hook_SpawnPost(int entity) { // } public void OnEntityDestroyed(int entity) { - if(entity > 0 && entity <= 2048 && commonType[entity] > 0) { - commonType[entity] = 0; - if(commonType[entity] == 2) { + if(entity > 0 && entity <= 2048 && commonType.ContainsKey(entity)) { + CommonType type; + if(commonType.GetValue(entity, type) && type == Common_Clown) { --clownCommonsSpawned; } + commonType.Remove(entity); if(--iCurrentCommons < CLOWN_MUSIC_THRESHOLD - 10) { clownMusicPlayed = false; } diff --git a/scripting/l4d2_prophunt.sp b/scripting/l4d2_prophunt.sp index 4c7964c..837f157 100644 --- a/scripting/l4d2_prophunt.sp +++ b/scripting/l4d2_prophunt.sp @@ -273,6 +273,7 @@ public void OnMapEnd() { Game.UnsetupPlayer(i); } } + Game.Cleanup(); } void ClearInventory(int client) { @@ -347,7 +348,7 @@ stock void GlowEntity(int entity, int client, float lifetime = 5.0) { GetEntPropVector(entity, Prop_Data, "m_vecMaxs", maxs); GetEntPropVector(entity, Prop_Data, "m_angRotation", ang); - Effect_DrawBeamBoxRotatableToClient(client, pos, mins, maxs, ang, g_iLaserIndex, 0, 0, 1, lifetime, 1.0, 1.0, 100, 0.1, COLOR_PROPFINDER, 0.0); + Effect_DrawBeamBoxRotatableToClient(client, pos, mins, maxs, ang, g_iLaserIndex, 0, 0, 1, lifetime, 1.0, 1.0, 100, 0.1, COLOR_PROPFINDER, 0); } public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3], float angles[3], int& weapon, int& subtype, int& cmdnum, int& tickcount, int& seed, int mouse[2]) { diff --git a/scripting/l4d_survivor_identity_fix.sp b/scripting/l4d_survivor_identity_fix.sp index 0837598..67fbb82 100644 --- a/scripting/l4d_survivor_identity_fix.sp +++ b/scripting/l4d_survivor_identity_fix.sp @@ -181,7 +181,7 @@ public void Event_PlayerToBot(Handle event, char[] name, bool dontBroadcast) } for(int i = 0; i < 8; i++) { if (strcmp(g_Models[player], survivor_models[i], false) == 0) { - // SetClientInfo(bot, "name", survivor_names[i]); + SetClientInfo(bot, "name", survivor_names[i]); break; } } diff --git a/scripting/sm_player_notes.sp b/scripting/sm_player_notes.sp index 5382aae..16d984b 100644 --- a/scripting/sm_player_notes.sp +++ b/scripting/sm_player_notes.sp @@ -250,7 +250,7 @@ public Action OnClientSayCommand(int client, const char[] command, const char[] // TODO: escape content DB.Format(query, sizeof(query), "INSERT INTO `notes` (steamid, markedBy, content) VALUES ('%s', '%s', '%s')", menuNoteTarget, buffer, sArgsTrimmed); DB.Query(DB_AddNote, query); - LogAction(client, -1, "\"%L\" added note for \"%s\" (%s): \"%s\"", client, menuNoteTargetName, menuNoteTarget, sArgsTrimmed); + LogAction(client, -1, "added note for \"%s\" (%s): \"%s\"", client, menuNoteTargetName, menuNoteTarget, sArgsTrimmed); Format(buffer, sizeof(buffer), "%N: ", client); CShowActivity2(client, buffer, "added a note for {green}%s: {default}\"%s\"", menuNoteTargetName, sArgs); }