lots of changes

This commit is contained in:
2026-10-22 16:17:56 +01:00
parent e4a75367aa
commit ac3ffd5be2
53 changed files with 7716 additions and 193 deletions
+536
View File
@@ -0,0 +1,536 @@
#pragma semicolon 1
#define LEADER3_CORE
#include <sourcemod>
#include <sdktools>
#include <sdkhooks>
#include <cstrike>
#include <clientprefs>
#include <zombiereloaded>
#include <basecomm>
#include <voice>
#include <leader>
#include <dhooks>
#undef REQUIRE_PLUGIN
#include <AFKManager>
#define REQUIRE_PLUGIN
#pragma newdecls required
#define PLUGIN_VERSION "3.0"
#define PREFIX "\x07FF00E1[Leader] \x01"
#define PREFIX_PLAIN "[Leader]"
#define MARKER_MODEL "models/unloze/unloze_ping.mdl"
#define MARKER_MODEL_HEIGHT 80.0 // hull_max.z of unloze_ping.mdl
#define MARKER_MODEL_SCALE 1.5
#define MARKER_TEXT_CLEARANCE 24.0 // headroom kept between the title text and a ceiling above it
#define SPRITE_DEFEND "materials/nide/defend.vmt"
#define SPRITE_FOLLOW "materials/nide/follow.vmt"
// Stock ct_urban patched to pull its materials from materials/models/leader3/glow_ct/
// (additive + $ignorez -> visible through walls). Bonemerges onto any stock CT skeleton.
// v2: IK chains/rules stripped (header-patched) -- the decompiled ragdoll sequence's IK
// yanked hand/foot bones to world origin on frames where the parent's bones were unavailable.
#define XRAY_MODEL "models/leader3/glow_ct2.mdl"
// m_fEffects bits (not in SM includes) used by the xray shell
#define EF_BONEMERGE (1 << 0)
#define EF_NOSHADOW (1 << 4)
#define EF_NORECEIVESHADOW (1 << 6)
#define EF_BONEMERGE_FASTCULL (1 << 7)
#define EF_PARENT_ANIMATES (1 << 9)
// unloze_ping.mdl skins
#define SKIN_STAR 0
#define SKIN_SKULL 1
#define SKIN_MOON 2
#define SKIN_CROSS 3
// g_iEffectFlags bits
#define EFFECT_XRAY (1 << 0)
#define EFFECT_BEACON (1 << 1)
#define EFFECT_TRACER_TOGGLED (1 << 2)
// g_iSpriteChoice
#define SPRITE_NONE -1
#define SPRITE_CHOICE_DEFEND 0
#define SPRITE_CHOICE_FOLLOW 1
// g_iTracerMode
#define TRACER_MODE_TAP 0 // hold +use OR tap to toggle
#define TRACER_MODE_HOLD 1 // hold +use only
#define TRACER_MODE_OFF 2
// Leader roster: userids, index 0 = primary
ArrayList g_alLeaders;
bool g_bIsLeader[MAXPLAYERS + 1];
// Per-leader effect state
int g_iEffectFlags[MAXPLAYERS + 1];
int g_iSpriteChoice[MAXPLAYERS + 1] = { SPRITE_NONE, ... };
int g_iTracerMode[MAXPLAYERS + 1];
// Per-leader colour override for xray/beacon/tracer; persisted as a cookie ("RRGGBB" hex).
bool g_bHasCustomColour[MAXPLAYERS + 1];
int g_iCustomColour[MAXPLAYERS + 1][3];
Cookie g_hColourCookie;
int g_iXrayRef[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... };
// ShouldTransmit vtable hook armed on each xray shell; null if gamedata failed.
Handle g_hXrayShouldTransmit;
int g_iSpriteRef[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... };
int g_iBeaconSerial[MAXPLAYERS + 1];
int g_iSerialGen;
int g_iLastButtons[MAXPLAYERS + 1];
float g_fUsePressedAt[MAXPLAYERS + 1];
float g_fNextTracerSend[MAXPLAYERS + 1];
// Beam sprites for beacon/tracer
int g_iBeamSprite = -1;
int g_iHaloSprite = -1;
// Leader whitelist (persisted in the SourceMod database)
Database g_hWhitelistDB;
bool g_bLeaderWhitelisted[MAXPLAYERS + 1];
// Force-mute
bool g_bForceLeaderMute;
bool g_bForceMuted[MAXPLAYERS + 1];
bool g_bAFKManager;
bool g_bWorldTextBroken;
GlobalForward g_fwdOnLeaderAdded;
GlobalForward g_fwdOnLeaderRemoved;
GlobalForward g_fwdOnPrimaryChanged;
ConVar g_cvMaxLeaders;
ConVar g_cvAllowVotes;
ConVar g_cvVoteRatio;
ConVar g_cvVoteAfkTime;
ConVar g_cvMarkerLimit;
ConVar g_cvBeaconRadius;
ConVar g_cvBeaconInterval;
ConVar g_cvTracerLife;
ConVar g_cvTracerWidth;
ConVar g_cvTracerInterval;
ConVar g_cvTracerTapTime;
ConVar g_cvXrayColourPrimary;
ConVar g_cvXrayColourSub;
ConVar g_cvXrayAlpha;
ConVar g_cvDeathSound;
int g_iXrayColourPrimary[4] = {0, 255, 0, 255};
int g_iXrayColourSub[4] = {0, 150, 255, 255};
#include "leader3/core.sp"
#include "leader3/whitelist.sp"
#include "leader3/effects.sp"
#include "leader3/markers.sp"
#include "leader3/chat.sp"
#include "leader3/vote.sp"
#include "leader3/menu.sp"
public Plugin myinfo =
{
name = "Leader3",
author = "marisakirisame.net",
description = "ZE leader plugin: multiple leaders, effects, persistent markers with titles.",
version = PLUGIN_VERSION,
url = ""
};
public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
CreateNative("Leader_CurrentLeader", Native_CurrentLeader);
CreateNative("Leader_SetLeader", Native_SetLeader);
CreateNative("Leader_Is", Native_Is);
CreateNative("Leader_GetLeaderCount", Native_GetLeaderCount);
CreateNative("Leader_GetLeader", Native_GetLeader);
RegPluginLibrary("leader");
return APLRes_Success;
}
public void OnPluginStart()
{
LoadTranslations("common.phrases");
LoadTranslations("core.phrases");
g_alLeaders = new ArrayList();
Markers_OnPluginStart();
Whitelist_OnPluginStart();
Xray_SetupForceTransmit();
g_hColourCookie = new Cookie("leader3_colour", "Leader3 custom effect colour (RRGGBB hex)", CookieAccess_Protected);
for (int i = 1; i <= MaxClients; i++)
{
if (IsClientInGame(i) && !IsFakeClient(i) && AreClientCookiesCached(i))
LeaderColour_LoadCookie(i);
}
g_fwdOnLeaderAdded = new GlobalForward("Leader_OnLeaderAdded", ET_Ignore, Param_Cell, Param_Cell);
g_fwdOnLeaderRemoved = new GlobalForward("Leader_OnLeaderRemoved", ET_Ignore, Param_Cell);
g_fwdOnPrimaryChanged = new GlobalForward("Leader_OnPrimaryChanged", ET_Ignore, Param_Cell, Param_Cell);
CreateConVar("sm_leader_version", PLUGIN_VERSION, "Leader3 version", FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY);
g_cvMaxLeaders = CreateConVar("sm_leader_max", "4", "Maximum concurrent leaders", FCVAR_NOTIFY, true, 1.0, true, 16.0);
g_cvAllowVotes = CreateConVar("sm_leader_allow_votes", "1", "Allow !voteleader", FCVAR_NOTIFY);
g_cvVoteRatio = CreateConVar("sm_leader_vote_ratio", "0.60", "Ratio of eligible voters needed for !voteleader", FCVAR_NOTIFY, true, 0.05, true, 1.0);
g_cvVoteAfkTime = CreateConVar("sm_leader_vote_afk_time", "180", "Seconds of idle time before a player no longer counts as an eligible voter", FCVAR_NOTIFY);
g_cvMarkerLimit = CreateConVar("sm_leader_marker_limit", "4", "Max markers per leader (one per type, max 4)", FCVAR_NOTIFY, true, 1.0, true, 4.0);
g_cvBeaconRadius = CreateConVar("sm_leader_beacon_radius", "375.0", "Beacon ring max radius", FCVAR_NOTIFY);
g_cvBeaconInterval = CreateConVar("sm_leader_beacon_interval", "1.0", "Beacon ring interval", FCVAR_NOTIFY, true, 0.1);
g_cvTracerLife = CreateConVar("sm_leader_tracer_life", "0.1", "Tracer beam life", FCVAR_NOTIFY);
g_cvTracerWidth = CreateConVar("sm_leader_tracer_width", "2.0", "Tracer beam width", FCVAR_NOTIFY);
g_cvTracerInterval = CreateConVar("sm_leader_tracer_interval", "0.05", "Seconds between tracer beam sends", FCVAR_NOTIFY, true, 0.05);
g_cvTracerTapTime = CreateConVar("sm_leader_tracer_tap_time", "0.3", "Max +use press duration counted as a tap (toggle)", FCVAR_NOTIFY);
g_cvXrayColourPrimary = CreateConVar("sm_leader_xray_colour_primary", "0 255 0", "Xray/tracer RGB for the primary leader", FCVAR_NOTIFY);
g_cvXrayColourSub = CreateConVar("sm_leader_xray_colour_sub", "0 150 255", "Xray/tracer RGB for sub-leaders", FCVAR_NOTIFY);
g_cvXrayAlpha = CreateConVar("sm_leader_xray_alpha", "200", "Xray shell brightness (renderamt of the additive player-model shell)", FCVAR_NOTIFY, true, 1.0, true, 255.0);
g_cvDeathSound = CreateConVar("sm_leader_death_sound", "ambient/alarms/klaxon1.wav", "Sound played to everyone when a leader dies (relative to sound/, empty = off)", FCVAR_NOTIFY);
g_cvXrayColourPrimary.AddChangeHook(OnXrayColourChanged);
g_cvXrayColourSub.AddChangeHook(OnXrayColourChanged);
g_cvDeathSound.AddChangeHook(OnDeathSoundChanged);
AutoExecConfig(true, "Leader3");
HookEvent("round_start", Event_RoundStart);
HookEvent("player_spawn", Event_PlayerSpawn);
HookEvent("player_death", Event_PlayerDeath);
HookEvent("player_team", Event_PlayerTeam);
RegConsoleCmd("sm_leader", Cmd_Leader, "Open the leader menu / (admin) set a leader");
RegConsoleCmd("sm_pingmenu", Cmd_Leader, "Open the leader menu / (admin) set a leader");
RegConsoleCmd("sm_currentleader", Cmd_Leaders, "List the current leaders");
RegConsoleCmd("sm_currentleaders", Cmd_Leaders, "List the current leaders");
RegConsoleCmd("sm_leaders", Cmd_Leaders, "List the current leaders");
RegConsoleCmd("sm_voteleader", Cmd_VoteLeader, "Vote for a player to become a leader");
RegConsoleCmd("sm_vl", Cmd_VoteLeader, "Vote for a player to become a leader");
RegConsoleCmd("sm_leadercolour", Cmd_LeaderColour, "Set your leader colour: sm_leadercolour <RRGGBB|R G B|default>");
RegConsoleCmd("sm_leadercolor", Cmd_LeaderColour, "Set your leader colour: sm_leadercolor <RRGGBB|R G B|default>");
RegConsoleCmd("sm_marker", Cmd_Marker, "Place a marker: sm_marker <star|skull|moon|cross>");
RegConsoleCmd("sm_markertitle", Cmd_MarkerTitle, "Set the title of your last marker");
RegConsoleCmd("sm_clearmarkers", Cmd_ClearMarkers, "Clear your markers");
RegConsoleCmd("sm_resign", Cmd_LeaderResign, "Resign from being a leader");
RegAdminCmd("sm_primaryleader", Cmd_PrimaryLeader, ADMFLAG_GENERIC, "Set the primary leader: sm_primaryleader <target>");
RegAdminCmd("sm_removeleader", Cmd_RemoveLeader, ADMFLAG_GENERIC, "Remove a leader: sm_removeleader <target|blank = all current>");
RegAdminCmd("sm_addleaderwhitelist", Cmd_AddLeaderWhitelist, ADMFLAG_GENERIC, "Add a player to the leader whitelist: sm_addleaderwhitelist <target|steamid> [name]");
RegAdminCmd("sm_removeleaderwhitelist", Cmd_RemoveLeaderWhitelist, ADMFLAG_GENERIC, "Remove a player from the leader whitelist: sm_removeleaderwhitelist <target|steamid>");
RegAdminCmd("sm_leaderwhitelist", Cmd_LeaderWhitelist, ADMFLAG_GENERIC, "Show the leader whitelist");
RegAdminCmd("sm_forceleader", Cmd_ForceLeader, ADMFLAG_GENERIC, "Toggle force mute while a leader is talking");
CreateTimer(0.5, Chat_CheckLeaderVoice, _, TIMER_REPEAT);
AddMultiTargetFilter("@leaders", Filter_Leaders, "all Leaders", false);
AddMultiTargetFilter("@!leaders", Filter_NotLeaders, "everyone but the Leaders", false);
AddMultiTargetFilter("@leader", Filter_Leader, "the Primary Leader", false);
AddMultiTargetFilter("@!leader", Filter_NotLeader, "everyone but the Primary Leader", false);
}
public void OnPluginEnd()
{
RemoveMultiTargetFilter("@leaders", Filter_Leaders);
RemoveMultiTargetFilter("@!leaders", Filter_NotLeaders);
RemoveMultiTargetFilter("@leader", Filter_Leader);
RemoveMultiTargetFilter("@!leader", Filter_NotLeader);
}
public void OnAllPluginsLoaded()
{
g_bAFKManager = LibraryExists("AFKManager");
}
public void OnLibraryAdded(const char[] name)
{
if (StrEqual(name, "AFKManager"))
g_bAFKManager = true;
}
public void OnLibraryRemoved(const char[] name)
{
if (StrEqual(name, "AFKManager"))
g_bAFKManager = false;
}
public void OnMapStart()
{
g_iBeamSprite = PrecacheModel("sprites/laser.vmt");
g_iHaloSprite = PrecacheModel("sprites/halo01.vmt");
g_bWorldTextBroken = false;
}
public void OnMapEnd()
{
Core_RemoveAllLeaders();
Markers_ClearAll(false);
}
public void OnConfigsExecuted()
{
PrecacheModel(MARKER_MODEL);
AddFileToDownloadsTable("models/unloze/unloze_ping.mdl");
AddFileToDownloadsTable("models/unloze/unloze_ping.vvd");
AddFileToDownloadsTable("models/unloze/unloze_ping.dx80.vtx");
AddFileToDownloadsTable("models/unloze/unloze_ping.dx90.vtx");
AddFileToDownloadsTable("models/unloze/unloze_ping.sw.vtx");
AddFileToDownloadsTable("materials/unloze/ping/ping.vmt");
AddFileToDownloadsTable("materials/unloze/ping/ping.vtf");
AddFileToDownloadsTable("materials/unloze/ping/ping_cross.vmt");
AddFileToDownloadsTable("materials/unloze/ping/ping_cross.vtf");
AddFileToDownloadsTable("materials/unloze/ping/ping_moon.vmt");
AddFileToDownloadsTable("materials/unloze/ping/ping_moon.vtf");
AddFileToDownloadsTable("materials/unloze/ping/ping_skull.vmt");
AddFileToDownloadsTable("materials/unloze/ping/ping_skull.vtf");
AddFileToDownloadsTable(SPRITE_DEFEND);
AddFileToDownloadsTable("materials/nide/defend.vtf");
AddFileToDownloadsTable(SPRITE_FOLLOW);
AddFileToDownloadsTable("materials/nide/follow.vtf");
PrecacheModel(XRAY_MODEL);
AddFileToDownloadsTable(XRAY_MODEL);
AddFileToDownloadsTable("models/leader3/glow_ct2.vvd");
AddFileToDownloadsTable("models/leader3/glow_ct2.dx80.vtx");
AddFileToDownloadsTable("models/leader3/glow_ct2.dx90.vtx");
AddFileToDownloadsTable("models/leader3/glow_ct2.sw.vtx");
AddFileToDownloadsTable("materials/models/leader3/glow_ct/ct_urban.vmt");
AddFileToDownloadsTable("materials/models/leader3/glow_ct/ct_urban_glass.vmt");
AddFileToDownloadsTable("materials/models/leader3/glow_ct/white.vtf");
PrecacheGeneric(SPRITE_DEFEND, true);
PrecacheGeneric("materials/nide/defend.vtf", true);
PrecacheGeneric(SPRITE_FOLLOW, true);
PrecacheGeneric("materials/nide/follow.vtf", true);
char sound[PLATFORM_MAX_PATH];
g_cvDeathSound.GetString(sound, sizeof(sound));
if (sound[0])
PrecacheSound(sound, true);
ParseXrayColours();
}
public void OnDeathSoundChanged(ConVar cvar, const char[] oldVal, const char[] newVal)
{
if (newVal[0])
PrecacheSound(newVal, true);
}
public void OnXrayColourChanged(ConVar cvar, const char[] oldVal, const char[] newVal)
{
ParseXrayColours();
}
void ParseXrayColours()
{
char buf[32];
g_cvXrayColourPrimary.GetString(buf, sizeof(buf));
ParseColourString(buf, g_iXrayColourPrimary);
g_cvXrayColourSub.GetString(buf, sizeof(buf));
ParseColourString(buf, g_iXrayColourSub);
}
void ParseColourString(const char[] str, int colour[4])
{
char parts[3][8];
int found = ExplodeString(str, " ", parts, sizeof(parts), sizeof(parts[]));
for (int i = 0; i < 3; i++)
colour[i] = (i < found) ? StringToInt(parts[i]) : 255;
colour[3] = 255;
}
public void OnClientAuthorized(int client, const char[] auth)
{
Whitelist_CheckClient(client);
}
public void OnClientDisconnect(int client)
{
Vote_OnClientDisconnect(client);
Core_OnClientDisconnect(client);
g_bLeaderWhitelisted[client] = false;
if (g_bForceMuted[client])
{
BaseComm_SetClientMute(client, false);
g_bForceMuted[client] = false;
}
}
public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
Markers_OnRoundStart();
Core_AnnounceLeaders();
}
public void Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast)
{
int userid = event.GetInt("userid");
// Attach effects slightly late: at spawn time ZR state and position may not be final.
CreateTimer(0.3, Timer_ResumeEffects, userid, TIMER_FLAG_NO_MAPCHANGE);
}
public Action Timer_ResumeEffects(Handle timer, any userid)
{
int client = GetClientOfUserId(userid);
if (client > 0 && IsEffectsActive(client))
Effects_Resume(client);
return Plugin_Stop;
}
public void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast)
{
int client = GetClientOfUserId(event.GetInt("userid"));
if (client <= 0)
return;
// Toll and announce only for an active (human) leader falling; an infected leader dying later as a zombie stays quiet.
// ZR_IsClientHuman throws on dead clients, so go by team: zombified leaders sit on T.
bool diedAsHuman = GetClientTeam(client) == CS_TEAM_CT;
if (IsLeader(client) && diedAsHuman)
{
char sound[PLATFORM_MAX_PATH];
g_cvDeathSound.GetString(sound, sizeof(sound));
if (sound[0])
EmitSoundToAll(sound);
}
// ZR infection fires a fake player_death (kill feed) while the client is still alive;
// ZR_OnClientInfected announces that one, so only a real death tolls "died" here.
if (IsPlayerAlive(client))
return;
int attacker = GetClientOfUserId(event.GetInt("attacker"));
char verb[80];
if (attacker > 0 && attacker != client)
Format(verb, sizeof(verb), "was killed by \x04%N\x01", attacker);
else
strcopy(verb, sizeof(verb), "died");
Core_OnLeaderIncapacitated(client, verb, diedAsHuman);
}
public void Event_PlayerTeam(Event event, const char[] name, bool dontBroadcast)
{
Vote_RecheckAll();
}
public void ZR_OnClientInfected(int client, int attacker, bool motherInfect, bool respawnOverride, bool respawn)
{
char verb[80];
if (attacker > 0 && attacker <= MaxClients && attacker != client)
Format(verb, sizeof(verb), "has been infected by \x04%N\x01", attacker);
else
strcopy(verb, sizeof(verb), "has been infected");
Core_OnLeaderIncapacitated(client, verb);
}
public void ZR_OnClientHumanPost(int client, bool respawn, bool protect)
{
if (IsEffectsActive(client))
Effects_Resume(client);
}
public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2])
{
if (!g_bIsLeader[client])
return Plugin_Continue;
if (impulse == 100)
{
ShowLeaderMenu(client);
}
else if (impulse == 201 || impulse == -201) // player spray; inverse value comes from SprayManager
{
Marker_PlaceBySpray(client);
}
Effects_OnPlayerRunCmd(client, buttons);
return Plugin_Continue;
}
//----------------------------------------------------------------------------------------------------
// Natives
//----------------------------------------------------------------------------------------------------
public int Native_CurrentLeader(Handle plugin, int numParams)
{
return GetPrimaryLeader();
}
public int Native_SetLeader(Handle plugin, int numParams)
{
int client = GetNativeCell(1);
if (client >= 1 && client <= MaxClients && IsClientInGame(client))
Core_MakePrimary(client);
return 0;
}
public int Native_Is(Handle plugin, int numParams)
{
int client = GetNativeCell(1);
if (client < 1 || client > MaxClients)
return 0;
return g_bIsLeader[client] ? 1 : 0;
}
public int Native_GetLeaderCount(Handle plugin, int numParams)
{
return g_alLeaders.Length;
}
public int Native_GetLeader(Handle plugin, int numParams)
{
int index = GetNativeCell(1);
if (index < 0 || index >= g_alLeaders.Length)
return -1;
int client = GetClientOfUserId(g_alLeaders.Get(index));
return client > 0 ? client : -1;
}
//----------------------------------------------------------------------------------------------------
// Target filters
//----------------------------------------------------------------------------------------------------
public bool Filter_Leaders(const char[] pattern, Handle clients)
{
for (int i = 1; i <= MaxClients; i++)
{
if (g_bIsLeader[i] && IsClientInGame(i))
PushArrayCell(clients, i);
}
return true;
}
public bool Filter_NotLeaders(const char[] pattern, Handle clients)
{
for (int i = 1; i <= MaxClients; i++)
{
if (!g_bIsLeader[i] && IsClientInGame(i) && !IsFakeClient(i))
PushArrayCell(clients, i);
}
return true;
}
public bool Filter_Leader(const char[] pattern, Handle clients)
{
int primary = GetPrimaryLeader();
if (primary != -1)
PushArrayCell(clients, primary);
return true;
}
public bool Filter_NotLeader(const char[] pattern, Handle clients)
{
int primary = GetPrimaryLeader();
for (int i = 1; i <= MaxClients; i++)
{
if (i != primary && IsClientInGame(i) && !IsFakeClient(i))
PushArrayCell(clients, i);
}
return true;
}
+90
View File
@@ -0,0 +1,90 @@
#if defined _leader_included_
#endinput
#endif
#define _leader_included_
/**
* Returns the current PRIMARY leader.
*
* @return Client index of the primary leader (-1 = no leader)
*/
native int Leader_CurrentLeader();
/**
* Makes a client the PRIMARY leader. If another primary exists, they are
* demoted to sub-leader. If the client is already a sub-leader, they are
* promoted.
*
* @param client Client index to be set as primary leader
*/
native void Leader_SetLeader(int client);
/**
* Checks if a client is currently a leader (primary or sub-leader).
*
* @param client Client index to check
* @return True if the client is a leader
*/
native bool Leader_Is(int client);
/**
* Returns the number of current leaders (primary + sub-leaders).
*
* @return Leader count
*/
native int Leader_GetLeaderCount();
/**
* Returns the leader at the given roster index.
*
* @param index Roster index (0 = primary)
* @return Client index, or -1 if no leader at that index
*/
native int Leader_GetLeader(int index);
/**
* Called after a client becomes a leader.
*
* @param client The new leader
* @param primary True if they became the primary leader
*/
forward void Leader_OnLeaderAdded(int client, bool primary);
/**
* Called after a client stops being a leader.
*
* @param client The removed leader
*/
forward void Leader_OnLeaderRemoved(int client);
/**
* Called when the primary leader changes.
*
* @param oldLeader Previous primary (-1 = none)
* @param newLeader New primary (-1 = none)
*/
forward void Leader_OnPrimaryChanged(int oldLeader, int newLeader);
#if !defined LEADER3_CORE
public SharedPlugin __pl_leader =
{
name = "leader",
file = "Leader3.smx",
#if defined REQUIRE_PLUGIN
required = 1,
#else
required = 0,
#endif
};
#if !defined REQUIRE_PLUGIN
public void __pl_leader_SetNTVOptional()
{
MarkNativeAsOptional("Leader_CurrentLeader");
MarkNativeAsOptional("Leader_SetLeader");
MarkNativeAsOptional("Leader_Is");
MarkNativeAsOptional("Leader_GetLeaderCount");
MarkNativeAsOptional("Leader_GetLeader");
}
#endif
#endif // LEADER3_CORE
+105
View File
@@ -0,0 +1,105 @@
//----------------------------------------------------------------------------------------------------
// Leader3 chat: `title listener, force-mute while a leader talks.
//----------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------
// `<text> in chat sets the title of the leader's last marker. '`' alone clears it.
//----------------------------------------------------------------------------------------------------
public Action OnClientSayCommand(int client, const char[] command, const char[] sArgs)
{
if (client == 0 || sArgs[0] != '`' || !IsLeader(client))
return Plugin_Continue;
char text[128];
strcopy(text, sizeof(text), sArgs[1]);
TrimString(text);
Marker_SetTitle(client, text);
return Plugin_Handled;
}
//----------------------------------------------------------------------------------------------------
// Force-mute: while any leader is talking, mute everyone without admin access.
//----------------------------------------------------------------------------------------------------
public Action Cmd_ForceLeader(int client, int args)
{
if (g_alLeaders.Length == 0)
{
ReplyToCommand(client, "%s Cannot use this when there is no leader at the moment.", PREFIX_PLAIN);
return Plugin_Handled;
}
// Enabling is reserved for whitelisted leaders (server console excepted); disabling is not.
if (!g_bForceLeaderMute && client != 0 && !Whitelist_IsWhitelisted(client))
{
ReplyToCommand(client, "%s Only players on the leader whitelist can enable force mute.", PREFIX_PLAIN);
return Plugin_Handled;
}
g_bForceLeaderMute = !g_bForceLeaderMute;
if (g_bForceLeaderMute)
PrintToChatAll("%s\x04%N\x01 activated force mute while a leader is speaking!", PREFIX, client);
else
PrintToChatAll("%s\x04%N\x01 deactivated force mute while a leader is speaking!", PREFIX, client);
return Plugin_Handled;
}
bool IsAnyLeaderTalking()
{
for (int i = 0; i < g_alLeaders.Length; i++)
{
int leader = GetClientOfUserId(g_alLeaders.Get(i));
if (leader > 0 && IsClientTalking(leader))
return true;
}
return false;
}
public Action Chat_CheckLeaderVoice(Handle timer)
{
if (!g_bForceLeaderMute || g_alLeaders.Length == 0)
{
if (!g_bForceLeaderMute)
Chat_UnmuteForced();
return Plugin_Continue;
}
if (IsAnyLeaderTalking())
{
for (int i = 1; i <= MaxClients; i++)
{
if (IsClientInGame(i) && !g_bIsLeader[i] && !BaseComm_IsClientMuted(i)
&& !CheckCommandAccess(i, "sm_admin", ADMFLAG_GENERIC, false))
{
BaseComm_SetClientMute(i, true);
g_bForceMuted[i] = true;
}
}
for (int i = 1; i <= MaxClients; i++)
{
if (g_bForceMuted[i] && IsClientInGame(i) && IsClientTalking(i))
PrintHintText(i, "You are muted right now because a leader is talking!");
}
}
else
{
Chat_UnmuteForced();
}
return Plugin_Continue;
}
void Chat_UnmuteForced()
{
for (int i = 1; i <= MaxClients; i++)
{
if (g_bForceMuted[i] && IsClientInGame(i))
{
BaseComm_SetClientMute(i, false);
g_bForceMuted[i] = false;
}
}
}
+544
View File
@@ -0,0 +1,544 @@
//----------------------------------------------------------------------------------------------------
// 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;
}
+599
View File
@@ -0,0 +1,599 @@
//----------------------------------------------------------------------------------------------------
// Leader3 effects: xray shell, overhead sprite, beacon rings, +use view tracer.
//----------------------------------------------------------------------------------------------------
void Effects_Resume(int client)
{
if (!IsEffectsActive(client))
return;
if (g_iEffectFlags[client] & EFFECT_XRAY)
Xray_Create(client);
if (g_iSpriteChoice[client] != SPRITE_NONE)
Sprite_Create(client);
if ((g_iEffectFlags[client] & EFFECT_BEACON) && g_iBeaconSerial[client] == 0)
Beacon_Start(client);
}
void Effects_Suspend(int client)
{
Xray_Kill(client);
Sprite_Kill(client);
Beacon_Stop(client);
g_iEffectFlags[client] &= ~EFFECT_TRACER_TOGGLED;
}
//----------------------------------------------------------------------------------------------------
// Xray shell: prop_dynamic_override with the shipped xray model ($additive + $ignorez material,
// so it shows through walls), bonemerged onto the player and tinted with the tier colour.
// SetTransmit hides it from zombies, from the leader themselves, and from first-person spectators
// of the leader.
//
// $ignorez only helps once the client has the entity; CheckTransmit culls edicts against the
// viewer's PVS first, so the shell (and the leader) vanished whenever the viewer was in another
// vis area - thick walls, other side of the map. Fix: strip the shell's edict state down to
// FL_EDICT_FULLCHECK so the engine calls ShouldTransmit() every frame, and DHook that to return
// FL_EDICT_ALWAYS. The engine then calls SetTransmit(pInfo, true), which marks the shell AND its
// move parent (the leader) for sending regardless of PVS, and still runs Hook_XrayShellTransmit,
// so zombies keep seeing nothing.
//----------------------------------------------------------------------------------------------------
void Xray_SetupForceTransmit()
{
// SDKHooks' gamedata keeps the SetTransmit vtable index current; ShouldTransmit sits two
// slots before it (ShouldTransmit, UpdateTransmitState, SetTransmit in baseentity.h).
Handle gc = LoadGameConfigFile("sdkhooks.games/engine.ep2v");
if (gc == null)
{
LogError("Could not load sdkhooks.games/engine.ep2v; leader xray stays PVS-limited.");
return;
}
int offset = GameConfGetOffset(gc, "SetTransmit");
delete gc;
if (offset == -1)
{
LogError("No SetTransmit offset in sdkhooks.games/engine.ep2v; leader xray stays PVS-limited.");
return;
}
g_hXrayShouldTransmit = DHookCreate(offset - 2, HookType_Entity, ReturnType_Int, ThisPointer_Ignore, Hook_XrayShouldTransmit);
DHookAddParam(g_hXrayShouldTransmit, HookParamType_ObjectPtr); // const CCheckTransmitInfo *
}
public MRESReturn Hook_XrayShouldTransmit(Handle hReturn, Handle hParams)
{
DHookSetReturn(hReturn, FL_EDICT_ALWAYS);
return MRES_Supercede;
}
void Xray_Create(int client)
{
Xray_Kill(client);
if (!IsEffectsActive(client))
return;
int ent = CreateEntityByName("prop_dynamic_override");
if (ent == -1)
return;
DispatchKeyValue(ent, "model", XRAY_MODEL);
DispatchKeyValue(ent, "solid", "0");
DispatchKeyValue(ent, "spawnflags", "256");
DispatchKeyValue(ent, "disableshadows", "1");
DispatchKeyValue(ent, "disablereceiveshadows", "1");
if (!DispatchSpawn(ent))
{
AcceptEntityInput(ent, "Kill");
return;
}
SetEntProp(ent, Prop_Send, "m_CollisionGroup", 11); // COLLISION_GROUP_WEAPON: never collides with players
int colour[4];
GetLeaderColour(client, colour);
SetEntityRenderMode(ent, RENDER_TRANSADD);
SetEntityRenderColor(ent, colour[0], colour[1], colour[2], g_cvXrayAlpha.IntValue);
// CBaseEntity::FollowEntity() recipe. The client only bone-merges when the entity has
// EF_BONEMERGE, a move parent AND MOVETYPE_NONE (C_BaseEntity::IsFollowingEntity) --
// prop_dynamic is MOVETYPE_PUSH and SetParent doesn't change that, hence t-posing shells.
float origin[3], angles[3];
GetClientAbsOrigin(client, origin);
GetClientAbsAngles(client, angles);
TeleportEntity(ent, origin, angles, NULL_VECTOR);
SetVariantString("!activator");
AcceptEntityInput(ent, "SetParent", client, ent);
SetEntityMoveType(ent, MOVETYPE_NONE);
SetEntProp(ent, Prop_Send, "m_fEffects",
EF_BONEMERGE|EF_BONEMERGE_FASTCULL|EF_NOSHADOW|EF_NORECEIVESHADOW|EF_PARENT_ANIMATES);
SetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity", client);
SDKHook(ent, SDKHook_SetTransmit, Hook_XrayShellTransmit);
if (g_hXrayShouldTransmit != null)
{
DHookEntity(g_hXrayShouldTransmit, false, ent);
// FULLCHECK is the absence of the other transmit-state flags.
SetEdictFlags(ent, GetEdictFlags(ent) & ~(FL_EDICT_ALWAYS|FL_EDICT_PVSCHECK|FL_EDICT_DONTSEND));
}
g_iXrayRef[client] = EntIndexToEntRef(ent);
}
public Action Hook_XrayShellTransmit(int ent, int client)
{
// Anything that recomputes transmit state (DispatchUpdateTransmitState) puts PVSCHECK
// back on the edict; strip it so Hook_XrayShouldTransmit resumes overriding PVS culling.
if (g_hXrayShouldTransmit != null)
{
int flags = GetEdictFlags(ent);
if (flags & (FL_EDICT_ALWAYS|FL_EDICT_PVSCHECK))
SetEdictFlags(ent, flags & ~(FL_EDICT_ALWAYS|FL_EDICT_PVSCHECK));
}
int owner = GetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity");
if (owner == client)
return Plugin_Handled;
// Humans only: never reveal the leader's through-wall xray to zombies.
if (IsLivingZombie(client))
return Plugin_Handled;
// First-person spectators of the owner would get the shell in their face too.
if (!IsPlayerAlive(client) && owner != -1
&& GetEntProp(client, Prop_Send, "m_iObserverMode") == 4
&& GetEntPropEnt(client, Prop_Send, "m_hObserverTarget") == owner)
return Plugin_Handled;
return Plugin_Continue;
}
void Xray_Kill(int client)
{
int ent = EntRefToEntIndex(g_iXrayRef[client]);
g_iXrayRef[client] = INVALID_ENT_REFERENCE;
if (ent == INVALID_ENT_REFERENCE || !IsValidEdict(ent))
return;
char classname[64];
GetEdictClassname(ent, classname, sizeof(classname));
if (StrContains(classname, "prop_dynamic") == 0)
AcceptEntityInput(ent, "Kill");
}
// Re-create the xray with the right tier colour (used when primary changes).
void Effects_RefreshXray(int client)
{
if ((g_iEffectFlags[client] & EFFECT_XRAY) && IsEffectsActive(client))
Xray_Create(client);
}
void Xray_Toggle(int client)
{
g_iEffectFlags[client] ^= EFFECT_XRAY;
if (g_iEffectFlags[client] & EFFECT_XRAY)
{
Xray_Create(client);
PrintToChat(client, "%sXray enabled.", PREFIX);
}
else
{
Xray_Kill(client);
PrintToChat(client, "%sXray disabled.", PREFIX);
}
}
//----------------------------------------------------------------------------------------------------
// Overhead sprite: 'Defend here' / 'Follow me'
//----------------------------------------------------------------------------------------------------
void Sprite_Set(int client, int choice)
{
g_iSpriteChoice[client] = choice;
Sprite_Kill(client);
if (choice == SPRITE_NONE)
{
PrintToChat(client, "%sSprite removed.", PREFIX);
return;
}
Sprite_Create(client);
PrintToChat(client, "%sSprite changed to '%s'.", PREFIX, choice == SPRITE_CHOICE_DEFEND ? "Defend Here" : "Follow Me");
}
void Sprite_Create(int client)
{
Sprite_Kill(client);
if (!IsEffectsActive(client) || g_iSpriteChoice[client] == SPRITE_NONE)
return;
char mat[PLATFORM_MAX_PATH];
strcopy(mat, sizeof(mat), g_iSpriteChoice[client] == SPRITE_CHOICE_DEFEND ? SPRITE_DEFEND : SPRITE_FOLLOW);
g_iSpriteRef[client] = AttachSpriteEnt(client, mat, "0.1", "1", "255 255 255", 45.0);
}
void Sprite_Kill(int client)
{
KillSpriteRef(g_iSpriteRef[client]);
g_iSpriteRef[client] = INVALID_ENT_REFERENCE;
}
//----------------------------------------------------------------------------------------------------
// env_sprite attach for the overhead sprite (Leader2's targetname-rename parenting trick).
//----------------------------------------------------------------------------------------------------
int AttachSpriteEnt(int client, const char[] material, const char[] scale, const char[] rendermode, const char[] colour, float zOffset)
{
if (!IsPlayerAlive(client))
return INVALID_ENT_REFERENCE;
char targetname[16], oldTargetname[64];
GetEntPropString(client, Prop_Data, "m_iName", oldTargetname, sizeof(oldTargetname));
Format(targetname, sizeof(targetname), "Client%d", client);
DispatchKeyValue(client, "targetname", targetname);
float origin[3];
GetClientEyePosition(client, origin);
origin[2] += zOffset;
int ent = CreateEntityByName("env_sprite");
if (ent == -1)
{
DispatchKeyValue(client, "targetname", oldTargetname);
return INVALID_ENT_REFERENCE;
}
DispatchKeyValue(ent, "model", material);
DispatchKeyValue(ent, "classname", "env_sprite");
DispatchKeyValue(ent, "spawnflags", "1");
DispatchKeyValue(ent, "scale", scale);
DispatchKeyValue(ent, "rendermode", rendermode);
DispatchKeyValue(ent, "rendercolor", colour);
DispatchSpawn(ent);
TeleportEntity(ent, origin, NULL_VECTOR, NULL_VECTOR);
SetVariantString(targetname);
AcceptEntityInput(ent, "SetParent", ent, ent, 0);
DispatchKeyValue(client, "targetname", oldTargetname);
return EntIndexToEntRef(ent);
}
void KillSpriteRef(int ref)
{
if (ref == INVALID_ENT_REFERENCE)
return;
int ent = EntRefToEntIndex(ref);
if (ent == INVALID_ENT_REFERENCE || !IsValidEdict(ent))
return;
char classname[64];
GetEdictClassname(ent, classname, sizeof(classname));
if (strcmp(classname, "env_sprite") == 0)
AcceptEntityInput(ent, "Kill");
}
//----------------------------------------------------------------------------------------------------
// Beacon (serial-guarded repeating timer, Leader2/Couch ring pattern)
//----------------------------------------------------------------------------------------------------
void Beacon_Toggle(int client)
{
g_iEffectFlags[client] ^= EFFECT_BEACON;
if (g_iEffectFlags[client] & EFFECT_BEACON)
{
if (IsEffectsActive(client))
Beacon_Start(client);
PrintToChat(client, "%sBeacon enabled.", PREFIX);
LogAction(client, client, "\"%L\" set a leader beacon on himself", client);
}
else
{
Beacon_Stop(client);
PrintToChat(client, "%sBeacon disabled.", PREFIX);
}
}
void Beacon_Start(int client)
{
g_iBeaconSerial[client] = ++g_iSerialGen;
CreateTimer(g_cvBeaconInterval.FloatValue, Timer_Beacon, client | (g_iSerialGen << 7), TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE);
}
void Beacon_Stop(int client)
{
g_iBeaconSerial[client] = 0;
}
public Action Timer_Beacon(Handle timer, any value)
{
int client = value & 0x7f;
int serial = value >> 7;
if (g_iBeaconSerial[client] != serial || !IsClientInGame(client) || !IsPlayerAlive(client))
{
if (g_iBeaconSerial[client] == serial)
g_iBeaconSerial[client] = 0;
return Plugin_Stop;
}
float vec[3];
GetClientAbsOrigin(client, vec);
vec[2] += 10.0;
float radius = g_cvBeaconRadius.FloatValue;
int tierColour[4];
GetLeaderColour(client, tierColour);
TE_SetupBeamRingPoint(vec, 10.0, radius, g_iBeamSprite, g_iHaloSprite, 0, 15, 0.5, 5.0, 0.0, tierColour, 10, 0);
TE_SendToAll();
int rainbowColour[4];
float gameTime = GetGameTime();
rainbowColour[0] = RoundFloat(Sine(2.5 * gameTime + 0.0) * 127.0 + 128.0);
rainbowColour[1] = RoundFloat(Sine(2.5 * gameTime + 2.0943951) * 127.0 + 128.0);
rainbowColour[2] = RoundFloat(Sine(2.5 * gameTime + 4.1887902) * 127.0 + 128.0);
rainbowColour[3] = 255;
TE_SetupBeamRingPoint(vec, 10.0, radius, g_iBeamSprite, g_iHaloSprite, 0, 10, 0.6, 10.0, 0.5, rainbowColour, 10, 0);
TE_SendToAll();
return Plugin_Continue;
}
void GetLeaderColour(int client, int colour[4])
{
if (g_bHasCustomColour[client])
{
colour[0] = g_iCustomColour[client][0];
colour[1] = g_iCustomColour[client][1];
colour[2] = g_iCustomColour[client][2];
}
else if (IsPrimaryLeader(client))
{
colour[0] = g_iXrayColourPrimary[0];
colour[1] = g_iXrayColourPrimary[1];
colour[2] = g_iXrayColourPrimary[2];
}
else
{
colour[0] = g_iXrayColourSub[0];
colour[1] = g_iXrayColourSub[1];
colour[2] = g_iXrayColourSub[2];
}
colour[3] = 255;
}
//----------------------------------------------------------------------------------------------------
// Custom leader colour: !leadercolour <name|RRGGBB|default>. Applies to xray, beacon and tracer.
//----------------------------------------------------------------------------------------------------
static const char g_sColourNames[][] = {
"red", "orange", "yellow", "lime", "green", "cyan",
"blue", "purple", "magenta", "pink", "white"
};
static const int g_iColourValues[][3] = {
{255, 0, 0}, {255, 128, 0}, {255, 255, 0}, {128, 255, 0}, {0, 255, 0}, {0, 255, 255},
{0, 100, 255}, {160, 0, 255}, {255, 0, 255}, {255, 105, 180}, {255, 255, 255}
};
public Action Cmd_LeaderColour(int client, int args)
{
if (client == 0)
{
ReplyToCommand(client, "%s This command can only be used in-game.", PREFIX_PLAIN);
return Plugin_Handled;
}
if (!IsLeader(client))
{
PrintToChat(client, "%sOnly leaders can set a leader colour.", PREFIX);
return Plugin_Handled;
}
if (args == 0)
{
LeaderColour_PrintUsage(client);
return Plugin_Handled;
}
char arg[32];
GetCmdArg(1, arg, sizeof(arg));
TrimString(arg);
if (StrEqual(arg, "default", false) || StrEqual(arg, "reset", false))
{
g_bHasCustomColour[client] = false;
LeaderColour_SaveCookie(client);
Effects_RefreshXray(client);
PrintToChat(client, "%sColour reset to the default %s colour.", PREFIX,
IsPrimaryLeader(client) ? "primary leader" : "sub-leader");
return Plugin_Handled;
}
int rgb[3];
if (!ParseCustomColour(arg, rgb))
{
LeaderColour_PrintUsage(client);
return Plugin_Handled;
}
// Refuse colours too dark to see as a xray tint or beam.
if (rgb[0] < 40 && rgb[1] < 40 && rgb[2] < 40)
{
PrintToChat(client, "%sThat colour is too dark to be visible. Pick a brighter one.", PREFIX);
return Plugin_Handled;
}
g_iCustomColour[client][0] = rgb[0];
g_iCustomColour[client][1] = rgb[1];
g_iCustomColour[client][2] = rgb[2];
g_bHasCustomColour[client] = true;
LeaderColour_SaveCookie(client);
Effects_RefreshXray(client);
PrintToChat(client, "%sColour set to \x07%02X%02X%02X#%02X%02X%02X\x01.", PREFIX,
rgb[0], rgb[1], rgb[2], rgb[0], rgb[1], rgb[2]);
return Plugin_Handled;
}
public void OnClientCookiesCached(int client)
{
if (!IsFakeClient(client))
LeaderColour_LoadCookie(client);
}
void LeaderColour_LoadCookie(int client)
{
char buf[16];
g_hColourCookie.Get(client, buf, sizeof(buf));
int rgb[3];
if (buf[0] && ParseCustomColour(buf, rgb))
{
g_iCustomColour[client][0] = rgb[0];
g_iCustomColour[client][1] = rgb[1];
g_iCustomColour[client][2] = rgb[2];
g_bHasCustomColour[client] = true;
}
}
void LeaderColour_SaveCookie(int client)
{
if (g_bHasCustomColour[client])
{
char buf[8];
Format(buf, sizeof(buf), "%02X%02X%02X",
g_iCustomColour[client][0], g_iCustomColour[client][1], g_iCustomColour[client][2]);
g_hColourCookie.Set(client, buf);
}
else
{
g_hColourCookie.Set(client, "");
}
}
void LeaderColour_PrintUsage(int client)
{
char names[256];
for (int i = 0; i < sizeof(g_sColourNames); i++)
{
if (names[0])
StrCat(names, sizeof(names), ", ");
StrCat(names, sizeof(names), g_sColourNames[i]);
}
PrintToChat(client, "%sUsage: \x04!leadercolour <colour|hex|default>\x01", PREFIX);
PrintToChat(client, "%sColours: \x04%s\x01, or a hex code like \x04FF66CC\x01.", PREFIX, names);
}
bool ParseCustomColour(const char[] str, int rgb[3])
{
// Colour name
for (int i = 0; i < sizeof(g_sColourNames); i++)
{
if (StrEqual(str, g_sColourNames[i], false))
{
rgb[0] = g_iColourValues[i][0];
rgb[1] = g_iColourValues[i][1];
rgb[2] = g_iColourValues[i][2];
return true;
}
}
// Hex code: RRGGBB, optionally prefixed with # or 0x
int offset = 0;
if (str[0] == '#')
offset = 1;
else if (str[0] == '0' && (str[1] == 'x' || str[1] == 'X'))
offset = 2;
if (strlen(str[offset]) != 6)
return false;
int value;
if (StringToIntEx(str[offset], value, 16) != 6)
return false;
rgb[0] = (value >> 16) & 0xFF;
rgb[1] = (value >> 8) & 0xFF;
rgb[2] = value & 0xFF;
return true;
}
//----------------------------------------------------------------------------------------------------
// View tracer: hold +use to show, tap +use to toggle. Visual only; buttons are never modified.
//----------------------------------------------------------------------------------------------------
void Effects_OnPlayerRunCmd(int client, int buttons)
{
bool pressed = (buttons & IN_USE) && !(g_iLastButtons[client] & IN_USE);
bool released = !(buttons & IN_USE) && (g_iLastButtons[client] & IN_USE);
g_iLastButtons[client] = buttons;
if (!IsEffectsActive(client) || g_iTracerMode[client] == TRACER_MODE_OFF)
return;
if (pressed)
{
g_fUsePressedAt[client] = GetGameTime();
}
else if (released && g_iTracerMode[client] == TRACER_MODE_TAP
&& GetGameTime() - g_fUsePressedAt[client] < g_cvTracerTapTime.FloatValue)
{
g_iEffectFlags[client] ^= EFFECT_TRACER_TOGGLED;
}
bool active = (buttons & IN_USE) != 0 || (g_iEffectFlags[client] & EFFECT_TRACER_TOGGLED) != 0;
if (!active)
return;
float now = GetGameTime();
if (now < g_fNextTracerSend[client])
return;
g_fNextTracerSend[client] = now + g_cvTracerInterval.FloatValue;
Tracer_Draw(client);
}
void Tracer_Draw(int client)
{
float endPos[3];
if (!TracePlayerAngles(client, endPos))
return;
float startPos[3];
GetClientEyePosition(client, startPos);
startPos[2] -= 5.0; // keep the beam out of the leader's own crosshair
int colour[4];
GetLeaderColour(client, colour);
TE_SetupBeamPoints(startPos, endPos, g_iBeamSprite, g_iHaloSprite, 0, 0,
g_cvTracerLife.FloatValue, g_cvTracerWidth.FloatValue, g_cvTracerWidth.FloatValue,
0, 0.0, colour, 0);
TE_SendToAll();
}
void Tracer_CycleMode(int client)
{
// Rotate Hold + Tap-toggle -> Hold only -> Off -> Hold + Tap-toggle.
switch (g_iTracerMode[client])
{
case TRACER_MODE_TAP: g_iTracerMode[client] = TRACER_MODE_HOLD;
case TRACER_MODE_HOLD: g_iTracerMode[client] = TRACER_MODE_OFF;
default: g_iTracerMode[client] = TRACER_MODE_TAP;
}
g_iEffectFlags[client] &= ~EFFECT_TRACER_TOGGLED;
char mode[40];
switch (g_iTracerMode[client])
{
case TRACER_MODE_TAP: mode = "hold +USE OR tap to toggle";
case TRACER_MODE_HOLD: mode = "hold +USE only";
default: mode = "off";
}
PrintToChat(client, "%sTracer mode: \x04%s\x01.", PREFIX, mode);
}
+626
View File
@@ -0,0 +1,626 @@
//----------------------------------------------------------------------------------------------------
// Leader3 markers: unloze_ping prop_dynamic markers with point_worldtext titles.
// The ArrayList is the authoritative store; entities are respawned from it every round.
//----------------------------------------------------------------------------------------------------
enum struct Marker
{
int ownerUserId;
char ownerName[MAX_NAME_LENGTH];
int skin;
float pos[3];
char title[128];
float placedAt;
int propRef;
int textRef;
}
ArrayList g_alMarkers;
void Markers_OnPluginStart()
{
g_alMarkers = new ArrayList(sizeof(Marker));
}
void SkinName(int skin, char[] buf, int maxlen)
{
switch (skin)
{
case SKIN_SKULL: strcopy(buf, maxlen, "Skull");
case SKIN_MOON: strcopy(buf, maxlen, "Moon");
case SKIN_CROSS: strcopy(buf, maxlen, "Cross");
default: strcopy(buf, maxlen, "Star");
}
}
int SkinFromName(const char[] name)
{
if (StrEqual(name, "star", false)) return SKIN_STAR;
if (StrEqual(name, "skull", false)) return SKIN_SKULL;
if (StrEqual(name, "moon", false)) return SKIN_MOON;
if (StrEqual(name, "cross", false)) return SKIN_CROSS;
return -1;
}
//----------------------------------------------------------------------------------------------------
// Placement
//----------------------------------------------------------------------------------------------------
void Marker_Place(int client, int skin)
{
if (!IsLeader(client))
return;
// Dead and spectating leaders may keep placing markers (leading from spec);
// only living zombies are blocked.
if (IsLivingZombie(client))
{
PrintToChat(client, "%sYou cannot place markers as a zombie.", PREFIX);
return;
}
float pos[3];
if (!TracePlayerAngles(client, pos))
{
PrintToChat(client, "%sCould not find a spot to place the marker on.", PREFIX);
return;
}
char skinName[16];
SkinName(skin, skinName, sizeof(skinName));
int userid = GetClientUserId(client);
int existing = FindMarkerByOwnerSkin(userid, skin);
Marker marker;
if (existing != -1)
{
// Moving keeps the title and stays out of chat. The prop and text entities are
// teleported rather than respawned so repeated moves don't churn edicts.
g_alMarkers.GetArray(existing, marker);
marker.pos = pos;
marker.placedAt = GetGameTime();
GetClientName(client, marker.ownerName, sizeof(marker.ownerName));
g_alMarkers.SetArray(existing, marker);
Marker_MoveProp(existing);
Marker_UpdateText(existing);
}
else
{
if (CountMarkersByOwner(userid) >= g_cvMarkerLimit.IntValue)
{
PrintToChat(client, "%sMarker limit reached (%d). Replace one of your markers or use \x04!clearmarkers\x01.", PREFIX, g_cvMarkerLimit.IntValue);
return;
}
marker.ownerUserId = userid;
GetClientName(client, marker.ownerName, sizeof(marker.ownerName));
marker.skin = skin;
marker.pos = pos;
marker.title[0] = '\0';
marker.placedAt = GetGameTime();
marker.propRef = INVALID_ENT_REFERENCE;
marker.textRef = INVALID_ENT_REFERENCE;
int idx = g_alMarkers.PushArray(marker);
SpawnMarkerProp(idx);
PrintToChatAll("%s\x04%N\x01 placed a \x04%s\x01 marker.", PREFIX, client, skinName);
PrintToChat(client, "%sType \x04`your text\x01 in chat to put a title on it.", PREFIX);
}
}
void Marker_PlaceBySpray(int client)
{
Marker_Place(client, SKIN_STAR);
}
int FindMarkerByOwnerSkin(int userid, int skin)
{
Marker marker;
for (int i = 0; i < g_alMarkers.Length; i++)
{
g_alMarkers.GetArray(i, marker);
if (marker.ownerUserId == userid && marker.skin == skin)
return i;
}
return -1;
}
bool Marker_GetTitle(int client, int skin, char[] buf, int maxlen)
{
int idx = FindMarkerByOwnerSkin(GetClientUserId(client), skin);
if (idx == -1)
return false;
Marker marker;
g_alMarkers.GetArray(idx, marker);
if (marker.title[0] == '\0')
return false;
strcopy(buf, maxlen, marker.title);
return true;
}
int CountMarkersByOwner(int userid)
{
int count = 0;
Marker marker;
for (int i = 0; i < g_alMarkers.Length; i++)
{
g_alMarkers.GetArray(i, marker);
if (marker.ownerUserId == userid)
count++;
}
return count;
}
//----------------------------------------------------------------------------------------------------
// Titles
//----------------------------------------------------------------------------------------------------
// Titles typed entirely in lowercase are uppercased; mixed-case titles are kept as typed.
void UppercaseAllLowerTitle(char[] text)
{
bool hasLower = false;
for (int i = 0; text[i] != '\0'; i++)
{
int bytes = IsCharMB(text[i]);
if (bytes > 1)
{
i += bytes - 1;
continue;
}
if (IsCharUpper(text[i]))
return;
if (IsCharLower(text[i]))
hasLower = true;
}
if (!hasLower)
return;
for (int i = 0; text[i] != '\0'; i++)
{
int bytes = IsCharMB(text[i]);
if (bytes > 1)
{
i += bytes - 1;
continue;
}
text[i] = CharToUpper(text[i]);
}
}
void Marker_SetTitle(int client, const char[] text)
{
int userid = GetClientUserId(client);
// Most recently placed marker owned by this leader.
int lastIdx = -1;
float lastTime = -1.0;
Marker marker;
for (int i = 0; i < g_alMarkers.Length; i++)
{
g_alMarkers.GetArray(i, marker);
if (marker.ownerUserId == userid && marker.placedAt > lastTime)
{
lastTime = marker.placedAt;
lastIdx = i;
}
}
if (lastIdx == -1)
{
PrintToChat(client, "%sYou have no marker to title. Place one first.", PREFIX);
return;
}
g_alMarkers.GetArray(lastIdx, marker);
strcopy(marker.title, sizeof(marker.title), text);
UppercaseAllLowerTitle(marker.title);
g_alMarkers.SetArray(lastIdx, marker);
Marker_UpdateText(lastIdx);
char skinName[16];
SkinName(marker.skin, skinName, sizeof(skinName));
if (text[0] == '\0')
{
PrintToChat(client, "%sTitle removed from your \x04%s\x01 marker.", PREFIX, skinName);
return;
}
PrintToChatAll("%s\x04%N\x01 marked \x04%s\x01: \x04%s", PREFIX, client, skinName, marker.title);
}
//----------------------------------------------------------------------------------------------------
// Entity lifecycle
//----------------------------------------------------------------------------------------------------
void SpawnMarkerProp(int idx)
{
Marker marker;
g_alMarkers.GetArray(idx, marker);
int ent = CreateEntityByName("prop_dynamic");
if (ent == -1)
return;
SetEntityModel(ent, MARKER_MODEL);
DispatchKeyValueFloat(ent, "modelscale", MARKER_MODEL_SCALE);
char skin[4];
IntToString(marker.skin, skin, sizeof(skin));
SetVariantString(skin);
AcceptEntityInput(ent, "Skin");
DispatchSpawn(ent);
TeleportEntity(ent, marker.pos, NULL_VECTOR, NULL_VECTOR);
marker.propRef = EntIndexToEntRef(ent);
g_alMarkers.SetArray(idx, marker);
}
void SpawnMarkerText(int idx)
{
Marker marker;
g_alMarkers.GetArray(idx, marker);
if (marker.title[0] == '\0' || g_bWorldTextBroken)
return;
int ent = CreateEntityByName("point_worldtext");
if (ent == -1)
{
// Engine build without point_worldtext: titles stay chat-only for this map.
g_bWorldTextBroken = true;
LogMessage("point_worldtext could not be created; marker titles will not render in the world.");
return;
}
char text[192];
Format(text, sizeof(text), "%s", marker.title);
DispatchKeyValue(ent, "message", text);
DispatchKeyValue(ent, "font", "10");
DispatchKeyValue(ent, "textsize", "12");
DispatchKeyValue(ent, "color", "255 255 255");
DispatchKeyValue(ent, "orientation", "1");
DispatchKeyValue(ent, "rainbow", "1");
DispatchSpawn(ent);
float pos[3];
ComputeMarkerTextPos(marker, pos);
TeleportEntity(ent, pos, NULL_VECTOR, NULL_VECTOR);
marker.textRef = EntIndexToEntRef(ent);
g_alMarkers.SetArray(idx, marker);
}
void ComputeMarkerTextPos(const Marker marker, float pos[3])
{
pos = marker.pos;
// unloze_ping.mdl is 80 units tall and spawned at modelscale 1.5 (120 units).
pos[2] += MARKER_MODEL_HEIGHT * MARKER_MODEL_SCALE + 8.0;
// Markers placed in low rooms would put the text inside the ceiling: trace up
// from the marker base and keep the text MARKER_TEXT_CLEARANCE below any hit.
float up[3];
up = marker.pos;
up[2] = pos[2] + MARKER_TEXT_CLEARANCE;
Handle trace = TR_TraceRayFilterEx(marker.pos, up, MASK_SOLID_BRUSHONLY, RayType_EndPoint, TraceFilter_NoPlayers);
if (TR_DidHit(trace))
{
float hit[3];
TR_GetEndPosition(hit, trace);
pos[2] = hit[2] - MARKER_TEXT_CLEARANCE;
}
delete trace;
}
// Teleports the existing marker prop to the stored position, spawning one only if the
// reference went stale.
void Marker_MoveProp(int idx)
{
Marker marker;
g_alMarkers.GetArray(idx, marker);
int ent = LiveEntRef(marker.propRef, "prop_dynamic");
if (ent == -1)
{
SpawnMarkerProp(idx);
return;
}
TeleportEntity(ent, marker.pos, NULL_VECTOR, NULL_VECTOR);
}
// Retitles/moves the existing point_worldtext in place (SetText is networked via
// SendPropString) instead of respawning it, so retitles and moves don't churn edicts.
void Marker_UpdateText(int idx)
{
Marker marker;
g_alMarkers.GetArray(idx, marker);
if (marker.title[0] == '\0')
{
KillEntRef(marker.textRef, "point_worldtext");
marker.textRef = INVALID_ENT_REFERENCE;
g_alMarkers.SetArray(idx, marker);
return;
}
int ent = LiveEntRef(marker.textRef, "point_worldtext");
if (ent == -1)
{
SpawnMarkerText(idx);
return;
}
SetVariantString(marker.title);
AcceptEntityInput(ent, "SetText");
float pos[3];
ComputeMarkerTextPos(marker, pos);
TeleportEntity(ent, pos, NULL_VECTOR, NULL_VECTOR);
}
void Marker_KillEnts(Marker marker)
{
KillEntRef(marker.propRef, "prop_dynamic");
KillEntRef(marker.textRef, "point_worldtext");
}
void KillEntRef(int ref, const char[] expectedClass)
{
int ent = LiveEntRef(ref, expectedClass);
if (ent != -1)
AcceptEntityInput(ent, "Kill");
}
// Resolves an entity reference, returning -1 unless it is still a live edict of the
// expected class.
int LiveEntRef(int ref, const char[] expectedClass)
{
if (ref == INVALID_ENT_REFERENCE)
return -1;
int ent = EntRefToEntIndex(ref);
if (ent == INVALID_ENT_REFERENCE || !IsValidEdict(ent))
return -1;
char classname[64];
GetEdictClassname(ent, classname, sizeof(classname));
return strcmp(classname, expectedClass) == 0 ? ent : -1;
}
// Round transitions delete runtime entities: rebuild every marker from the stored data.
void Markers_OnRoundStart()
{
Marker marker;
for (int i = 0; i < g_alMarkers.Length; i++)
{
g_alMarkers.GetArray(i, marker);
marker.propRef = INVALID_ENT_REFERENCE;
marker.textRef = INVALID_ENT_REFERENCE;
g_alMarkers.SetArray(i, marker);
SpawnMarkerProp(i);
SpawnMarkerText(i);
}
}
void Markers_ClearAll(bool killEnts = true)
{
if (killEnts)
{
Marker marker;
for (int i = 0; i < g_alMarkers.Length; i++)
{
g_alMarkers.GetArray(i, marker);
Marker_KillEnts(marker);
}
}
g_alMarkers.Clear();
}
void Marker_ClearByOwner(int client, bool announce = true)
{
int userid = GetClientUserId(client);
int removed = 0;
Marker marker;
for (int i = g_alMarkers.Length - 1; i >= 0; i--)
{
g_alMarkers.GetArray(i, marker);
if (marker.ownerUserId != userid)
continue;
Marker_KillEnts(marker);
g_alMarkers.Erase(i);
removed++;
}
if (!announce)
return;
if (removed > 0)
PrintToChat(client, "%sRemoved %d of your markers.", PREFIX, removed);
else
PrintToChat(client, "%sYou have no markers.", PREFIX);
}
//----------------------------------------------------------------------------------------------------
// Commands
//----------------------------------------------------------------------------------------------------
public Action Cmd_Marker(int client, int args)
{
if (client == 0)
{
ReplyToCommand(client, "%s Cannot use this from the console.", PREFIX_PLAIN);
return Plugin_Handled;
}
if (!IsLeader(client))
{
ReplyToCommand(client, "%s Only leaders can place markers.", PREFIX_PLAIN);
return Plugin_Handled;
}
if (args < 1)
{
ReplyToCommand(client, "%s Usage: sm_marker <star|skull|moon|cross>", PREFIX_PLAIN);
return Plugin_Handled;
}
char arg[16];
GetCmdArg(1, arg, sizeof(arg));
int skin = SkinFromName(arg);
if (skin == -1)
{
ReplyToCommand(client, "%s Unknown marker type '%s'. Use star, skull, moon or cross.", PREFIX_PLAIN, arg);
return Plugin_Handled;
}
Marker_Place(client, skin);
return Plugin_Handled;
}
public Action Cmd_MarkerTitle(int client, int args)
{
if (client == 0)
{
ReplyToCommand(client, "%s Cannot use this from the console.", PREFIX_PLAIN);
return Plugin_Handled;
}
if (!IsLeader(client))
{
ReplyToCommand(client, "%s Only leaders can title markers.", PREFIX_PLAIN);
return Plugin_Handled;
}
char text[128];
GetCmdArgString(text, sizeof(text));
StripQuotes(text);
TrimString(text);
Marker_SetTitle(client, text);
return Plugin_Handled;
}
public Action Cmd_ClearMarkers(int client, int args)
{
if (client == 0)
{
Markers_ClearAll();
ReplyToCommand(client, "%s All markers cleared.", PREFIX_PLAIN);
return Plugin_Handled;
}
if (args >= 1 && CheckCommandAccess(client, "sm_admin", ADMFLAG_GENERIC, false))
{
char arg[65];
GetCmdArg(1, arg, sizeof(arg));
if (StrEqual(arg, "all", false))
{
Markers_ClearAll();
PrintToChatAll("%sAll markers have been cleared by \x04%N\x01.", PREFIX, client);
return Plugin_Handled;
}
char targetName[MAX_TARGET_LENGTH];
int targets[MAXPLAYERS];
bool tnIsMl;
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;
}
for (int i = 0; i < count; i++)
Marker_ClearByOwner(targets[i]);
ReplyToCommand(client, "%s Cleared markers of %s.", PREFIX_PLAIN, targetName);
return Plugin_Handled;
}
if (!IsLeader(client))
{
ReplyToCommand(client, "%s Only leaders can clear markers.", PREFIX_PLAIN);
return Plugin_Handled;
}
Marker_ClearByOwner(client);
return Plugin_Handled;
}
//----------------------------------------------------------------------------------------------------
// Aim trace (port of Leader2's TracePlayerAngles)
//----------------------------------------------------------------------------------------------------
bool TracePlayerAngles(int client, float vecResult[3])
{
if (!IsClientInGame(client))
return false;
float vecEyeAngles[3], vecEyeOrigin[3];
GetClientEyeAngles(client, vecEyeAngles);
GetClientEyePosition(client, vecEyeOrigin);
Handle trace = TR_TraceRayFilterEx(vecEyeOrigin, vecEyeAngles, MASK_SHOT_HULL, RayType_Infinite, TraceFilter_NoPlayers);
// AllSolid: eyes clipped inside a solid (crowd pushes); endpos degenerates to the eye.
bool hit = TR_DidHit(trace) && !TR_AllSolid(trace);
int hitEnt = hit ? TR_GetEntityIndex(trace) : -1;
if (hit)
TR_GetEndPosition(vecResult, trace);
delete trace;
if (!hit)
return false;
// NaN or out-of-grid components reach the client as 0 (WriteBitCoord encodes them
// as zero), which draws the beam to the world origin. Catch them before the TE.
if (vecResult[0] != vecResult[0] || vecResult[1] != vecResult[1] || vecResult[2] != vecResult[2]
|| FloatAbs(vecResult[0]) > 16384.0 || FloatAbs(vecResult[1]) > 16384.0 || FloatAbs(vecResult[2]) > 16384.0)
{
LogSuspectTrace(client, hitEnt, vecResult, vecEyeOrigin, vecEyeAngles, "insane endpos");
return false;
}
// ZE maps park "removed" but still solid props at the world origin; their hulls
// catch the ray there. Hits on the world itself near origin stay legitimate.
if (hitEnt > 0 && GetVectorLength(vecResult) < 64.0)
{
LogSuspectTrace(client, hitEnt, vecResult, vecEyeOrigin, vecEyeAngles, "entity hull at world origin");
return false;
}
return true;
}
// One line per 5s so a leader holding the tracer on a culprit doesn't flood the log.
static float g_fNextSuspectTraceLog;
static void LogSuspectTrace(int client, int ent, const float end[3], const float eye[3], const float ang[3], const char[] why)
{
float now = GetGameTime();
if (now < g_fNextSuspectTraceLog)
return;
g_fNextSuspectTraceLog = now + 5.0;
char classname[64] = "worldspawn";
char model[PLATFORM_MAX_PATH];
if (ent > 0 && IsValidEdict(ent))
{
GetEdictClassname(ent, classname, sizeof(classname));
GetEntPropString(ent, Prop_Data, "m_ModelName", model, sizeof(model));
}
LogMessage("Aim trace rejected (%s) for %L: end (%.1f %.1f %.1f), hit ent %d (%s, model '%s'), eye (%.1f %.1f %.1f), ang (%.1f %.1f %.1f)",
why, client, end[0], end[1], end[2], ent, classname, model,
eye[0], eye[1], eye[2], ang[0], ang[1], ang[2]);
}
public bool TraceFilter_NoPlayers(int entity, int contentsMask)
{
return entity > MaxClients;
}
+365
View File
@@ -0,0 +1,365 @@
//----------------------------------------------------------------------------------------------------
// Leader3 menus. Opened with !leader or the flashlight key (impulse 100).
//----------------------------------------------------------------------------------------------------
bool CanManageLeaders(int client)
{
return IsPrimaryLeader(client) || CheckCommandAccess(client, "sm_admin", ADMFLAG_GENERIC, false);
}
void AddMarkerMenuItem(Menu menu, int client, const char[] info, const char[] name, int skin)
{
char title[128], display[160];
if (Marker_GetTitle(client, skin, title, sizeof(title)))
{
Format(display, sizeof(display), "%s: %s", name, title);
menu.AddItem(info, display);
}
else
{
menu.AddItem(info, name);
}
}
void ShowLeaderMenu(int client)
{
if (!IsLeader(client))
return;
Menu menu = new Menu(MenuHandler_Main);
menu.SetTitle("Leader Menu (%s)\nBeacon: %s | Xray: %s",
IsPrimaryLeader(client) ? "PRIMARY" : "SUB-LEADER",
(g_iEffectFlags[client] & EFFECT_BEACON) ? "On" : "Off",
(g_iEffectFlags[client] & EFFECT_XRAY) ? "On" : "Off");
AddMarkerMenuItem(menu, client, "skull", "Skull", SKIN_SKULL);
AddMarkerMenuItem(menu, client, "cross", "Cross", SKIN_CROSS);
AddMarkerMenuItem(menu, client, "moon", "Moon", SKIN_MOON);
AddMarkerMenuItem(menu, client, "star", "Star", SKIN_STAR);
menu.AddItem("clear", "Clear markers");
char buf[32];
switch (g_iSpriteChoice[client])
{
case SPRITE_CHOICE_FOLLOW: buf = "Sprite: Follow me";
case SPRITE_CHOICE_DEFEND: buf = "Sprite: Defend here";
default: buf = "Sprite: Off";
}
menu.AddItem("sprite", buf);
menu.AddItem("settings", "Settings");
menu.ExitButton = true;
menu.Display(client, MENU_TIME_FOREVER);
}
public int MenuHandler_Main(Menu menu, MenuAction action, int client, int position)
{
if (action == MenuAction_End)
{
delete menu;
return 0;
}
if (action != MenuAction_Select || !IsLeader(client))
return 0;
char info[32];
menu.GetItem(position, info, sizeof(info));
// Zombie leaders may still use the settings menu and clear their own markers;
// the other main-menu items act in the world, so don't act and just let the menu close.
if (IsLivingZombie(client) && !StrEqual(info, "settings") && !StrEqual(info, "clear"))
{
PrintToChat(client, "%sYou cannot use this as a zombie.", PREFIX);
return 0;
}
int skin = SkinFromName(info);
if (skin != -1)
{
Marker_Place(client, skin);
ShowLeaderMenu(client);
}
else if (StrEqual(info, "clear"))
{
Marker_ClearByOwner(client);
ShowLeaderMenu(client);
}
else if (StrEqual(info, "sprite"))
{
// Rotate Off -> Follow me -> Defend here -> Off.
switch (g_iSpriteChoice[client])
{
case SPRITE_NONE: Sprite_Set(client, SPRITE_CHOICE_FOLLOW);
case SPRITE_CHOICE_FOLLOW: Sprite_Set(client, SPRITE_CHOICE_DEFEND);
default: Sprite_Set(client, SPRITE_NONE);
}
ShowLeaderMenu(client);
}
else if (StrEqual(info, "settings"))
{
ShowSettingsMenu(client);
}
return 0;
}
//----------------------------------------------------------------------------------------------------
// Settings / Extra
//----------------------------------------------------------------------------------------------------
void ShowSettingsMenu(int client)
{
Menu menu = new Menu(MenuHandler_Settings);
menu.SetTitle("Leader Settings");
char buf[64];
Format(buf, sizeof(buf), "Beacon: %s", (g_iEffectFlags[client] & EFFECT_BEACON) ? "On" : "Off");
menu.AddItem("beacon", buf);
Format(buf, sizeof(buf), "Xray: %s", (g_iEffectFlags[client] & EFFECT_XRAY) ? "On" : "Off");
menu.AddItem("xray", buf);
switch (g_iTracerMode[client])
{
case TRACER_MODE_TAP: buf = "Tracer: Hold + Tap-toggle";
case TRACER_MODE_HOLD: buf = "Tracer: Hold only";
default: buf = "Tracer: Off";
}
menu.AddItem("tracer", buf);
menu.AddItem("resign", "Resign from Leader");
if (CanManageLeaders(client))
{
menu.AddItem("manage", "Manage Leaders");
menu.AddItem("clearall", "Clear ALL Markers");
Format(buf, sizeof(buf), "Force-Mute: %s", g_bForceLeaderMute ? "On" : "Off");
menu.AddItem("forcemute", buf);
}
menu.ExitBackButton = true;
menu.ExitButton = true;
menu.Display(client, MENU_TIME_FOREVER);
}
public int MenuHandler_Settings(Menu menu, MenuAction action, int client, int position)
{
if (action == MenuAction_End)
{
delete menu;
return 0;
}
if (action == MenuAction_Cancel && position == MenuCancel_ExitBack)
{
ShowLeaderMenu(client);
return 0;
}
if (action != MenuAction_Select || !IsLeader(client))
return 0;
char info[32];
menu.GetItem(position, info, sizeof(info));
if (StrEqual(info, "beacon"))
{
Beacon_Toggle(client);
ShowSettingsMenu(client);
}
else if (StrEqual(info, "xray"))
{
Xray_Toggle(client);
ShowSettingsMenu(client);
}
else if (StrEqual(info, "tracer"))
{
Tracer_CycleMode(client);
ShowSettingsMenu(client);
}
else if (StrEqual(info, "resign"))
{
RemoveLeaderClient(client, "resigned");
}
else if (StrEqual(info, "manage") && CanManageLeaders(client))
{
ShowManageMenu(client);
}
else if (StrEqual(info, "clearall") && CanManageLeaders(client))
{
Markers_ClearAll();
PrintToChatAll("%sAll markers have been cleared by \x04%N\x01.", PREFIX, client);
ShowSettingsMenu(client);
}
else if (StrEqual(info, "forcemute") && CanManageLeaders(client))
{
// Enabling is reserved for whitelisted leaders; disabling is not.
if (!g_bForceLeaderMute && !Whitelist_IsWhitelisted(client))
{
PrintToChat(client, "%sOnly players on the leader whitelist can enable force mute.", PREFIX);
}
else
{
g_bForceLeaderMute = !g_bForceLeaderMute;
PrintToChatAll("%s\x04%N\x01 %s force mute while a leader is speaking!", PREFIX, client,
g_bForceLeaderMute ? "activated" : "deactivated");
}
ShowSettingsMenu(client);
}
return 0;
}
//----------------------------------------------------------------------------------------------------
// Manage leaders (primary leader or admin)
//----------------------------------------------------------------------------------------------------
void ShowManageMenu(int client)
{
Menu menu = new Menu(MenuHandler_Manage);
menu.SetTitle("Manage Leaders\n[P] primary, [L] sub-leader");
char info[12], display[MAX_NAME_LENGTH + 8];
bool any = false;
// Active leaders first, in roster order (index 0 = primary).
for (int i = 0; i < g_alLeaders.Length; i++)
{
int leader = GetClientOfUserId(g_alLeaders.Get(i));
if (leader <= 0)
continue;
any = true;
IntToString(GetClientUserId(leader), info, sizeof(info));
Format(display, sizeof(display), "[%s] %N", i == 0 ? "P" : "L", leader);
menu.AddItem(info, display);
}
// Then everyone else who could become one.
for (int i = 1; i <= MaxClients; i++)
{
if (g_bIsLeader[i] || !IsClientInGame(i) || IsFakeClient(i))
continue;
if (!IsLivingHuman(i))
continue;
any = true;
IntToString(GetClientUserId(i), info, sizeof(info));
Format(display, sizeof(display), "%N", i);
menu.AddItem(info, display);
}
if (!any)
menu.AddItem("", "No players available", ITEMDRAW_DISABLED);
menu.ExitBackButton = true;
menu.ExitButton = true;
menu.Display(client, MENU_TIME_FOREVER);
}
public int MenuHandler_Manage(Menu menu, MenuAction action, int client, int position)
{
if (action == MenuAction_End)
{
delete menu;
return 0;
}
if (action == MenuAction_Cancel && position == MenuCancel_ExitBack)
{
ShowSettingsMenu(client);
return 0;
}
if (action != MenuAction_Select || !CanManageLeaders(client))
return 0;
char info[12];
menu.GetItem(position, info, sizeof(info));
int target = GetClientOfUserId(StringToInt(info));
if (target == 0)
{
PrintToChat(client, "%sThat player is no longer available.", PREFIX);
ShowManageMenu(client);
return 0;
}
ShowManageActionMenu(client, target);
return 0;
}
void ShowManageActionMenu(int client, int target)
{
Menu menu = new Menu(MenuHandler_ManageAction);
menu.SetTitle("Manage: %N", target);
char info[16];
IntToString(GetClientUserId(target), info, sizeof(info));
if (g_bIsLeader[target])
{
if (!IsPrimaryLeader(target))
menu.AddItem(info, "Make Primary Leader");
menu.AddItem(info, "Demote (remove leader)");
}
else
{
menu.AddItem(info, "Promote to Sub-Leader");
}
menu.ExitBackButton = true;
menu.ExitButton = true;
menu.Display(client, MENU_TIME_FOREVER);
}
public int MenuHandler_ManageAction(Menu menu, MenuAction action, int client, int position)
{
if (action == MenuAction_End)
{
delete menu;
return 0;
}
if (action == MenuAction_Cancel && position == MenuCancel_ExitBack)
{
ShowManageMenu(client);
return 0;
}
if (action != MenuAction_Select || !CanManageLeaders(client))
return 0;
char info[16], display[64];
int style;
menu.GetItem(position, info, sizeof(info), style, display, sizeof(display));
int target = GetClientOfUserId(StringToInt(info));
if (target == 0)
{
PrintToChat(client, "%sThat player is no longer available.", PREFIX);
ShowManageMenu(client);
return 0;
}
if (StrContains(display, "Primary") != -1)
{
Core_MakePrimary(target, client);
}
else if (StrContains(display, "Demote") != -1)
{
char reason[80];
Format(reason, sizeof(reason), "demoted by \x04%N\x01", client);
RemoveLeaderClient(target, reason);
}
else if (StrContains(display, "Promote") != -1)
{
if (!IsLivingHuman(target))
PrintToChat(client, "%sThat player has to be a living human.", PREFIX);
else
AddLeader(target, false, client);
}
ShowManageMenu(client);
return 0;
}
+136
View File
@@ -0,0 +1,136 @@
//----------------------------------------------------------------------------------------------------
// Leader3 vote: !voteleader / !vl <name>. Passes at sm_leader_vote_ratio of eligible voters.
// Eligible = in-game, not a bot, not AFK (AFKManager, when loaded).
//----------------------------------------------------------------------------------------------------
int g_iVoteCount[MAXPLAYERS + 1]; // votes received, per candidate
int g_iVotedForSerial[MAXPLAYERS + 1]; // per voter: client serial of their candidate
public Action Cmd_VoteLeader(int client, int args)
{
if (client == 0)
{
ReplyToCommand(client, "%s Cannot use this from the console.", PREFIX_PLAIN);
return Plugin_Handled;
}
if (!g_cvAllowVotes.BoolValue)
{
ReplyToCommand(client, "%s Voting for leader is disabled.", PREFIX_PLAIN);
return Plugin_Handled;
}
if (args < 1)
{
ReplyToCommand(client, "%s Usage: sm_voteleader <player>", PREFIX_PLAIN);
return Plugin_Handled;
}
char arg[65];
GetCmdArg(1, arg, sizeof(arg));
int target = FindTarget(client, arg, true, false);
if (target == -1)
return Plugin_Handled;
if (g_bIsLeader[target])
{
ReplyToCommand(client, "%s %N is already a leader!", PREFIX_PLAIN, target);
return Plugin_Handled;
}
if (g_alLeaders.Length >= g_cvMaxLeaders.IntValue)
{
ReplyToCommand(client, "%s The maximum of %d leaders is already reached.", PREFIX_PLAIN, g_cvMaxLeaders.IntValue);
return Plugin_Handled;
}
int previous = GetClientFromSerial(g_iVotedForSerial[client]);
if (previous == target)
{
ReplyToCommand(client, "%s You have already voted for this player!", PREFIX_PLAIN);
return Plugin_Handled;
}
if (previous != 0)
g_iVoteCount[previous]--;
g_iVoteCount[target]++;
g_iVotedForSerial[client] = GetClientSerial(target);
PrintToChatAll("%s\x04%N\x01 voted for \x04%N\x01 to become a leader (\x04%d/%d\x01 votes).",
PREFIX, client, target, g_iVoteCount[target], Vote_Needed());
Vote_Check(target);
return Plugin_Handled;
}
int Vote_EligibleCount()
{
int afkTime = g_cvVoteAfkTime.IntValue;
int count = 0;
for (int i = 1; i <= MaxClients; i++)
{
if (!IsClientInGame(i) || IsFakeClient(i))
continue;
if (g_bAFKManager && afkTime > 0 && GetClientIdleTime(i) >= afkTime)
continue;
count++;
}
return count;
}
int Vote_Needed()
{
int needed = RoundToCeil(float(Vote_EligibleCount()) * g_cvVoteRatio.FloatValue);
return needed < 2 ? 2 : needed;
}
void Vote_Check(int candidate)
{
if (!IsValidClient(candidate) || g_iVoteCount[candidate] <= 0)
return;
if (g_iVoteCount[candidate] < Vote_Needed())
return;
Vote_ClearCandidate(candidate);
PrintToChatAll("%s\x04%N\x01 has been voted to become a leader!", PREFIX, candidate);
if (AddLeader(candidate))
ShowLeaderMenu(candidate);
}
void Vote_ClearCandidate(int candidate)
{
g_iVoteCount[candidate] = 0;
int serial = GetClientSerial(candidate);
for (int i = 1; i <= MaxClients; i++)
{
if (g_iVotedForSerial[i] == serial)
g_iVotedForSerial[i] = 0;
}
}
// A shrinking voter pool can push a pending candidate over the threshold.
void Vote_RecheckAll()
{
for (int i = 1; i <= MaxClients; i++)
{
if (g_iVoteCount[i] > 0)
Vote_Check(i);
}
}
void Vote_OnClientDisconnect(int client)
{
int previous = GetClientFromSerial(g_iVotedForSerial[client]);
if (previous != 0)
g_iVoteCount[previous]--;
g_iVotedForSerial[client] = 0;
if (g_iVoteCount[client] > 0)
Vote_ClearCandidate(client);
Vote_RecheckAll();
}
+371
View File
@@ -0,0 +1,371 @@
//----------------------------------------------------------------------------------------------------
// Leader3 whitelist: players allowed to self-assign leader without a vote or admin flags.
// Stored in the SourceMod database ("sourcemod" config when defined, else storage-local).
//----------------------------------------------------------------------------------------------------
void Whitelist_OnPluginStart()
{
char conf[64];
strcopy(conf, sizeof(conf), SQL_CheckConfig("sourcemod") ? "sourcemod" : "storage-local");
Database.Connect(Whitelist_OnDBConnected, conf);
}
public void Whitelist_OnDBConnected(Database db, const char[] error, any data)
{
if (db == null)
{
LogError("[Leader3] Whitelist database connection failed: %s", error);
return;
}
g_hWhitelistDB = db;
g_hWhitelistDB.SetCharset("utf8");
g_hWhitelistDB.Query(Whitelist_OnTableCreated,
"CREATE TABLE IF NOT EXISTS leader_whitelist ("
... "steam_id VARCHAR(32) NOT NULL PRIMARY KEY, "
... "name VARCHAR(64) NOT NULL DEFAULT '', "
... "added_by VARCHAR(96) NOT NULL DEFAULT '', "
... "added_at INTEGER NOT NULL DEFAULT 0)");
}
public void Whitelist_OnTableCreated(Database db, DBResultSet results, const char[] error, any data)
{
if (results == null)
{
LogError("[Leader3] Failed to create leader_whitelist table: %s", error);
delete g_hWhitelistDB;
return;
}
// Late load / slow connect: check everyone already on.
for (int i = 1; i <= MaxClients; i++)
{
if (IsClientInGame(i) && IsClientAuthorized(i))
Whitelist_CheckClient(i);
}
}
void Whitelist_CheckClient(int client)
{
g_bLeaderWhitelisted[client] = false;
char auth[32];
if (g_hWhitelistDB == null || IsFakeClient(client) || !GetClientAuthId(client, AuthId_Steam2, auth, sizeof(auth)))
return;
char query[128];
g_hWhitelistDB.Format(query, sizeof(query), "SELECT 1 FROM leader_whitelist WHERE steam_id = '%s'", auth);
g_hWhitelistDB.Query(Whitelist_OnCheckDone, query, GetClientUserId(client));
}
public void Whitelist_OnCheckDone(Database db, DBResultSet results, const char[] error, any userid)
{
if (results == null)
{
LogError("[Leader3] Whitelist lookup failed: %s", error);
return;
}
int client = GetClientOfUserId(userid);
if (client > 0)
g_bLeaderWhitelisted[client] = results.FetchRow();
}
bool Whitelist_IsWhitelisted(int client)
{
return client >= 1 && client <= MaxClients && g_bLeaderWhitelisted[client];
}
void Whitelist_Add(int admin, int target)
{
if (g_hWhitelistDB == null)
{
ReplyToCommand(admin, "%s The whitelist database is not available.", PREFIX_PLAIN);
return;
}
char auth[32];
if (!GetClientAuthId(target, AuthId_Steam2, auth, sizeof(auth)))
{
ReplyToCommand(admin, "%s %N is not authorized with Steam yet, try again shortly.", PREFIX_PLAIN, target);
return;
}
if (g_bLeaderWhitelisted[target])
{
ReplyToCommand(admin, "%s %N is already on the leader whitelist.", PREFIX_PLAIN, target);
return;
}
char name[MAX_NAME_LENGTH], adminAuth[96];
GetClientName(target, name, sizeof(name));
if (admin > 0)
GetClientAuthId(admin, AuthId_Steam2, adminAuth, sizeof(adminAuth));
else
strcopy(adminAuth, sizeof(adminAuth), "CONSOLE");
char query[512];
g_hWhitelistDB.Format(query, sizeof(query),
"REPLACE INTO leader_whitelist (steam_id, name, added_by, added_at) VALUES ('%s', '%s', '%s', %d)",
auth, name, adminAuth, GetTime());
g_hWhitelistDB.Query(Whitelist_OnWriteDone, query);
g_bLeaderWhitelisted[target] = true;
ReplyToCommand(admin, "%s Added %N (%s) to the leader whitelist.", PREFIX_PLAIN, target, auth);
PrintToChat(target, "%sYou are now on the \x04leader whitelist\x01! Type \x04!leader\x01 to lead at any time.", PREFIX);
LogAction(admin, target, "\"%L\" added \"%L\" to the leader whitelist", admin, target);
}
// Normalize a typed SteamID to canonical upper-case Steam2 form (STEAM_X:Y:Z).
// The steam_id column comparison is case-sensitive, so lower-case input must not reach the DB.
static bool Whitelist_NormalizeSteam2(const char[] input, char[] auth, int maxlen)
{
strcopy(auth, maxlen, input);
for (int i = 0; auth[i] != '\0'; i++)
auth[i] = CharToUpper(auth[i]);
if (StrContains(auth, "STEAM_", true) != 0)
return false;
if (!IsCharNumeric(auth[6]) || auth[7] != ':' || (auth[8] != '0' && auth[8] != '1') || auth[9] != ':' || !IsCharNumeric(auth[10]))
return false;
for (int i = 11; auth[i] != '\0'; i++)
{
if (!IsCharNumeric(auth[i]))
return false;
}
return true;
}
// Add a possibly offline player by SteamID (sm_addleaderwhitelist STEAM_0:X:XXXX [name]).
void Whitelist_AddAuth(int admin, const char[] authArg, const char[] name)
{
if (g_hWhitelistDB == null)
{
ReplyToCommand(admin, "%s The whitelist database is not available.", PREFIX_PLAIN);
return;
}
char auth[32];
if (!Whitelist_NormalizeSteam2(authArg, auth, sizeof(auth)))
{
ReplyToCommand(admin, "%s '%s' is not a valid SteamID (expected STEAM_0:X:XXXX).", PREFIX_PLAIN, authArg);
return;
}
char adminAuth[96];
if (admin > 0)
GetClientAuthId(admin, AuthId_Steam2, adminAuth, sizeof(adminAuth));
else
strcopy(adminAuth, sizeof(adminAuth), "CONSOLE");
char query[512];
g_hWhitelistDB.Format(query, sizeof(query),
"REPLACE INTO leader_whitelist (steam_id, name, added_by, added_at) VALUES ('%s', '%s', '%s', %d)",
auth, name, adminAuth, GetTime());
g_hWhitelistDB.Query(Whitelist_OnWriteDone, query);
// Update the cache should that player be connected under this ID.
char clientAuth[32];
for (int i = 1; i <= MaxClients; i++)
{
if (!g_bLeaderWhitelisted[i] && IsClientInGame(i) && !IsFakeClient(i)
&& GetClientAuthId(i, AuthId_Steam2, clientAuth, sizeof(clientAuth))
&& StrEqual(auth, clientAuth))
{
g_bLeaderWhitelisted[i] = true;
PrintToChat(i, "%sYou are now on the \x04leader whitelist\x01! Type \x04!leader\x01 to lead at any time.", PREFIX);
}
}
ReplyToCommand(admin, "%s Added %s to the leader whitelist.", PREFIX_PLAIN, auth);
LogAction(admin, -1, "\"%L\" added \"%s\" to the leader whitelist", admin, auth);
}
// Remove a connected client from the whitelist. Returns true if they were on it.
bool Whitelist_Remove(int admin, int target)
{
if (!g_bLeaderWhitelisted[target])
return false;
g_bLeaderWhitelisted[target] = false;
char auth[32];
if (GetClientAuthId(target, AuthId_Steam2, auth, sizeof(auth)))
Whitelist_DeleteAuth(auth);
ReplyToCommand(admin, "%s Removed %N from the leader whitelist.", PREFIX_PLAIN, target);
LogAction(admin, target, "\"%L\" removed \"%L\" from the leader whitelist", admin, target);
return true;
}
// Remove an offline player by SteamID (sm_removeleaderwhitelist STEAM_0:X:XXXX).
void Whitelist_RemoveAuth(int admin, const char[] authArg)
{
if (g_hWhitelistDB == null)
{
ReplyToCommand(admin, "%s The whitelist database is not available.", PREFIX_PLAIN);
return;
}
char auth[32];
if (!Whitelist_NormalizeSteam2(authArg, auth, sizeof(auth)))
{
ReplyToCommand(admin, "%s '%s' is not a valid SteamID (expected STEAM_0:X:XXXX).", PREFIX_PLAIN, authArg);
return;
}
Whitelist_DeleteAuth(auth);
// Clear the cache should that player be connected under this ID.
char clientAuth[32];
for (int i = 1; i <= MaxClients; i++)
{
if (g_bLeaderWhitelisted[i] && IsClientInGame(i)
&& GetClientAuthId(i, AuthId_Steam2, clientAuth, sizeof(clientAuth))
&& StrEqual(auth, clientAuth))
{
g_bLeaderWhitelisted[i] = false;
}
}
ReplyToCommand(admin, "%s Removed %s from the leader whitelist (if present).", PREFIX_PLAIN, auth);
LogAction(admin, -1, "\"%L\" removed \"%s\" from the leader whitelist", admin, auth);
}
void Whitelist_DeleteAuth(const char[] auth)
{
if (g_hWhitelistDB == null)
return;
char query[128];
g_hWhitelistDB.Format(query, sizeof(query), "DELETE FROM leader_whitelist WHERE steam_id = '%s'", auth);
g_hWhitelistDB.Query(Whitelist_OnWriteDone, query);
}
public void Whitelist_OnWriteDone(Database db, DBResultSet results, const char[] error, any data)
{
if (results == null)
LogError("[Leader3] Whitelist write failed: %s", error);
}
//----------------------------------------------------------------------------------------------------
// Commands
//----------------------------------------------------------------------------------------------------
public Action Cmd_AddLeaderWhitelist(int client, int args)
{
if (args < 1)
{
ReplyToCommand(client, "%s Usage: sm_addleaderwhitelist <target|steamid> [name] - adds a player to the leader whitelist.", 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_CONNECTED | COMMAND_FILTER_NO_BOTS, targetName, sizeof(targetName), tnIsMl);
if (count <= 0)
{
// Offline whitelist add by SteamID.
if (StrContains(arg, "STEAM_", false) == 0)
{
char name[MAX_NAME_LENGTH];
if (args >= 2)
GetCmdArg(2, name, sizeof(name));
Whitelist_AddAuth(client, arg, name);
return Plugin_Handled;
}
ReplyToTargetError(client, count);
return Plugin_Handled;
}
for (int i = 0; i < count; i++)
Whitelist_Add(client, targets[i]);
return Plugin_Handled;
}
public Action Cmd_RemoveLeaderWhitelist(int client, int args)
{
if (args < 1)
{
ReplyToCommand(client, "%s Usage: sm_removeleaderwhitelist <target|steamid> - removes a player from the leader whitelist.", 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_CONNECTED | COMMAND_FILTER_NO_BOTS, targetName, sizeof(targetName), tnIsMl);
if (count <= 0)
{
// Offline whitelist removal by SteamID.
if (StrContains(arg, "STEAM_", false) == 0)
{
Whitelist_RemoveAuth(client, arg);
return Plugin_Handled;
}
ReplyToTargetError(client, count);
return Plugin_Handled;
}
for (int i = 0; i < count; i++)
{
if (!Whitelist_Remove(client, targets[i]))
ReplyToCommand(client, "%s %N is not on the leader whitelist.", PREFIX_PLAIN, targets[i]);
}
return Plugin_Handled;
}
public Action Cmd_LeaderWhitelist(int client, int args)
{
if (g_hWhitelistDB == null)
{
ReplyToCommand(client, "%s The whitelist database is not available.", PREFIX_PLAIN);
return Plugin_Handled;
}
g_hWhitelistDB.Query(Whitelist_OnListDone,
"SELECT steam_id, name FROM leader_whitelist ORDER BY added_at DESC",
client > 0 ? GetClientUserId(client) : 0);
return Plugin_Handled;
}
public void Whitelist_OnListDone(Database db, DBResultSet results, const char[] error, any userid)
{
int client = (userid == 0) ? 0 : GetClientOfUserId(userid);
if (userid != 0 && client == 0)
return;
if (results == null)
{
LogError("[Leader3] Whitelist listing failed: %s", error);
ReplyToCommand(client, "%s Failed to read the leader whitelist.", PREFIX_PLAIN);
return;
}
if (results.RowCount == 0)
{
ReplyToCommand(client, "%s The leader whitelist is empty.", PREFIX_PLAIN);
return;
}
ReplyToCommand(client, "%s Leader whitelist (%d):", PREFIX_PLAIN, results.RowCount);
char auth[32], name[MAX_NAME_LENGTH];
while (results.FetchRow())
{
results.FetchString(0, auth, sizeof(auth));
results.FetchString(1, name, sizeof(name));
ReplyToCommand(client, " %s - %s", auth, name);
}
}