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
@@ -0,0 +1,8 @@
"UnlitGeneric"
{
"$basetexture" "models/leader3/glow_ct/white"
"$additive" "1"
"$ignorez" "1"
"$model" "1"
"$nocull" "1"
}
@@ -0,0 +1,8 @@
"UnlitGeneric"
{
"$basetexture" "models/leader3/glow_ct/white"
"$additive" "1"
"$ignorez" "1"
"$model" "1"
"$nocull" "1"
}
@@ -0,0 +1,5 @@
"UnlitGeneric"
{
"$basetexture" "nide/defend"
"$translucent" 1
}
Binary file not shown.
@@ -0,0 +1,5 @@
"UnlitGeneric"
{
"$basetexture" "nide/follow"
"$translucent" 1
}
Binary file not shown.
@@ -0,0 +1,10 @@
"VertexLitGeneric"
{
"$basetexture" "unloze/ping/ping"
"$selfillum" "1"
"$selfillumtint" "[3 3 3]"
//"$selfillummaskscale" "10000"
"$additive" "1"
"$nocull" "1"
}
Binary file not shown.
@@ -0,0 +1,10 @@
"VertexLitGeneric"
{
"$basetexture" "unloze/ping/ping_cross"
"$selfillum" "1"
"$selfillumtint" "[3 3 3]"
//"$selfillummaskscale" "10000"
"$additive" "1"
"$nocull" "1"
}
Binary file not shown.
@@ -0,0 +1,10 @@
"VertexLitGeneric"
{
"$basetexture" "unloze/ping/ping_moon"
"$selfillum" "1"
"$selfillumtint" "[3 3 3]"
//"$selfillummaskscale" "10000"
"$additive" "1"
"$nocull" "1"
}
Binary file not shown.
@@ -0,0 +1,10 @@
"VertexLitGeneric"
{
"$basetexture" "unloze/ping/ping_skull"
"$selfillum" "1"
"$selfillumtint" "[3 3 3]"
//"$selfillummaskscale" "10000"
"$additive" "1"
"$nocull" "1"
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+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);
}
}