sm-plugins/FakeClients/scripting/FakeClients_nide.sp

481 lines
13 KiB
SourcePawn

#pragma semicolon 1
#pragma newdecls required
#include <sourcemod>
#include <sdktools>
#include <cstrike>
#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");
}