545 lines
14 KiB
SourcePawn
545 lines
14 KiB
SourcePawn
//----------------------------------------------------------------------------------------------------
|
|
// Leader3 core: leader roster (index 0 = primary), add/remove/promote, lifecycle handling.
|
|
//----------------------------------------------------------------------------------------------------
|
|
|
|
bool IsValidClient(int client, bool nobots = true)
|
|
{
|
|
if (client <= 0 || client > MaxClients || !IsClientConnected(client) || (nobots && IsFakeClient(client)))
|
|
return false;
|
|
return IsClientInGame(client);
|
|
}
|
|
|
|
bool IsLeader(int client)
|
|
{
|
|
return client >= 1 && client <= MaxClients && g_bIsLeader[client];
|
|
}
|
|
|
|
int GetPrimaryLeader()
|
|
{
|
|
if (g_alLeaders.Length == 0)
|
|
return -1;
|
|
int client = GetClientOfUserId(g_alLeaders.Get(0));
|
|
return client > 0 ? client : -1;
|
|
}
|
|
|
|
bool IsPrimaryLeader(int client)
|
|
{
|
|
return client != -1 && client == GetPrimaryLeader();
|
|
}
|
|
|
|
// ZR_IsClientZombie/ZR_IsClientHuman throw on dead clients, so the alive check
|
|
// has to come first in both helpers.
|
|
bool IsLivingZombie(int client)
|
|
{
|
|
return IsPlayerAlive(client) && ZR_IsClientZombie(client);
|
|
}
|
|
|
|
bool IsLivingHuman(int client)
|
|
{
|
|
return IsPlayerAlive(client) && ZR_IsClientHuman(client);
|
|
}
|
|
|
|
// The single gate every visual effect checks.
|
|
bool IsEffectsActive(int client)
|
|
{
|
|
return IsLeader(client) && IsClientInGame(client) && IsLivingHuman(client);
|
|
}
|
|
|
|
int GetLeaderRosterIndex(int client)
|
|
{
|
|
if (!IsLeader(client))
|
|
return -1;
|
|
return g_alLeaders.FindValue(GetClientUserId(client));
|
|
}
|
|
|
|
// 'actor' is who performed the promotion (0 = console); -1 or self = no credit shown.
|
|
bool AddLeader(int client, bool asPrimary = false, int actor = -1)
|
|
{
|
|
if (!IsValidClient(client))
|
|
return false;
|
|
|
|
if (g_bIsLeader[client])
|
|
{
|
|
if (asPrimary)
|
|
Core_MakePrimary(client, actor);
|
|
return true;
|
|
}
|
|
|
|
if (g_alLeaders.Length >= g_cvMaxLeaders.IntValue)
|
|
{
|
|
PrintToChatAll("%sCannot add \x04%N\x01 as leader: the maximum of %d leaders is reached.", PREFIX, client, g_cvMaxLeaders.IntValue);
|
|
return false;
|
|
}
|
|
|
|
int oldPrimary = GetPrimaryLeader();
|
|
bool primary = asPrimary || g_alLeaders.Length == 0;
|
|
|
|
if (primary && g_alLeaders.Length > 0)
|
|
{
|
|
g_alLeaders.ShiftUp(0);
|
|
g_alLeaders.Set(0, GetClientUserId(client));
|
|
}
|
|
else
|
|
{
|
|
g_alLeaders.Push(GetClientUserId(client));
|
|
}
|
|
|
|
g_bIsLeader[client] = true;
|
|
|
|
// Fresh defaults: xray off, beacon off, no sprite, hold-to-show tracer.
|
|
g_iEffectFlags[client] = 0;
|
|
g_iSpriteChoice[client] = SPRITE_NONE;
|
|
g_iTracerMode[client] = TRACER_MODE_HOLD;
|
|
g_iLastButtons[client] = 0;
|
|
g_fNextTracerSend[client] = 0.0;
|
|
|
|
bool credit = (actor >= 0 && actor != client);
|
|
if (primary)
|
|
{
|
|
if (credit)
|
|
PrintToChatAll("%s\x04%N\x01: \x04%N\x01 is now the primary leader! Follow their markers and orders.", PREFIX, actor, client);
|
|
else
|
|
PrintToChatAll("%s\x04%N\x01 is now the leader! Follow their markers and orders.", PREFIX, client);
|
|
PrintToChat(client, "%sYou are now the \x04primary leader\x01! Type \x04!leader\x01 or press your \x04flashlight key\x01 for the menu.", PREFIX);
|
|
}
|
|
else
|
|
{
|
|
if (credit)
|
|
PrintToChatAll("%s\x04%N\x01: \x04%N\x01 is now a sub-leader!", PREFIX, actor, client);
|
|
else
|
|
PrintToChatAll("%s\x04%N\x01 is now a sub-leader!", PREFIX, client);
|
|
PrintToChat(client, "%sYou are now a \x04sub-leader\x01! Type \x04!leader\x01 or press your \x04flashlight key\x01 for the menu.", PREFIX);
|
|
}
|
|
|
|
Call_StartForward(g_fwdOnLeaderAdded);
|
|
Call_PushCell(client);
|
|
Call_PushCell(primary);
|
|
Call_Finish();
|
|
|
|
if (primary && oldPrimary != client)
|
|
FirePrimaryChanged(oldPrimary, client);
|
|
|
|
if (IsEffectsActive(client))
|
|
Effects_Resume(client);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Make a client the primary leader (adding them as leader if needed).
|
|
// 'actor' is who performed the promotion (0 = console); -1 or self = no credit shown.
|
|
void Core_MakePrimary(int client, int actor = -1)
|
|
{
|
|
if (!IsValidClient(client))
|
|
return;
|
|
|
|
if (!g_bIsLeader[client])
|
|
{
|
|
AddLeader(client, true, actor);
|
|
return;
|
|
}
|
|
|
|
int oldPrimary = GetPrimaryLeader();
|
|
if (oldPrimary == client)
|
|
return;
|
|
|
|
int idx = GetLeaderRosterIndex(client);
|
|
if (idx <= 0)
|
|
return;
|
|
|
|
g_alLeaders.Erase(idx);
|
|
g_alLeaders.ShiftUp(0);
|
|
g_alLeaders.Set(0, GetClientUserId(client));
|
|
|
|
if (actor >= 0 && actor != client)
|
|
PrintToChatAll("%s\x04%N\x01: \x04%N\x01 is now the primary leader!", PREFIX, actor, client);
|
|
else
|
|
PrintToChatAll("%s\x04%N\x01 is now the primary leader!", PREFIX, client);
|
|
FirePrimaryChanged(oldPrimary, client);
|
|
|
|
// Primary/sub xray colours differ: refresh both affected leaders.
|
|
Effects_RefreshXray(client);
|
|
if (oldPrimary != -1)
|
|
Effects_RefreshXray(oldPrimary);
|
|
}
|
|
|
|
void RemoveLeaderClient(int client, const char[] reason, bool announce = true)
|
|
{
|
|
int idx = GetLeaderRosterIndex(client);
|
|
if (idx == -1)
|
|
return;
|
|
|
|
bool wasPrimary = (idx == 0);
|
|
|
|
Effects_Suspend(client);
|
|
Marker_ClearByOwner(client, false);
|
|
g_alLeaders.Erase(idx);
|
|
g_bIsLeader[client] = false;
|
|
g_iEffectFlags[client] = 0;
|
|
g_iSpriteChoice[client] = SPRITE_NONE;
|
|
|
|
if (announce)
|
|
PrintToChatAll("%s\x04%N\x01 is no longer a leader (%s).", PREFIX, client, reason);
|
|
|
|
Call_StartForward(g_fwdOnLeaderRemoved);
|
|
Call_PushCell(client);
|
|
Call_Finish();
|
|
|
|
if (wasPrimary)
|
|
{
|
|
int newPrimary = GetPrimaryLeader();
|
|
if (newPrimary != -1)
|
|
{
|
|
PrintToChatAll("%s\x04%N\x01 has been promoted to primary leader!", PREFIX, newPrimary);
|
|
Effects_RefreshXray(newPrimary);
|
|
}
|
|
FirePrimaryChanged(client, newPrimary);
|
|
}
|
|
}
|
|
|
|
void Core_RemoveAllLeaders(bool announce = false, int actor = -1)
|
|
{
|
|
for (int i = 1; i <= MaxClients; i++)
|
|
{
|
|
if (g_bIsLeader[i])
|
|
{
|
|
Effects_Suspend(i);
|
|
Marker_ClearByOwner(i, false);
|
|
g_bIsLeader[i] = false;
|
|
g_iEffectFlags[i] = 0;
|
|
g_iSpriteChoice[i] = SPRITE_NONE;
|
|
|
|
Call_StartForward(g_fwdOnLeaderRemoved);
|
|
Call_PushCell(i);
|
|
Call_Finish();
|
|
}
|
|
}
|
|
int oldPrimary = GetPrimaryLeader();
|
|
g_alLeaders.Clear();
|
|
if (oldPrimary != -1)
|
|
FirePrimaryChanged(oldPrimary, -1);
|
|
|
|
if (announce)
|
|
{
|
|
if (actor >= 0)
|
|
PrintToChatAll("%sAll leaders have been removed by \x04%N\x01.", PREFIX, actor);
|
|
else
|
|
PrintToChatAll("%sAll leaders have been removed.", PREFIX);
|
|
}
|
|
}
|
|
|
|
void FirePrimaryChanged(int oldLeader, int newLeader)
|
|
{
|
|
Call_StartForward(g_fwdOnPrimaryChanged);
|
|
Call_PushCell(oldLeader);
|
|
Call_PushCell(newLeader);
|
|
Call_Finish();
|
|
}
|
|
|
|
// A leader died or got infected: effects off, markers and leadership stay.
|
|
void Core_OnLeaderIncapacitated(int client, const char[] verb, bool announce = true)
|
|
{
|
|
if (!IsLeader(client))
|
|
return;
|
|
|
|
Effects_Suspend(client);
|
|
|
|
if (!announce)
|
|
return;
|
|
|
|
char remaining[256];
|
|
int count = Core_GetActiveLeaderNames(client, remaining, sizeof(remaining));
|
|
|
|
if (count == 0)
|
|
PrintToChatAll("%s\x04%N\x01 %s! \x07FF4040No leaders remain!", PREFIX, client, verb);
|
|
else if (count == 1)
|
|
PrintToChatAll("%s\x04%N\x01 %s! Remaining leader: \x04%s\x01.", PREFIX, client, verb, remaining);
|
|
else
|
|
PrintToChatAll("%s\x04%N\x01 %s! Remaining leaders: \x04%s\x01.", PREFIX, client, verb, remaining);
|
|
}
|
|
|
|
// List the leaders still standing (alive humans), excluding 'exclude'. Roster order, primary first.
|
|
int Core_GetActiveLeaderNames(int exclude, char[] buf, int maxlen)
|
|
{
|
|
int count = 0;
|
|
char name[MAX_NAME_LENGTH];
|
|
|
|
for (int i = 0; i < g_alLeaders.Length; i++)
|
|
{
|
|
int leader = GetClientOfUserId(g_alLeaders.Get(i));
|
|
if (leader <= 0 || leader == exclude)
|
|
continue;
|
|
|
|
if (!IsLivingHuman(leader))
|
|
continue;
|
|
|
|
GetClientName(leader, name, sizeof(name));
|
|
if (buf[0])
|
|
StrCat(buf, maxlen, "\x01, \x04");
|
|
StrCat(buf, maxlen, name);
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
void Core_OnClientDisconnect(int client)
|
|
{
|
|
if (g_bIsLeader[client])
|
|
RemoveLeaderClient(client, "disconnected");
|
|
|
|
g_iEffectFlags[client] = 0;
|
|
g_iSpriteChoice[client] = SPRITE_NONE;
|
|
g_iTracerMode[client] = TRACER_MODE_HOLD;
|
|
g_bHasCustomColour[client] = false;
|
|
g_iXrayRef[client] = INVALID_ENT_REFERENCE;
|
|
g_iSpriteRef[client] = INVALID_ENT_REFERENCE;
|
|
g_iBeaconSerial[client] = 0;
|
|
g_iLastButtons[client] = 0;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------------------
|
|
// Commands
|
|
//----------------------------------------------------------------------------------------------------
|
|
public Action Cmd_Leader(int client, int args)
|
|
{
|
|
bool admin = CheckCommandAccess(client, "sm_admin", ADMFLAG_GENERIC, false);
|
|
|
|
if (args >= 1)
|
|
{
|
|
if (!admin)
|
|
{
|
|
ReplyToCommand(client, "%s Only admins can assign leaders. Use !voteleader <name> instead.", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
char arg[65], targetName[MAX_TARGET_LENGTH];
|
|
int targets[MAXPLAYERS];
|
|
bool tnIsMl;
|
|
GetCmdArg(1, arg, sizeof(arg));
|
|
|
|
int count = ProcessTargetString(arg, client, targets, sizeof(targets),
|
|
COMMAND_FILTER_ALIVE | COMMAND_FILTER_NO_BOTS, targetName, sizeof(targetName), tnIsMl);
|
|
if (count <= 0)
|
|
{
|
|
ReplyToTargetError(client, count);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
int target = targets[i];
|
|
if (g_bIsLeader[target])
|
|
{
|
|
ReplyToCommand(client, "%s %N is already a leader.", PREFIX_PLAIN, target);
|
|
continue;
|
|
}
|
|
if (ZR_IsClientZombie(target))
|
|
{
|
|
ReplyToCommand(client, "%s %N has to be a human!", PREFIX_PLAIN, target);
|
|
continue;
|
|
}
|
|
if (AddLeader(target, false, client) && count == 1)
|
|
ShowLeaderMenu(target);
|
|
}
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
if (client == 0)
|
|
{
|
|
ReplyToCommand(client, "%s Usage: sm_leader <target>", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
if (g_bIsLeader[client])
|
|
{
|
|
ShowLeaderMenu(client);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
// Admins may always self-assign as an override; whitelisted players may too.
|
|
if (admin || Whitelist_IsWhitelisted(client))
|
|
{
|
|
if (!IsPlayerAlive(client))
|
|
{
|
|
ReplyToCommand(client, "%s You have to be alive!", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
if (ZR_IsClientZombie(client))
|
|
{
|
|
ReplyToCommand(client, "%s You have to be a human!", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
if (AddLeader(client))
|
|
ShowLeaderMenu(client);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
ReplyToCommand(client, "%s You are not a leader. Ask players to vote for you with !voteleader <your name>, or ask an admin to whitelist you.", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
void Core_AnnounceLeaders()
|
|
{
|
|
char buf[512], name[MAX_NAME_LENGTH];
|
|
int count = 0;
|
|
|
|
// Index 0 is the primary leader, so the roster order already puts them first.
|
|
for (int i = 0; i < g_alLeaders.Length; i++)
|
|
{
|
|
int leader = GetClientOfUserId(g_alLeaders.Get(i));
|
|
if (leader <= 0)
|
|
continue;
|
|
|
|
GetClientName(leader, name, sizeof(name));
|
|
if (buf[0])
|
|
StrCat(buf, sizeof(buf), "\x01, \x04");
|
|
StrCat(buf, sizeof(buf), name);
|
|
count++;
|
|
}
|
|
|
|
if (count == 1)
|
|
PrintToChatAll("%sYour leader for this round is \x04%s\x01.", PREFIX, buf);
|
|
else if (count > 1)
|
|
PrintToChatAll("%sYour leaders for this round are \x04%s\x01.", PREFIX, buf);
|
|
}
|
|
|
|
public Action Cmd_Leaders(int client, int args)
|
|
{
|
|
if (g_alLeaders.Length == 0)
|
|
{
|
|
ReplyToCommand(client, "%s There are no leaders right now.", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
char buf[512], name[MAX_NAME_LENGTH];
|
|
for (int i = 0; i < g_alLeaders.Length; i++)
|
|
{
|
|
int leader = GetClientOfUserId(g_alLeaders.Get(i));
|
|
if (leader <= 0)
|
|
continue;
|
|
|
|
GetClientName(leader, name, sizeof(name));
|
|
if (buf[0])
|
|
StrCat(buf, sizeof(buf), ", ");
|
|
StrCat(buf, sizeof(buf), name);
|
|
if (i == 0)
|
|
StrCat(buf, sizeof(buf), " [PRIMARY]");
|
|
if (!IsLivingHuman(leader))
|
|
StrCat(buf, sizeof(buf), " (dead)");
|
|
}
|
|
ReplyToCommand(client, "%s Current leaders: %s", PREFIX_PLAIN, buf);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
public Action Cmd_LeaderResign(int client, int args)
|
|
{
|
|
if (client == 0)
|
|
{
|
|
ReplyToCommand(client, "%s This command can only be used in-game.", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
if (!g_bIsLeader[client])
|
|
{
|
|
ReplyToCommand(client, "%s You are not a leader.", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
RemoveLeaderClient(client, "resigned");
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
public Action Cmd_PrimaryLeader(int client, int args)
|
|
{
|
|
if (args < 1)
|
|
{
|
|
ReplyToCommand(client, "%s Usage: sm_primaryleader <target>", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
char arg[65], targetName[MAX_TARGET_LENGTH];
|
|
int targets[MAXPLAYERS];
|
|
bool tnIsMl;
|
|
GetCmdArg(1, arg, sizeof(arg));
|
|
|
|
int count = ProcessTargetString(arg, client, targets, sizeof(targets),
|
|
COMMAND_FILTER_ALIVE | COMMAND_FILTER_NO_BOTS, targetName, sizeof(targetName), tnIsMl);
|
|
if (count <= 0)
|
|
{
|
|
ReplyToTargetError(client, count);
|
|
return Plugin_Handled;
|
|
}
|
|
if (count > 1)
|
|
{
|
|
ReplyToCommand(client, "%s There can only be one primary leader, pick a single player.", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
int target = targets[0];
|
|
if (ZR_IsClientZombie(target))
|
|
{
|
|
ReplyToCommand(client, "%s %N has to be a human!", PREFIX_PLAIN, target);
|
|
return Plugin_Handled;
|
|
}
|
|
if (IsPrimaryLeader(target))
|
|
{
|
|
ReplyToCommand(client, "%s %N is already the primary leader.", PREFIX_PLAIN, target);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
Core_MakePrimary(target, client);
|
|
LogAction(client, target, "\"%L\" made \"%L\" the primary leader", client, target);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
public Action Cmd_RemoveLeader(int client, int args)
|
|
{
|
|
if (args == 0)
|
|
{
|
|
if (g_alLeaders.Length == 0)
|
|
{
|
|
ReplyToCommand(client, "%s There is no leader to remove.", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
Core_RemoveAllLeaders(true, client);
|
|
LogAction(client, -1, "\"%L\" removed all leaders", client);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
char arg[65], targetName[MAX_TARGET_LENGTH];
|
|
int targets[MAXPLAYERS];
|
|
bool tnIsMl;
|
|
GetCmdArg(1, arg, sizeof(arg));
|
|
|
|
int count = ProcessTargetString(arg, client, targets, sizeof(targets),
|
|
COMMAND_FILTER_CONNECTED | COMMAND_FILTER_NO_BOTS, targetName, sizeof(targetName), tnIsMl);
|
|
if (count <= 0)
|
|
{
|
|
ReplyToTargetError(client, count);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
int removed = 0;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
int target = targets[i];
|
|
|
|
if (!g_bIsLeader[target])
|
|
{
|
|
if (count == 1)
|
|
ReplyToCommand(client, "%s %N is not a leader.", PREFIX_PLAIN, target);
|
|
continue;
|
|
}
|
|
|
|
char reason[80];
|
|
Format(reason, sizeof(reason), "removed by \x04%N\x01", client);
|
|
RemoveLeaderClient(target, reason);
|
|
LogAction(client, target, "\"%L\" removed leader \"%L\"", client, target);
|
|
removed++;
|
|
}
|
|
|
|
if (count > 1)
|
|
ReplyToCommand(client, "%s Removed %d leader(s) in %s.", PREFIX_PLAIN, removed, targetName);
|
|
return Plugin_Handled;
|
|
}
|