425 lines
11 KiB
SourcePawn
425 lines
11 KiB
SourcePawn
#include <sourcemod>
|
|
#include <sdktools>
|
|
#include <cstrike>
|
|
#include <zombiereloaded>
|
|
#include <ripext>
|
|
|
|
#pragma semicolon 1
|
|
#pragma newdecls required
|
|
|
|
#define MIN_PLAYERS 2
|
|
|
|
int g_iWarmup = 0;
|
|
int g_iMaxWarmup = 0;
|
|
bool g_bWarmup = false;
|
|
ConVar g_CVar_sm_warmuptime;
|
|
ConVar g_CVar_sm_warmupratio;
|
|
ConVar g_CVar_sm_warmupmaxtime;
|
|
ConVar g_CVar_sm_warmupmintime;
|
|
ConVar g_CVar_sm_warmupbase;
|
|
ConVar g_CVar_sm_warmupperplayer;
|
|
ConVar g_CVar_sm_warmuppermib;
|
|
ConVar g_CVar_sm_warmupdefaultmib;
|
|
ConVar g_CVar_sm_warmupfastdl;
|
|
bool g_bLateLoad = false;
|
|
|
|
// Max warmup inputs: compressed map size on fastdl and the most players seen still connecting
|
|
float g_fMapMiB = 0.0;
|
|
int g_iPeakConnecting = 0;
|
|
int g_iMapSerial = 0;
|
|
bool g_bTriedLowercase = false;
|
|
Handle g_hWarmupEndFwd;
|
|
|
|
bool g_bRoundEnded = false;
|
|
bool g_bZombieSpawned = false;
|
|
int g_TeamChangeQueue[MAXPLAYERS + 1] = { -1, ... };
|
|
|
|
public Plugin myinfo =
|
|
{
|
|
name = "TeamManager",
|
|
author = "BotoX",
|
|
description = "",
|
|
version = "1.0",
|
|
url = "https://github.com/CSSZombieEscape/sm-plugins/tree/master/TeamManager"
|
|
};
|
|
|
|
public void OnPluginStart()
|
|
{
|
|
if (GetEngineVersion() == Engine_CSGO)
|
|
AddCommandListener(OnJoinTeamCommand, "joingame");
|
|
|
|
AddCommandListener(OnJoinTeamCommand, "jointeam");
|
|
HookEvent("round_start", OnRoundStart, EventHookMode_Pre);
|
|
HookEvent("round_end", OnRoundEnd, EventHookMode_PostNoCopy);
|
|
|
|
g_CVar_sm_warmuptime = CreateConVar("sm_warmuptime", "10", "Warmup timer.", 0, true, 0.0, true, 60.0);
|
|
g_CVar_sm_warmupratio = CreateConVar("sm_warmupratio", "0.90", "Ratio of connected players that need to be in game to start warmup timer.", 0, true, 0.0, true, 1.0);
|
|
g_CVar_sm_warmupmaxtime = CreateConVar("sm_warmupmaxtime", "150", "Upper limit for the computed max warmup time.", 0, true, 0.0, true, 600.0);
|
|
g_CVar_sm_warmupmintime = CreateConVar("sm_warmupmintime", "30", "Lower limit for the computed max warmup time.", 0, true, 0.0, true, 600.0);
|
|
g_CVar_sm_warmupbase = CreateConVar("sm_warmupbase", "20", "Base seconds of the computed max warmup time.", 0, true, 0.0);
|
|
g_CVar_sm_warmupperplayer = CreateConVar("sm_warmupperplayer", "0.0", "Max warmup seconds added per player still connecting.", 0, true, 0.0);
|
|
g_CVar_sm_warmuppermib = CreateConVar("sm_warmuppermib", "0.5", "Max warmup seconds added per MiB of the compressed map on fastdl.", 0, true, 0.0);
|
|
g_CVar_sm_warmupdefaultmib = CreateConVar("sm_warmupdefaultmib", "60", "Map size in MiB to assume until, or if, the fastdl lookup fails.", 0, true, 0.0);
|
|
g_CVar_sm_warmupfastdl = CreateConVar("sm_warmupfastdl", "https://fastdl.unloze.com/css_ze/maps/", "FastDL maps URL, <map>.bsp.bz2 is appended.");
|
|
|
|
g_hWarmupEndFwd = CreateGlobalForward("TeamManager_WarmupEnd", ET_Ignore);
|
|
|
|
AutoExecConfig(true, "plugin.TeamManager");
|
|
|
|
for(int i = 1; i <= MaxClients; i++)
|
|
{
|
|
if(IsClientInGame(i) && IsPlayerAlive(i) && ZR_IsClientZombie(i))
|
|
{
|
|
g_bZombieSpawned = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
|
|
{
|
|
CreateNative("TeamManager_InWarmup", Native_InWarmup);
|
|
|
|
RegPluginLibrary("TeamManager");
|
|
|
|
g_bLateLoad = late;
|
|
return APLRes_Success;
|
|
}
|
|
|
|
public void OnConfigsExecuted()
|
|
{
|
|
if(g_bLateLoad)
|
|
{
|
|
g_bLateLoad = false;
|
|
return;
|
|
}
|
|
|
|
g_iWarmup = 0;
|
|
g_iMaxWarmup = 0;
|
|
g_bWarmup = false;
|
|
g_bRoundEnded = false;
|
|
g_bZombieSpawned = false;
|
|
|
|
g_iMapSerial++;
|
|
g_iPeakConnecting = 0;
|
|
g_fMapMiB = g_CVar_sm_warmupdefaultmib.FloatValue;
|
|
g_bTriedLowercase = false;
|
|
|
|
// sm_warmuptime 0 disables warmup entirely, some maps break if the round is restarted
|
|
if(g_CVar_sm_warmuptime.IntValue > 0)
|
|
{
|
|
char sMap[PLATFORM_MAX_PATH];
|
|
GetCurrentMap(sMap, sizeof(sMap));
|
|
RequestMapSize(sMap);
|
|
|
|
g_bWarmup = true;
|
|
CreateTimer(1.0, OnWarmupTimer, 0, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE);
|
|
}
|
|
}
|
|
|
|
void RequestMapSize(const char[] sMap)
|
|
{
|
|
char sBaseURL[256];
|
|
g_CVar_sm_warmupfastdl.GetString(sBaseURL, sizeof(sBaseURL));
|
|
|
|
char sURL[512];
|
|
Format(sURL, sizeof(sURL), "%s%s.bsp.bz2", sBaseURL, sMap);
|
|
|
|
// rip.ext has no HEAD, a one byte range request returns the full size in Content-Range instead
|
|
HTTPRequest request = new HTTPRequest(sURL);
|
|
request.SetHeader("Range", "bytes=0-0");
|
|
request.Timeout = 10;
|
|
request.Get(OnMapSizeResponse, g_iMapSerial);
|
|
}
|
|
|
|
public void OnMapSizeResponse(HTTPResponse response, any serial, const char[] error)
|
|
{
|
|
if(serial != g_iMapSerial)
|
|
return;
|
|
|
|
char sMap[PLATFORM_MAX_PATH];
|
|
GetCurrentMap(sMap, sizeof(sMap));
|
|
|
|
if(error[0])
|
|
{
|
|
LogMessage("Map size lookup for %s failed (%s), assuming %.1f MiB.", sMap, error, g_fMapMiB);
|
|
return;
|
|
}
|
|
|
|
char sSize[64];
|
|
bool bFound = false;
|
|
|
|
if(response.Status == HTTPStatus_PartialContent && response.GetHeader("Content-Range", sSize, sizeof(sSize)))
|
|
{
|
|
int iSlash = FindCharInString(sSize, '/', true);
|
|
if(iSlash != -1)
|
|
{
|
|
strcopy(sSize, sizeof(sSize), sSize[iSlash + 1]);
|
|
bFound = true;
|
|
}
|
|
}
|
|
else if(response.Status == HTTPStatus_OK && response.GetHeader("Content-Length", sSize, sizeof(sSize)))
|
|
bFound = true;
|
|
else if(response.Status == HTTPStatus_NotFound && !g_bTriedLowercase)
|
|
{
|
|
// fastdl is case sensitive and mostly stores lowercase names
|
|
g_bTriedLowercase = true;
|
|
|
|
char sLower[PLATFORM_MAX_PATH];
|
|
strcopy(sLower, sizeof(sLower), sMap);
|
|
for(int i = 0; sLower[i]; i++)
|
|
sLower[i] = CharToLower(sLower[i]);
|
|
|
|
if(!StrEqual(sLower, sMap))
|
|
{
|
|
RequestMapSize(sLower);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// float, a large file would overflow an int byte count
|
|
float fBytes = bFound ? StringToFloat(sSize) : 0.0;
|
|
if(fBytes <= 0.0)
|
|
{
|
|
LogMessage("Map size lookup for %s got no size (HTTP %d), assuming %.1f MiB.", sMap, response.Status, g_fMapMiB);
|
|
return;
|
|
}
|
|
|
|
g_fMapMiB = fBytes / 1048576.0;
|
|
LogMessage("Map %s is %.1f MiB on fastdl.", sMap, g_fMapMiB);
|
|
}
|
|
|
|
int GetMaxWarmupTime()
|
|
{
|
|
float fTime = g_CVar_sm_warmupbase.FloatValue
|
|
+ g_CVar_sm_warmupperplayer.FloatValue * g_iPeakConnecting
|
|
+ g_CVar_sm_warmuppermib.FloatValue * g_fMapMiB;
|
|
|
|
int iTime = RoundToCeil(fTime);
|
|
int iMin = g_CVar_sm_warmupmintime.IntValue;
|
|
int iMax = g_CVar_sm_warmupmaxtime.IntValue;
|
|
|
|
if(iTime < iMin)
|
|
iTime = iMin;
|
|
if(iTime > iMax)
|
|
iTime = iMax;
|
|
|
|
return iTime;
|
|
}
|
|
|
|
public Action OnWarmupTimer(Handle timer)
|
|
{
|
|
// Disabled after warmup started (e.g. a map config ran late): stop without restarting the round
|
|
if(g_CVar_sm_warmuptime.IntValue <= 0)
|
|
{
|
|
g_iMaxWarmup = 0;
|
|
g_bWarmup = false;
|
|
Call_StartForward(g_hWarmupEndFwd);
|
|
Call_Finish();
|
|
return Plugin_Stop;
|
|
}
|
|
|
|
// Only players still loading in count, anyone who left during the map change is already gone
|
|
int iConnecting = 0;
|
|
int iReady = 0;
|
|
for(int client = 1; client <= MaxClients; client++)
|
|
{
|
|
if(!IsClientConnected(client) || IsFakeClient(client))
|
|
continue;
|
|
|
|
if(IsClientInGame(client))
|
|
iReady++;
|
|
else
|
|
iConnecting++;
|
|
}
|
|
|
|
if(iConnecting > g_iPeakConnecting)
|
|
g_iPeakConnecting = iConnecting;
|
|
|
|
int iMaxWarmupTime = GetMaxWarmupTime();
|
|
|
|
if(g_iMaxWarmup >= iMaxWarmupTime || g_iWarmup >= g_CVar_sm_warmuptime.IntValue)
|
|
{
|
|
char sMap[PLATFORM_MAX_PATH];
|
|
GetCurrentMap(sMap, sizeof(sMap));
|
|
LogMessage("Warmup on %s ended after %ds with %d ready players: %.1f MiB, %d peak connecting, max warmup %ds.",
|
|
sMap, g_iMaxWarmup + g_iWarmup, iReady, g_fMapMiB, g_iPeakConnecting, iMaxWarmupTime);
|
|
|
|
g_iMaxWarmup = 0;
|
|
g_bWarmup = false;
|
|
float fDelay = 3.0;
|
|
CS_TerminateRound(fDelay, CSRoundEnd_GameStart, false);
|
|
CreateTimer(fDelay, Timer_FireForward, _, TIMER_FLAG_NO_MAPCHANGE);
|
|
return Plugin_Stop;
|
|
}
|
|
|
|
if(g_CVar_sm_warmupratio.FloatValue > 0.0)
|
|
{
|
|
int ClientsConnected = GetClientCount(false);
|
|
int ClientsInGame = GetClientCount(true);
|
|
int ClientsNeeded = RoundToCeil(float(ClientsConnected) * g_CVar_sm_warmupratio.FloatValue);
|
|
ClientsNeeded = ClientsNeeded > MIN_PLAYERS ? ClientsNeeded : MIN_PLAYERS;
|
|
|
|
if(ClientsInGame < ClientsNeeded)
|
|
{
|
|
g_iMaxWarmup++;
|
|
PrintCenterTextAll("Warmup: Waiting for %d more players to join or %d seconds.", ClientsNeeded - ClientsInGame, iMaxWarmupTime - g_iMaxWarmup);
|
|
return Plugin_Continue;
|
|
}
|
|
else
|
|
{
|
|
g_iWarmup++;
|
|
PrintCenterTextAll("Warmup: Enough players joined. %d seconds left. ", g_CVar_sm_warmuptime.IntValue - g_iWarmup);
|
|
return Plugin_Continue;
|
|
}
|
|
}
|
|
|
|
return Plugin_Continue;
|
|
}
|
|
|
|
public Action Timer_FireForward(Handle hThis)
|
|
{
|
|
Call_StartForward(g_hWarmupEndFwd);
|
|
Call_Finish();
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
public void OnClientDisconnect(int client)
|
|
{
|
|
g_TeamChangeQueue[client] = -1;
|
|
}
|
|
|
|
public Action OnJoinTeamCommand(int client, const char[] command, int argc)
|
|
{
|
|
if(client < 1 || client >= MaxClients || !IsClientInGame(client))
|
|
return Plugin_Continue;
|
|
|
|
if(StrEqual(command, "joingame", false))
|
|
{
|
|
if(GetClientTeam(client) != CS_TEAM_NONE)
|
|
return Plugin_Continue;
|
|
|
|
ShowVGUIPanel(client, "team");
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
char sArg[8];
|
|
GetCmdArg(1, sArg, sizeof(sArg));
|
|
|
|
int CurrentTeam = GetClientTeam(client);
|
|
int NewTeam = StringToInt(sArg);
|
|
|
|
if(NewTeam < CS_TEAM_NONE || NewTeam > CS_TEAM_CT)
|
|
return Plugin_Handled;
|
|
|
|
// prevent accidental suicide
|
|
if(g_bZombieSpawned && IsPlayerAlive(client) && NewTeam != CS_TEAM_SPECTATOR)
|
|
return Plugin_Handled;
|
|
|
|
if(g_bRoundEnded)
|
|
{
|
|
if(NewTeam == CS_TEAM_T || NewTeam == CS_TEAM_NONE)
|
|
NewTeam = CS_TEAM_CT;
|
|
|
|
if(NewTeam == CurrentTeam)
|
|
{
|
|
if(g_TeamChangeQueue[client] != -1)
|
|
{
|
|
g_TeamChangeQueue[client] = -1;
|
|
PrintCenterText(client, "Team change request canceled.");
|
|
}
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
g_TeamChangeQueue[client] = NewTeam;
|
|
PrintCenterText(client, "You will be placed in the selected team shortly.");
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
if(!g_bZombieSpawned)
|
|
{
|
|
if(NewTeam == CS_TEAM_T || NewTeam == CS_TEAM_NONE)
|
|
NewTeam = CS_TEAM_CT;
|
|
}
|
|
else if(NewTeam == CS_TEAM_CT || NewTeam == CS_TEAM_NONE)
|
|
NewTeam = CS_TEAM_T;
|
|
|
|
if(g_bZombieSpawned && NewTeam == CurrentTeam)
|
|
return Plugin_Handled;
|
|
|
|
ForcePlayerSuicide(client);
|
|
ChangeClientTeam(client, CS_TEAM_NONE);
|
|
ChangeClientTeam(client, NewTeam);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
public void OnRoundStart(Event event, const char[] name, bool dontBroadcast)
|
|
{
|
|
g_bRoundEnded = false;
|
|
g_bZombieSpawned = false;
|
|
|
|
for(int client = 1; client <= MaxClients; client++)
|
|
{
|
|
if(!IsClientInGame(client))
|
|
continue;
|
|
|
|
int CurrentTeam = GetClientTeam(client);
|
|
int NewTeam = CS_TEAM_CT;
|
|
|
|
if(g_TeamChangeQueue[client] != -1)
|
|
{
|
|
NewTeam = g_TeamChangeQueue[client];
|
|
g_TeamChangeQueue[client] = -1;
|
|
}
|
|
else if(CurrentTeam <= CS_TEAM_SPECTATOR)
|
|
continue;
|
|
|
|
if(NewTeam == CurrentTeam)
|
|
continue;
|
|
|
|
if(NewTeam >= CS_TEAM_T)
|
|
CS_SwitchTeam(client, NewTeam);
|
|
else
|
|
ChangeClientTeam(client, NewTeam);
|
|
|
|
if(NewTeam >= CS_TEAM_T && !IsPlayerAlive(client))
|
|
CS_RespawnPlayer(client);
|
|
}
|
|
}
|
|
|
|
public void OnRoundEnd(Event event, const char[] name, bool dontBroadcast)
|
|
{
|
|
g_bRoundEnded = true;
|
|
g_bZombieSpawned = false;
|
|
}
|
|
|
|
public Action CS_OnTerminateRound(float &delay, CSRoundEndReason &reason)
|
|
{
|
|
if(g_bWarmup)
|
|
return Plugin_Handled;
|
|
|
|
return Plugin_Continue;
|
|
}
|
|
|
|
public Action ZR_OnClientInfect(int &client, int &attacker, bool &motherInfect, bool &respawnOverride, bool &respawn)
|
|
{
|
|
if(g_bWarmup)
|
|
return Plugin_Handled;
|
|
|
|
g_bZombieSpawned = true;
|
|
|
|
return Plugin_Continue;
|
|
}
|
|
|
|
public Action ZR_OnInfectCountdown()
|
|
{
|
|
if(g_bWarmup)
|
|
return Plugin_Handled;
|
|
|
|
return Plugin_Continue;
|
|
}
|
|
|
|
public int Native_InWarmup(Handle hPlugin, int numParams)
|
|
{
|
|
return g_bWarmup;
|
|
} |