#pragma semicolon 1 #pragma newdecls required #include #include #include #define MAX_TIERS 32 ConVar g_hCount; ConVar g_hDelay; ConVar g_hUseTiers; ArrayList g_hNames; int g_iTierThreshold[MAX_TIERS]; // real player count that triggers this tier int g_iTierMaxBots[MAX_TIERS]; // max bots allowed for this tier int g_iTierCount = 0; int g_iPendingBots = 0; bool g_bUseTiers = false; public Plugin myinfo = { name = "FakeClients", author = "Tsunami, .Rushaway", description = "Put fake clients in server with tier system", version = "3.0.0", url = "https://github.com/srcdslab/sm-plugin-FakeClients" } public void OnPluginStart() { g_hCount = CreateConVar("sm_fakeclients_players", "8", "Fallback: number of bots when tier system is disabled", _, true, 0.0, true, 64.0); g_hDelay = CreateConVar("sm_fakeclients_delay", "120", "Delay after map change before fake clients join (seconds)", _, true, 0.0, true, 10000.0); g_hUseTiers = CreateConVar("sm_fakeclients_tiers", "0", "Use tier system from fakeclients_tiers.cfg (1 = enabled, 0 = disabled)", _, true, 0.0, true, 1.0); g_bUseTiers = g_hUseTiers.BoolValue; g_hUseTiers.AddChangeHook(OnConVarChanged); RegAdminCmd("sm_debugfakes", Command_DebugFakes, ADMFLAG_GENERIC, "Shows the amount of fake-clients on server"); RegAdminCmd("sm_fakes", Command_Fakes, ADMFLAG_GENERIC, "Shows the fake-clients on server"); AutoExecConfig(true); } public Action Command_DebugFakes(int client, int argc) { int iFakes = 0; int iFakesInTeam = 0; int iPlayers = GetClientCount(false); for(int i = 1; i <= MaxClients; i++) { if (!IsClientConnected(i)) { continue; } if (IsFakeClient(i) || IsClientSourceTV(i)) { iFakes++; iPlayers--; } if (IsFakeClient(i) && GetClientTeam(i) > CS_TEAM_SPECTATOR) iFakesInTeam++; } ReplyToCommand(client, "[SM] There are currently %d Fake-Clients, from which %d are in Spectate. Real Clients %d", iFakes, iFakes - iFakesInTeam, iPlayers); return Plugin_Handled; } //---------------------------------------------------------------------------------------------------- // Purpose: //---------------------------------------------------------------------------------------------------- public Action Command_Fakes(int client, int args) { char aBuf[1024]; char aBuf2[MAX_NAME_LENGTH]; for(int i = 1; i <= MaxClients; i++) { if(IsClientInGame(i)) { if(IsClientConnected(i) && IsFakeClient(i) && !IsClientSourceTV(i)) { GetClientName(i, aBuf2, sizeof(aBuf2)); StrCat(aBuf, sizeof(aBuf), aBuf2); StrCat(aBuf, sizeof(aBuf), ", "); } } } if(strlen(aBuf)) { aBuf[strlen(aBuf) - 2] = 0; ReplyToCommand(client, "[SM] Fake-Clients online: %s", aBuf); } else ReplyToCommand(client, "[SM] Fake-Clients online: none"); return Plugin_Handled; } public void OnMapStart() { ParseNames(); ParseTiers(); CreateTimer(g_hDelay.FloatValue, Timer_CreateFakeClients, _, TIMER_FLAG_NO_MAPCHANGE); } public void OnConVarChanged(ConVar hConVar, const char[] sOldValue, const char[] sNewValue) { if (hConVar == g_hUseTiers) { g_bUseTiers = g_hUseTiers.BoolValue; LogMessage("Tier system %s", g_bUseTiers ? "enabled" : "disabled"); if (g_bUseTiers) { g_iPendingBots = 0; for (int i = 1; i <= MaxClients; i++) { if (!IsClientConnected(i) || !IsClientInGame(i)) continue; if (!IsFakeClient(i) || IsClientSourceTV(i)) continue; KickClient(i, "Client Disconnect"); } // Wait a moment for all bots to be kicked before trying to add new ones according to tiers. CreateTimer(1.0, Timer_CreateFakeClients, _, TIMER_FLAG_NO_MAPCHANGE); } else { AdjustFakeClientsToTier(); } } } /** * Returns the target bot count based on the current number of real players. * Falls back to sm_fakeclients_players if tiers are disabled or not loaded. */ int GetTargetBotCount(int iRealPlayers) { if (!g_bUseTiers || g_iTierCount == 0) return g_hCount.IntValue; // Walk tiers from highest threshold down, pick the first one that applies for (int t = g_iTierCount - 1; t >= 0; t--) { if (iRealPlayers >= g_iTierThreshold[t]) return g_iTierMaxBots[t]; } // No tier matched (e.g. config missing threshold "0") LogMessage("Warning: no tier matched for %d real players, defaulting to 0 bots", iRealPlayers); return 0; } /** * Computes the clamped target bot count, accounting for available slots and * server over-capacity. Shared by AdjustFakeClientsToTier and OnClientPutInServer. */ int ComputeTarget(int iBots, int iRealPlayers, int iReservedSlots) { int iTarget = GetTargetBotCount(iRealPlayers); if (iTarget < 0) iTarget = 0; // Bots currently being added already count as occupied slots int iEffectiveBots = iBots + g_iPendingBots; int iFreeSlots = MaxClients - (iRealPlayers + iEffectiveBots + iReservedSlots); if (iFreeSlots < 0) { iTarget = iEffectiveBots + iFreeSlots; if (iTarget < 0) iTarget = 0; } else { int iMaxBotsBySlots = iEffectiveBots + iFreeSlots; if (iTarget > iMaxBotsBySlots) iTarget = iMaxBotsBySlots; } return iTarget; } /** * Kicks excess bots until the bot count reaches iTarget. * Never kicks real players or SourceTV. */ void KickExcessBots(int iBots, int iRealPlayers, int iTarget) { int iToKick = iBots - iTarget; int iKicked = 0; for (int i = 1; i <= MaxClients && iKicked < iToKick; i++) { if (!IsClientConnected(i) || !IsClientInGame(i)) continue; // Safety: never kick real players or SourceTV if (!IsFakeClient(i) || IsClientSourceTV(i)) continue; LogMessage("Kicking fake client '%N' (slot %d) — bots:%d target:%d", i, i, iBots, iTarget); KickClient(i, "Client Disconnect"); iKicked++; } if (iKicked > 0) LogMessage("Kicked %d bot(s) — real:%d bots:%d target:%d slots:%d", iKicked, iRealPlayers, iBots, iTarget, MaxClients); } /** * Schedules staggered timers to add bots up to iToAdd, respecting free slots. */ void ScheduleBotsToAdd(int iToAdd, int iFreeSlots) { if (iFreeSlots <= 0 || iToAdd <= 0) return; if (iToAdd > iFreeSlots) iToAdd = iFreeSlots; // Keep a staggered cadence but add jitter so joins look less scripted. float fNextDelay = GetRandomFloat(0.4, 1.2); for (int j = 0; j < iToAdd; j++) { g_iPendingBots++; CreateTimer(fNextDelay, Timer_CreateFakeClient, _, TIMER_FLAG_NO_MAPCHANGE); fNextDelay += GetRandomFloat(0.7, 1.9); } } /** * Compares current bot count against the target and either kicks excess bots * or schedules new ones to fill the gap. */ void AdjustFakeClientsToTier() { int iBots, iRealPlayers, iReservedSlots; CollectClientCounts(iBots, iRealPlayers, iReservedSlots); int iTarget = ComputeTarget(iBots, iRealPlayers, iReservedSlots); int iFreeSlots = MaxClients - (iRealPlayers + iBots + iReservedSlots); // Too many active bots -> kick extras if (iBots > iTarget) { g_iPendingBots = 0; KickExcessBots(iBots, iRealPlayers, iTarget); } // Active + pending bots are below target -> add missing bots else if ((iBots + g_iPendingBots) < iTarget) ScheduleBotsToAdd(iTarget - iBots - g_iPendingBots, iFreeSlots); } /** * Collects fake bots, real players and reserved slots in a single client loop. */ void CollectClientCounts(int &iBots, int &iRealPlayers, int &iReservedSlots) { iBots = 0; iRealPlayers = 0; bool bHasSourceTV = false; for (int i = 1; i <= MaxClients; i++) { if (!IsClientConnected(i)) continue; if (IsClientSourceTV(i)) { bHasSourceTV = true; continue; // SourceTV occupies its own slot in MaxClients — do not count it as a bot } if (IsFakeClient(i)) iBots++; else iRealPlayers++; } // Reserve 1 free slot so a real player can always connect. // When SourceTV is active, reserve 1 extra (SourceTV's slot is already in MaxClients). iReservedSlots = bHasSourceTV ? 2 : 1; } public void OnClientPutInServer(int client) { // Skip fake clients: bot additions are managed via timers. // Calling AdjustFakeClientsToTier on every bot join would cause cascading timers. if (!client || IsFakeClient(client)) return; // A real player joined: only kick excess bots, never schedule additions. // Scheduling here would race with the staggered timers already in flight. int iBots, iRealPlayers, iReservedSlots; CollectClientCounts(iBots, iRealPlayers, iReservedSlots); int iTarget = ComputeTarget(iBots, iRealPlayers, iReservedSlots); if (iBots > iTarget) KickExcessBots(iBots, iRealPlayers, iTarget); } public void OnClientDisconnect(int client) { // Ignore bot disconnects (caused by our own kicks) to avoid cascading timers. if (IsFakeClient(client)) return; CreateTimer(0.5, Timer_CreateFakeClients, _, TIMER_FLAG_NO_MAPCHANGE); } public Action Timer_CreateFakeClient(Handle timer) { g_iPendingBots--; if (g_iPendingBots < 0) g_iPendingBots = 0; int iBots, iRealPlayers, iReservedSlots; CollectClientCounts(iBots, iRealPlayers, iReservedSlots); int iTarget = ComputeTarget(iBots, iRealPlayers, iReservedSlots); int iFreeSlots = MaxClients - (iRealPlayers + iBots + iReservedSlots); // Recheck: tier may have changed since this timer was scheduled if (iFreeSlots <= 0 || iBots >= iTarget) return Plugin_Handled; char sName[MAX_NAME_LENGTH]; char sTarget[MAX_TARGET_LENGTH]; int iTargets[MAXPLAYERS]; bool bTN_Is_ML; // Pick a random name, re-roll if it's already taken by a fake client. // Stop after trying all available names to prevent an infinite loop when // the name list is smaller than the number of bots (duplicates are allowed). // If no names are configured, CreateFakeClient will assign a default name based on the engine. int iNameCount = g_hNames.Length; if (iNameCount > 0) { g_hNames.GetString(GetRandomInt(0, iNameCount - 1), sName, sizeof(sName)); for (int iAttempt = 1; iAttempt < iNameCount; iAttempt++) { if (ProcessTargetString(sName, 0, iTargets, MAXPLAYERS, COMMAND_FILTER_NO_MULTI, sTarget, MAX_TARGET_LENGTH, bTN_Is_ML) != 1 || !IsFakeClient(iTargets[0])) break; g_hNames.GetString(GetRandomInt(0, iNameCount - 1), sName, sizeof(sName)); } } CreateFakeClient(sName); return Plugin_Handled; } public Action Timer_CreateFakeClients(Handle timer) { AdjustFakeClientsToTier(); return Plugin_Continue; } /** * Loads tier thresholds from configs/fakeclients_tiers.cfg. Max 32 tiers. * Format (KeyValues): * * "FakeClientsTiers" * { * "0" "12" // 0 real players → up to 12 bots * "5" "10" // 5+ real players → up to 10 bots * "15" "8" // And so on... * } * * Entries are sorted by threshold automatically, so order in the file * does not matter. */ stock void ParseTiers() { if (!g_bUseTiers) return; g_iTierCount = 0; char sPath[PLATFORM_MAX_PATH]; BuildPath(Path_SM, sPath, sizeof(sPath), "configs/fakeclients_tiers.cfg"); KeyValues kv = new KeyValues("FakeClientsTiers"); if (!kv.ImportFromFile(sPath)) { LogError("configs/fakeclients_tiers.cfg not found — falling back to sm_fakeclients_players"); g_hUseTiers.BoolValue = false; delete kv; return; } if (!kv.GotoFirstSubKey(false)) { LogError("configs/fakeclients_tiers.cfg is empty or malformed"); g_hUseTiers.BoolValue = false; delete kv; return; } do { if (g_iTierCount >= MAX_TIERS) break; char sKey[16], sVal[16]; kv.GetSectionName(sKey, sizeof(sKey)); kv.GetString(NULL_STRING, sVal, sizeof(sVal), "-1"); int iMaxBots = StringToInt(sVal); if (iMaxBots < 0) continue; // skip malformed lines g_iTierThreshold[g_iTierCount] = StringToInt(sKey); g_iTierMaxBots[g_iTierCount] = iMaxBots; g_iTierCount++; } while (kv.GotoNextKey(false)); delete kv; // Bubble sort tiers by threshold (ascending) so GetTargetBotCount() works correctly for (int i = 0; i < g_iTierCount - 1; i++) { for (int j = 0; j < g_iTierCount - 1 - i; j++) { if (g_iTierThreshold[j] > g_iTierThreshold[j + 1]) { int tmp; tmp = g_iTierThreshold[j]; g_iTierThreshold[j] = g_iTierThreshold[j + 1]; g_iTierThreshold[j + 1] = tmp; tmp = g_iTierMaxBots[j]; g_iTierMaxBots[j] = g_iTierMaxBots[j + 1]; g_iTierMaxBots[j + 1] = tmp; } } } LogMessage("Loaded %d tier(s) from fakeclients_tiers.cfg", g_iTierCount); } /** * Loads bot display names from configs/fakeclients.txt (one name per line). */ stock void ParseNames() { delete g_hNames; g_hNames = new ArrayList(MAX_NAME_LENGTH); char sBuffer[256]; BuildPath(Path_SM, sBuffer, sizeof(sBuffer), "configs/fakeclients.txt"); File hConfig = OpenFile(sBuffer, "r"); if (!hConfig) { LogError("configs/fakeclients.txt not found — using default engine name"); return; } while (hConfig.ReadLine(sBuffer, sizeof(sBuffer))) { TrimString(sBuffer); if (strlen(sBuffer) > 0) g_hNames.PushString(sBuffer); } delete hConfig; if (!g_hNames.Length) LogError("configs/fakeclients.txt is empty — using default engine name"); }