#include #include #include #include #include #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, .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; }