Files
sm-plugins/Leader3/scripting/Leader3.sp
T
2026-10-22 16:17:56 +01:00

537 lines
18 KiB
SourcePawn

#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;
}