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
+9 -4
View File
@@ -880,10 +880,15 @@ void BreakCouch(int couch, int attacker)
EmitSoundToAll(g_sExplodeSounds[GetRandomInt(0, sizeof(g_sExplodeSounds) - 1)], SOUND_FROM_PLAYER, SNDCHAN_STATIC, SNDLEVEL_NONE);
EmitSoundToAll(g_sBreakSounds[GetRandomInt(0, sizeof(g_sBreakSounds) - 1)], couch, SNDCHAN_STATIC, SNDLEVEL_SCREAMING);
if (attacker >= 1 && attacker <= MaxClients && IsClientInGame(attacker))
PrintToChatAll(PREFIX ... "%N\x01 broke the couch! Game over!", attacker);
else
PrintToChatAll(PREFIX ... "The couch\x01 broke! Game over!");
// Printed three times so it stands out in a busy chat.
bool bNamed = attacker >= 1 && attacker <= MaxClients && IsClientInGame(attacker);
for (int i = 0; i < 3; i++)
{
if (bNamed)
PrintToChatAll(PREFIX ... "%N\x01 broke the couch! Game over!", attacker);
else
PrintToChatAll(PREFIX ... "The couch\x01 broke! Game over!");
}
// While the entity still exists, so listeners can read its position.
Call_StartForward(g_fwdOnCouchBroken);
+48 -14
View File
@@ -1,6 +1,7 @@
#include <sourcemod>
#include <ripext>
#include <geoip>
#include <multicolors>
#define EVENTNOTIFIER_CORE
#include <EventNotifier>
@@ -8,7 +9,7 @@
#pragma semicolon 1
#pragma newdecls required
#define PLUGIN_VERSION "1.1"
#define PLUGIN_VERSION "2.0"
#define TIMEZONE_FILE "configs/eventnotifier_timezones.cfg"
#define TIMEZONE_NAME_LEN 64
@@ -40,7 +41,7 @@ public Plugin myinfo =
{
name = "EventNotifier",
author = "marisakirisame.net",
description = "Fetches upcoming Unloze events and exposes them to other plugins",
description = "Fetches upcoming Unloze events, shows them via sm_event and announces them in chat",
version = PLUGIN_VERSION,
url = "https://marisakirisame.net"
};
@@ -75,6 +76,9 @@ public void OnPluginStart()
g_cvServer.AddChangeHook(OnSourceChanged);
g_cvInterval.AddChangeHook(OnIntervalChanged);
Menu_OnPluginStart();
Announce_OnPluginStart();
AutoExecConfig(true, "EventNotifier");
RegAdminCmd("sm_event_refresh", Command_EventRefresh, ADMFLAG_GENERIC, "Re-fetches the event list now");
@@ -108,6 +112,7 @@ public void OnClientDisconnect(int client)
public void OnConfigsExecuted()
{
RestartRefreshTimer();
RestartAnnounceTimer();
FetchEvents();
}
@@ -134,37 +139,58 @@ public Action Timer_Refresh(Handle timer)
}
// ---------------------------------------------------------------------------
// Natives
// Public API
//
// The natives stay registered for other plugins. The menu and announcement
// modules compiled into this plugin call these functions instead: with
// EVENTNOTIFIER_CORE defined the include declares no natives, so the API
// names resolve to the plain functions below.
// ---------------------------------------------------------------------------
public any Native_GetEventCount(Handle plugin, int numParams)
int EventNotifier_GetEventCount()
{
return g_Events.Length;
}
public any Native_HasFetched(Handle plugin, int numParams)
bool EventNotifier_HasFetched()
{
return g_bEverFetched;
}
bool EventNotifier_GetEvent(int index, EventInfo ev)
{
if (index < 0 || index >= g_Events.Length)
return false;
g_Events.GetArray(index, ev);
return true;
}
public any Native_GetEventCount(Handle plugin, int numParams)
{
return EventNotifier_GetEventCount();
}
public any Native_HasFetched(Handle plugin, int numParams)
{
return EventNotifier_HasFetched();
}
public any Native_FindEventByNumber(Handle plugin, int numParams)
{
return FindEventByNumber(GetNativeCell(1));
return EventNotifier_FindEventByNumber(GetNativeCell(1));
}
public any Native_GetEvent(Handle plugin, int numParams)
{
int index = GetNativeCell(1);
int size = GetNativeCell(3);
if (size < sizeof(EventInfo))
return ThrowNativeError(SP_ERROR_NATIVE, "EventInfo buffer too small (%d < %d), recompile against the current EventNotifier.inc", size, sizeof(EventInfo));
if (index < 0 || index >= g_Events.Length)
EventInfo ev;
if (!EventNotifier_GetEvent(GetNativeCell(1), ev))
return false;
EventInfo ev;
g_Events.GetArray(index, ev);
SetNativeArray(2, ev, sizeof(EventInfo));
return true;
}
@@ -183,7 +209,7 @@ public any Native_FormatClientTime(Handle plugin, int numParams)
int maxlen = GetNativeCell(4);
char[] buffer = new char[maxlen];
FormatClientTime(client, GetNativeCell(2), buffer, maxlen, GetNativeCell(5));
EventNotifier_FormatClientTime(client, GetNativeCell(2), buffer, maxlen, GetNativeCell(5));
SetNativeString(3, buffer, maxlen);
return 0;
}
@@ -307,6 +333,7 @@ void ParseEvent(JSONObject obj, EventInfo ev)
if (!ev.name[0])
GetJsonString(obj, "title", ev.name, sizeof(ev.name));
GetJsonString(obj, "url", ev.url, sizeof(ev.url));
GetJsonString(obj, "server", ev.server, sizeof(ev.server));
GetJsonString(obj, "rewards", ev.rewards, sizeof(ev.rewards));
GetJsonString(obj, "leader", ev.leader, sizeof(ev.leader));
@@ -364,7 +391,7 @@ public Action Command_EventRefresh(int client, int args)
return Plugin_Handled;
}
int FindEventByNumber(int number)
int EventNotifier_FindEventByNumber(int number)
{
int count = g_Events.Length;
for (int i = 0; i < count; i++)
@@ -475,7 +502,7 @@ void GetClientZonePeriod(int client, int stamp, TzPeriod out)
// Formats stamp in the client's timezone as "Thu 01 Oct 2026, 20:00 BST"
// (or without the year). FormatTime can't be used, it only knows the server's zone.
void FormatClientTime(int client, int stamp, char[] buffer, int maxlen, bool withYear)
void EventNotifier_FormatClientTime(int client, int stamp, char[] buffer, int maxlen, bool withYear = true)
{
static const char weekdays[][] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
static const char months[][] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
@@ -585,3 +612,10 @@ void CivilFromDays(int z, int &y, int &m, int &d)
m = (mp < 10) ? mp + 3 : mp - 9;
y = yoe + era * 400 + ((m <= 2) ? 1 : 0);
}
// ---------------------------------------------------------------------------
// Modules
// ---------------------------------------------------------------------------
#include "EventNotifier/menu.sp"
#include "EventNotifier/announcements.sp"
@@ -1,12 +1,3 @@
#include <sourcemod>
#include <multicolors>
#include <EventNotifier>
#pragma semicolon 1
#pragma newdecls required
#define PLUGIN_VERSION "1.1"
// Shortest automatic announcement interval, in seconds. Values below this are
// raised to it; 0 or below disables the timer entirely.
#define MIN_ANNOUNCE_INTERVAL 60.0
@@ -16,41 +7,25 @@
// Visible characters allowed for the map list before " +N more" is appended.
#define ANNOUNCE_MAPS_BUDGET 120
ConVar g_cvInterval;
ConVar g_cvCount;
ConVar g_cvAnnounceInterval;
ConVar g_cvAnnounceCount;
Handle g_hAnnounceTimer;
int g_iRotation;
public Plugin myinfo =
void Announce_OnPluginStart()
{
name = "EventNotifier Announcements",
author = "marisakirisame.net",
description = "Periodically announces upcoming Unloze events in chat",
version = PLUGIN_VERSION,
url = "https://marisakirisame.net"
};
public void OnPluginStart()
{
g_cvInterval = CreateConVar("sm_event_announce_interval", "120",
g_cvAnnounceInterval = CreateConVar("sm_event_announce_interval", "120",
"Seconds between event announcements in chat, -1 to disable automatic announcements (sm_event_announce still works)", _, true, -1.0);
g_cvCount = CreateConVar("sm_event_announce_count", "3",
g_cvAnnounceCount = CreateConVar("sm_event_announce_count", "3",
"Rotate announcements through the next N upcoming events, one per announcement", _, true, 1.0, true, 20.0);
g_cvInterval.AddChangeHook(OnIntervalChanged);
AutoExecConfig(true, "EventNotifierAnnouncements");
g_cvAnnounceInterval.AddChangeHook(OnAnnounceIntervalChanged);
RegAdminCmd("sm_event_announce", Command_Announce, ADMFLAG_GENERIC, "sm_event_announce [event number] - Posts an event announcement in chat now (next in rotation if no number given)");
}
public void OnConfigsExecuted()
{
RestartAnnounceTimer();
}
public void OnIntervalChanged(ConVar convar, const char[] oldValue, const char[] newValue)
public void OnAnnounceIntervalChanged(ConVar convar, const char[] oldValue, const char[] newValue)
{
RestartAnnounceTimer();
}
@@ -59,7 +34,7 @@ void RestartAnnounceTimer()
{
delete g_hAnnounceTimer;
float interval = g_cvInterval.FloatValue;
float interval = g_cvAnnounceInterval.FloatValue;
if (interval <= 0.0)
return;
@@ -124,7 +99,7 @@ bool PostAnnouncement()
if (count <= 0)
return false;
int window = g_cvCount.IntValue;
int window = g_cvAnnounceCount.IntValue;
if (window > count)
window = count;
@@ -1,27 +1,15 @@
#include <sourcemod>
#include <EventNotifier>
#pragma semicolon 1
#pragma newdecls required
#define PLUGIN_VERSION "1.2"
#define CHAT_PREFIX "\x04[Events]\x01"
#define PANEL_LINE_WIDTH 60
#define KEY_FORUM 1
#define KEY_BACK 8
#define KEY_EXIT 10
public Plugin myinfo =
{
name = "EventNotifier Menu",
author = "marisakirisame.net",
description = "Shows upcoming Unloze events in-game via sm_event",
version = PLUGIN_VERSION,
url = "https://marisakirisame.net"
};
// Event number shown on each client's details panel, for EventDetails_Handler.
static int g_iDetailsNumber[MAXPLAYERS + 1];
public void OnPluginStart()
void Menu_OnPluginStart()
{
RegConsoleCmd("sm_event", Command_EventMenu, "Shows upcoming Unloze events. Usage: sm_event [event number]");
RegConsoleCmd("sm_events", Command_EventMenu, "Shows upcoming Unloze events. Usage: sm_events [event number]");
@@ -174,21 +162,46 @@ void ShowEventDetails(int client, int index)
panel.DrawText(line);
panel.DrawText(" ");
if (ev.url[0])
{
panel.CurrentKey = KEY_FORUM;
panel.DrawItem("View forum post");
}
panel.CurrentKey = KEY_BACK;
panel.DrawItem("Back");
panel.CurrentKey = KEY_EXIT;
panel.DrawItem("Exit");
g_iDetailsNumber[client] = ev.number;
panel.Send(client, EventDetails_Handler, MENU_TIME_FOREVER);
delete panel;
}
public int EventDetails_Handler(Menu menu, MenuAction action, int param1, int param2)
{
if (action == MenuAction_Select && param2 == KEY_BACK)
if (action != MenuAction_Select || !IsClientInGame(param1))
return 0;
if (param2 == KEY_BACK)
{
if (IsClientInGame(param1))
Command_EventMenu(param1, 0);
Command_EventMenu(param1, 0);
}
else if (param2 == KEY_FORUM)
{
int index = EventNotifier_FindEventByNumber(g_iDetailsNumber[param1]);
EventInfo ev;
if (index == -1 || !EventNotifier_GetEvent(index, ev) || !ev.url[0])
{
PrintToChat(param1, "%s That event is no longer in the upcoming list.", CHAT_PREFIX);
return 0;
}
ShowMOTDPanel(param1, ev.name, ev.url, MOTDPANEL_TYPE_URL);
// Keep the details panel underneath so Back/Exit still work after
// the MOTD window is closed.
ShowEventDetails(param1, index);
}
return 0;
}
@@ -5,6 +5,7 @@
#define _EventNotifier_included
#define EVENT_STR_LEN 128
#define EVENT_URL_LEN 192
#define EVENT_MAPS_LEN 1024
enum struct EventInfo
@@ -12,6 +13,7 @@ enum struct EventInfo
int number;
int start; // unix timestamp (UTC), 0 if unknown
int end; // unix timestamp (UTC), 0 if unknown
char url[EVENT_URL_LEN]; // forum thread URL, empty if unknown
char name[EVENT_STR_LEN];
char server[EVENT_STR_LEN];
char rewards[EVENT_STR_LEN];
File diff suppressed because it is too large Load Diff
@@ -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);
}
}
+180
View File
@@ -0,0 +1,180 @@
"NameFilter2"
{
"settings"
{
// names shorter than this after filtering get a replacement name
"min_length" "2"
// percentage of a name's letters that must survive folding; below
// this the name gets a replacement instead. 0 disables the check
"min_survived_letters" "50"
// a name containing a contiguous typeable (printable ASCII) run with
// at least this many letters/digits is kept as-is, unicode decoration
// and all, unless an advert or banned filter matches. An admin can
// target such a name by its typeable part. 0 disables the exemption
"keep_typeable_run" "5"
// non-ASCII characters that are always kept.
// accented Latin letters (À-ÿ, Latin Extended-A) are also kept, but only
// when the name starts with at least two plain Latin letters; otherwise
// they are reduced to their base letter (Å -> A, æ -> ae)
"whitelist" "äöüÄÖܧğĞšŠ"
}
// single-character lookalikes folded to ASCII before unmapped unicode is stripped.
// key = the confusable character (raw UTF-8), value = ASCII replacement
"confusables"
{
// Cyrillic lowercase
"а" "a"
"е" "e"
"о" "o"
"р" "p"
"с" "c"
"у" "y"
"х" "x"
"і" "i"
"ѕ" "s"
"ј" "j"
// Cyrillic uppercase
"А" "A"
"В" "B"
"Е" "E"
"К" "K"
"М" "M"
"Н" "H"
"О" "O"
"Р" "P"
"С" "C"
"Т" "T"
"Х" "X"
"Ѕ" "S"
"І" "I"
"Ј" "J"
"У" "Y"
"З" "3"
// Cyrillic mirrored/aesthetic lookalikes
"Я" "R"
"я" "r"
"И" "N"
"и" "n"
"в" "b"
"к" "k"
"м" "m"
"н" "h"
"т" "t"
// Cyrillic with diacritics, same lookalikes as their base letters
"Ѐ" "E"
"Ё" "E"
"ѐ" "e"
"ё" "e"
"Ї" "I"
"ї" "i"
"Ў" "Y"
"ў" "y"
"Ѝ" "N"
"Й" "N"
"ѝ" "n"
"й" "n"
// Greek
"Α" "A"
"Β" "B"
"Ε" "E"
"Ζ" "Z"
"Η" "H"
"Ι" "I"
"Κ" "K"
"Μ" "M"
"Ν" "N"
"Ο" "O"
"Ρ" "P"
"Τ" "T"
"Υ" "Y"
"Χ" "X"
"α" "a"
"ν" "v"
"ο" "o"
// Greek with tonos/dialytika, same lookalikes as their base letters
"Ά" "A"
"Έ" "E"
"Ή" "H"
"Ί" "I"
"Ϊ" "I"
"Ό" "O"
"Ύ" "Y"
"Ϋ" "Y"
"Ώ" "O"
"ά" "a"
"ή" "n"
"ό" "o"
"ώ" "w"
// Greek aesthetic lookalikes
"Λ" "A"
"Σ" "E"
"Δ" "A"
"Ω" "O"
"Θ" "O"
"θ" "o"
"μ" "u"
"ω" "w"
"η" "n"
// math symbols used as letters
"∂" "d"
"∑" "E"
}
// regexes (case-insensitive) whose matches are REMOVED from the name;
// the rest of the name is kept. Applied to the pure-ASCII folded name.
"adverts"
{
"1" "(astraserver|bruss\.org|marcoplay|marcoserv|css-vip|for-css)\.(ru|com)"
"2" "discord\.gg/\S+"
}
// regexes (case-insensitive); ANY match assigns a replacement name.
// Anchor with ^...$ for full-match "too default" names.
"banned"
{
"1" "^player[0-9]*$"
"2" "^unnamed$"
"3" "^default$"
"4" "^nick(name)?$"
"5" "[a4]+d+m+[il1!]+n+"
"6" "@((!?)me|(c?)t(s?)|(!?)admins|(!?)friends|random((c?)t?)|humans|spec|alive|dead|aim|bots)"
}
// replacement names; picked deterministically (client % count, next-free probe)
"names"
{
"1" "Reimu Hakurei"
"2" "Koishi Komeiji"
"3" "Youmu Konpaku"
"4" "Sakuya Izayoi"
"5" "Flandre Scarlet"
"6" "Remilia Scarlet"
"7" "Satori Komeiji"
"8" "Alice Margatroid"
"9" "Fujiwara no Mokou"
"10" "Aya Shameimaru"
"11" "Tenshi Hinanawi"
"12" "Yukari Yakumo"
"13" "Sanae Kochiya"
"14" "Shion Yorigami"
"15" "Reisen Udongein Inaba"
"16" "Saeko Busujima"
"17" "Rei Miyamoto"
"18" "Saya Takagi"
"19" "Eru Chitanda"
"20" "Fuyumi Irisu"
"21" "Lisara Restall"
"22" "Sen Yarizui"
"23" "Momiji Binboda"
"24" "Ranmaru Rindou"
"25" "Tatenashi Sarashiki"
"26" "Laura Bodewig"
"27" "Houki Shinonono"
"28" "Ririchiyo Shirakiin"
"29" "Karuta Roromiya"
}
}
File diff suppressed because it is too large Load Diff
+131
View File
@@ -0,0 +1,131 @@
#pragma semicolon 1
#include <sourcemod>
#include <sdktools_sound>
#include <sdktools_stringtables>
#include <cstrike>
#include <clientprefs>
#undef REQUIRE_PLUGIN
#include <ccc>
#include <leader>
#include <zombiereloaded>
#define REQUIRE_PLUGIN
#pragma newdecls required
#define PLUGIN_VERSION "1.0"
ConVar g_cvEnabled;
ConVar g_cvCustom;
ConVar g_cvCooldown;
ConVar g_cvRoundLimit;
ConVar g_cvDead;
ConVar g_cvSound;
bool g_bCCC;
bool g_bLeader;
bool g_bZR;
float g_flNextRadio[MAXPLAYERS + 1];
int g_iRadioCount[MAXPLAYERS + 1];
public Plugin myinfo =
{
name = "Radio",
author = "marisakirisame.net",
description = "Stock CS:S radio reimplemented with custom-chatcolors styling",
version = PLUGIN_VERSION,
url = ""
};
public void OnPluginStart()
{
if (GetUserMessageType() == UM_Protobuf)
SetFailState("This plugin only supports bitbuffer usermessages (CS:S).");
if (GetUserMessageId("SendAudio") == INVALID_MESSAGE_ID || GetUserMessageId("SayText2") == INVALID_MESSAGE_ID)
SetFailState("SendAudio/SayText2 usermessages unavailable.");
g_cvEnabled = CreateConVar("sm_radio_enabled", "1", "Enable the custom radio handling (0 = fully stock behaviour)", _, true, 0.0, true, 1.0);
g_cvCustom = CreateConVar("sm_radio_custom", "1", "Enable the custom radio commands (thanks, cheer); 0 = stock radio set only", _, true, 0.0, true, 1.0);
g_cvCooldown = CreateConVar("sm_radio_cooldown", "1.5", "Seconds a player has to wait between radio messages", _, true, 0.0);
g_cvRoundLimit = CreateConVar("sm_radio_round_limit", "0", "Max radio messages per player per round (engine default = 60, 0 = unlimited)", _, true, 0.0);
g_cvDead = CreateConVar("sm_radio_dead", "0", "Allow dead players to use the radio", _, true, 0.0, true, 1.0);
g_cvSound = CreateConVar("sm_radio_sound", "1", "Play the radio voice line", _, true, 0.0, true, 1.0);
AutoExecConfig(true);
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);
Commands_Init();
Grenade_Init();
Bans_Init();
Settings_Init();
}
bool Radio_IsLeader(int client)
{
// A zombified leader gets no [Leader] tag and no cooldown bypass.
if (g_bZR
&& GetFeatureStatus(FeatureType_Native, "ZR_IsClientZombie") == FeatureStatus_Available
&& ZR_IsClientZombie(client))
{
return false;
}
return g_bLeader
&& GetFeatureStatus(FeatureType_Native, "Leader_Is") == FeatureStatus_Available
&& Leader_Is(client);
}
public void OnAllPluginsLoaded()
{
g_bCCC = LibraryExists("ccc");
g_bLeader = LibraryExists("leader");
g_bZR = LibraryExists("zombiereloaded");
}
public void OnLibraryAdded(const char[] name)
{
if (StrEqual(name, "ccc"))
g_bCCC = true;
else if (StrEqual(name, "leader"))
g_bLeader = true;
else if (StrEqual(name, "zombiereloaded"))
g_bZR = true;
}
public void OnLibraryRemoved(const char[] name)
{
if (StrEqual(name, "ccc"))
g_bCCC = false;
else if (StrEqual(name, "leader"))
g_bLeader = false;
else if (StrEqual(name, "zombiereloaded"))
g_bZR = false;
}
public void OnMapStart()
{
Bans_Clear();
Commands_PrecacheSounds();
}
public void OnClientConnected(int client)
{
g_flNextRadio[client] = 0.0;
g_iRadioCount[client] = 0;
Settings_OnClientConnected(client);
}
public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
for (int i = 1; i <= MaxClients; i++)
g_iRadioCount[i] = 0;
}
#include "radio/commands.sp"
#include "radio/grenade.sp"
#include "radio/format.sp"
#include "radio/bans.sp"
#include "radio/settings.sp"
+90
View File
@@ -0,0 +1,90 @@
//----------------------------------------------------------------------------------------------------
// Radio bans: per-map bans keyed by SteamID, so they survive reconnects but die with the map.
//----------------------------------------------------------------------------------------------------
static StringMap g_BannedAuths;
void Bans_Init()
{
g_BannedAuths = new StringMap();
RegAdminCmd("sm_radiomute", Command_RadioBan, ADMFLAG_CHAT, "sm_radiomute <target> - mute target(s)' radio for the rest of the map");
RegAdminCmd("sm_radiounmute", Command_RadioUnban, ADMFLAG_CHAT, "sm_radiounmute <target> - allow target(s) to use the radio again");
}
void Bans_Clear()
{
g_BannedAuths.Clear();
}
bool Bans_IsBanned(int client)
{
char auth[32];
if (!GetClientAuthId(client, AuthId_Steam2, auth, sizeof(auth)))
return false;
int dummy;
return g_BannedAuths.GetValue(auth, dummy);
}
static void Bans_Set(int client, bool banned)
{
char auth[32];
if (!GetClientAuthId(client, AuthId_Steam2, auth, sizeof(auth)))
return;
if (banned)
g_BannedAuths.SetValue(auth, 1);
else
g_BannedAuths.Remove(auth);
}
public Action Command_RadioBan(int client, int argc)
{
return Bans_Command(client, argc, true, "sm_radiomute");
}
public Action Command_RadioUnban(int client, int argc)
{
return Bans_Command(client, argc, false, "sm_radiounmute");
}
static Action Bans_Command(int client, int argc, bool ban, const char[] command)
{
if (argc < 1)
{
ReplyToCommand(client, "[SM] Usage: %s <target>", command);
return Plugin_Handled;
}
char pattern[MAX_TARGET_LENGTH];
GetCmdArg(1, pattern, sizeof(pattern));
char targetName[MAX_TARGET_LENGTH];
int targets[MAXPLAYERS];
bool tnIsML;
int count = ProcessTargetString(pattern, 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++)
Bans_Set(targets[i], ban);
if (ban)
{
ShowActivity2(client, "[SM] ", "Radio-muted %s for the rest of the map.", targetName);
LogAction(client, count == 1 ? targets[0] : -1, "\"%L\" radio-muted \"%s\"", client, targetName);
}
else
{
ShowActivity2(client, "[SM] ", "Radio-unmuted %s.", targetName);
LogAction(client, count == 1 ? targets[0] : -1, "\"%L\" radio-unmuted \"%s\"", client, targetName);
}
return Plugin_Handled;
}
+234
View File
@@ -0,0 +1,234 @@
//----------------------------------------------------------------------------------------------------
// Radio commands: takes over the stock CS:S radio commands (the listener blocks the game's
// handling) and re-emits the voice line plus a custom-chatcolors styled chat line to the team.
//----------------------------------------------------------------------------------------------------
static const char g_sRadioCommands[][] = {
"coverme", "takepoint", "holdpos", "regroup", "followme", "takingfire", "go",
"fallback", "sticktog", "getinpos", "stormfront", "report", "roger", "enemyspot",
"needbackup", "sectorclear", "inposition", "reportingin", "getout", "negative", "enemydown",
"thanks", "cheer"
};
// Everything from this index on is a custom addition, not a stock radio command
// (gated behind sm_radio_custom).
#define RADIO_FIRST_CUSTOM (sizeof(g_sRadioCommands) - 2)
static const char g_sRadioSounds[][] = {
"radio/ct_coverme.wav", "radio/takepoint.wav", "radio/position.wav", "radio/regroup.wav",
"radio/followme.wav", "radio/fireassis.wav", "radio/com_go.wav", "radio/fallback.wav",
"radio/sticktog.wav", "radio/com_getinpos.wav", "radio/stormfront.wav", "radio/com_reportin.wav",
"radio/roger.wav", "radio/ct_enemys.wav", "radio/ct_backup.wav", "radio/clear.wav",
"radio/ct_inpos.wav", "radio/ct_reportingin.wav", "radio/blow.wav", "radio/negative.wav",
"radio/enemydown.wav",
"", "" // thanks/cheer have no stock radio sound; a random bot-chatter wav is picked below
};
#define SOUND_AFFIRMATIVE "radio/ct_affirm.wav"
#define SOUND_FIREINTHEHOLE "radio/ct_fireinhole.wav"
static const char g_sThanksSounds[][] = {
"bot/nice.wav", "bot/nice2.wav", "bot/good_one.wav", "bot/good_one2.wav",
"bot/very_nice.wav", "bot/well_done.wav"
};
static const char g_sCheerSounds[][] = {
"bot/whoo.wav", "bot/whoo2.wav", "bot/yesss.wav", "bot/yesss2.wav",
"bot/yea_baby.wav", "bot/oh_yea.wav", "bot/oh_yea2.wav"
};
// The bot chatter wavs are mastered 6-10 dB hotter than the radio wavs (radio averages
// -16.6 LUFS), so each one carries a gain that brings it down to the radio loudness.
static const float g_flThanksScales[] = {
0.61, 0.44, 0.45, 0.63,
0.43, 0.42
};
static const float g_flCheerScales[] = {
0.33, 0.27, 0.25, 0.32,
0.31, 0.32, 0.39
};
static const char g_sRadioPhrases[][] = {
"Cover Me!", "You take the point.", "Hold This position.", "Regroup Team.",
"Follow Me.", "Taking fire...need assistance!", "Go go go!", "Team, fall back!",
"Stick together, team.", "Get in position and wait for my go.", "Storm the Front!",
"Report in, team.", "Roger that.", "Enemy spotted.", "Need backup.", "Sector clear.",
"I'm in position.", "Reporting in.", "Get out of there, it's gonna blow!",
"Negative.", "Enemy down.",
"Thanks!", "Cheered!!!"
};
static const char g_sCheerColors[][] = {
"FF0000", "FF4500", "FF8C00", "FFD700", "ADFF2F",
"00FF00", "00BFFF", "1E90FF", "0000CD", "9400D3"
};
static const char g_sCheerText[] = "Cheered!!!";
static int g_iCheerOffset;
static void Commands_BuildCheerPhrase(char[] phrase, int maxlen)
{
phrase[0] = '\0';
for (int i = 0; i < sizeof(g_sCheerText) - 1; i++)
{
Format(phrase, maxlen, "%s\x07%s%c", phrase,
g_sCheerColors[(i + g_iCheerOffset) % sizeof(g_sCheerColors)], g_sCheerText[i]);
}
g_iCheerOffset = (g_iCheerOffset + 1) % sizeof(g_sCheerColors);
}
static StringMap g_RadioIndex;
void Commands_PrecacheSounds()
{
for (int i = 0; i < sizeof(g_sRadioSounds); i++)
{
if (g_sRadioSounds[i][0])
PrecacheSound(g_sRadioSounds[i]);
}
PrecacheSound(SOUND_AFFIRMATIVE);
PrecacheSound(SOUND_FIREINTHEHOLE); // grenade.sp
for (int i = 0; i < sizeof(g_sThanksSounds); i++)
PrecacheSound(g_sThanksSounds[i]);
for (int i = 0; i < sizeof(g_sCheerSounds); i++)
PrecacheSound(g_sCheerSounds[i]);
}
void Radio_EmitToPlayers(const int[] players, int playersNum, const char[] sound, float scale = 1.0)
{
for (int i = 0; i < playersNum; i++)
{
float volume = Settings_GetVolume(players[i]);
if (volume <= 0.0)
continue;
EmitSoundToClient(players[i], sound, SOUND_FROM_PLAYER, SNDCHAN_VOICE,
SNDLEVEL_NORMAL, SND_NOFLAGS, volume * scale);
}
}
void Commands_Init()
{
g_RadioIndex = new StringMap();
for (int i = 0; i < sizeof(g_sRadioCommands); i++)
{
g_RadioIndex.SetValue(g_sRadioCommands[i], i);
AddCommandListener(Command_Radio, g_sRadioCommands[i]);
}
}
public Action Command_Radio(int client, const char[] command, int argc)
{
if (!g_cvEnabled.BoolValue)
return Plugin_Continue;
if (client < 1 || client > MaxClients || !IsClientInGame(client))
return Plugin_Continue;
// Console commands are case-insensitive, StringMap keys are not.
char cmd[16];
strcopy(cmd, sizeof(cmd), command);
for (int i = 0; cmd[i]; i++)
cmd[i] = CharToLower(cmd[i]);
int index;
if (!g_RadioIndex.GetValue(cmd, index))
return Plugin_Continue;
// With the custom commands disabled they fall through as unknown commands.
if (index >= RADIO_FIRST_CUSTOM && !g_cvCustom.BoolValue)
return Plugin_Continue;
if (Bans_IsBanned(client))
return Plugin_Handled;
int team = GetClientTeam(client);
if (team != CS_TEAM_T && team != CS_TEAM_CT)
return Plugin_Handled;
if (!IsPlayerAlive(client) && !g_cvDead.BoolValue)
return Plugin_Handled;
bool bLeader = Radio_IsLeader(client);
float flNow = GetGameTime();
bool bPlaySound = g_cvSound.BoolValue;
if (!bLeader)
{
if (flNow < g_flNextRadio[client])
return Plugin_Handled;
int limit = g_cvRoundLimit.IntValue;
if (limit > 0 && g_iRadioCount[client] >= limit)
return Plugin_Handled;
g_flNextRadio[client] = flNow + g_cvCooldown.FloatValue;
}
else if (flNow < g_flNextRadio[client])
{
// Leaders may spam the chat line, but the voice line still honours the cooldown.
bPlaySound = false;
}
else
{
g_flNextRadio[client] = flNow + g_cvCooldown.FloatValue;
}
g_iRadioCount[client]++;
bool bCustom = (index >= RADIO_FIRST_CUSTOM);
int[] players = new int[MaxClients];
int playersNum = 0;
for (int i = 1; i <= MaxClients; i++)
{
if (!IsClientInGame(i) || IsFakeClient(i) || GetClientTeam(i) != team)
continue;
// Players may opt out of the custom commands in !settings.
if (bCustom && !Settings_GetCustom(i))
continue;
players[playersNum++] = i;
}
if (!playersNum)
return Plugin_Handled;
// phrase must hold the coloured cheer line (10 glyphs each prefixed by a 7-byte \x07RRGGBB code)
char sound[32], phrase[128];
float soundScale = 1.0;
strcopy(sound, sizeof(sound), g_sRadioSounds[index]);
strcopy(phrase, sizeof(phrase), g_sRadioPhrases[index]);
// Stock "roger" picks one of two acknowledgements at random.
if (StrEqual(cmd, "roger") && GetRandomInt(0, 1))
{
strcopy(sound, sizeof(sound), SOUND_AFFIRMATIVE);
strcopy(phrase, sizeof(phrase), "Affirmative.");
}
else if (StrEqual(cmd, "thanks"))
{
int pick = GetRandomInt(0, sizeof(g_sThanksSounds) - 1);
strcopy(sound, sizeof(sound), g_sThanksSounds[pick]);
soundScale = g_flThanksScales[pick];
}
else if (StrEqual(cmd, "cheer"))
{
int pick = GetRandomInt(0, sizeof(g_sCheerSounds) - 1);
strcopy(sound, sizeof(sound), g_sCheerSounds[pick]);
soundScale = g_flCheerScales[pick];
Commands_BuildCheerPhrase(phrase, sizeof(phrase));
}
if (bPlaySound && sound[0])
Radio_EmitToPlayers(players, playersNum, sound, soundScale);
char text[256];
Format_RadioLine(client, bLeader, phrase, text, sizeof(text));
Radio_SendChat(client, players, playersNum, text);
return Plugin_Handled;
}
+97
View File
@@ -0,0 +1,97 @@
//----------------------------------------------------------------------------------------------------
// Radio format: assembles the chat line, colouring the sender's tag/name/text through
// custom-chatcolors when it is loaded. Colour mapping mirrors custom-chatcolors.sp.
//----------------------------------------------------------------------------------------------------
// Same prefix Leader3 puts on its own chat lines (Leader3.sp PREFIX).
#define LEADER_PREFIX "\x07FF00E1[Leader] \x01"
// ent_idx = author makes \x03 render as the author's team colour; the chat flag must be
// true (the client's non-chat path mishandles raw coloured text); BLOCKHOOKS keeps chat
// processors (CCC included) from re-processing our synthetic line.
void Radio_SendChat(int author, const int[] players, int playersNum, const char[] text)
{
Handle say = StartMessage("SayText2", players, playersNum, USERMSG_RELIABLE | USERMSG_BLOCKHOOKS);
BfWriteByte(say, author);
BfWriteByte(say, 1);
BfWriteString(say, text);
EndMessage();
}
static void ColorCode(int color, bool alpha, char[] out, int maxlen)
{
switch (color)
{
case COLOR_TEAM: strcopy(out, maxlen, "\x03");
case COLOR_CGREEN: strcopy(out, maxlen, "\x04");
case COLOR_OLIVE: strcopy(out, maxlen, "\x05");
case COLOR_NULL: out[0] = '\0';
case COLOR_NONE: strcopy(out, maxlen, "\x01");
default:
{
if (alpha)
FormatEx(out, maxlen, "\x08%08X", color);
else
FormatEx(out, maxlen, "\x07%06X", color);
}
}
}
void Format_RadioLine(int client, bool leader, const char[] phrase, char[] out, int maxlen)
{
char sender[MAX_NAME_LENGTH + 96];
bool useCCC = g_bCCC && GetFeatureStatus(FeatureType_Native, "CCC_GetColor") == FeatureStatus_Available;
if (useCCC)
{
// CCC stores tags in 64-byte buffers (custom-chatcolors.sp g_sTag) and a processed
// tag can carry an inline \x07RRGGBB code per character, so match CCC's size.
char tag[64], tagCode[16], nameCode[16];
bool alpha;
CCC_GetTag(client, tag, sizeof(tag));
alpha = false;
ColorCode(CCC_GetColor(client, CCC_TagColor, alpha), alpha, tagCode, sizeof(tagCode));
alpha = false;
int nameColor = CCC_GetColor(client, CCC_NameColor, alpha);
if (nameColor == COLOR_NULL && tag[0])
{
// CCC name colour "X" (COLOR_NULL) erases the name so only the tag shows
FormatEx(sender, sizeof(sender), "%s%s", tagCode, tag);
}
else
{
ColorCode(nameColor, alpha, nameCode, sizeof(nameCode));
if (tag[0])
FormatEx(sender, sizeof(sender), "%s%s%s%N", tagCode, tag, nameCode, client);
else
FormatEx(sender, sizeof(sender), "%s%N", nameCode, client);
}
}
else
{
FormatEx(sender, sizeof(sender), "\x03%N", client);
}
char location[64];
GetEntPropString(client, Prop_Send, "m_szLastPlaceName", location, sizeof(location));
// Leading \x01 guards the CS:S quirk where a colour code as the very first byte may not
// render; the \x01 before (RADIO): resets the line to the default orange, so the phrase
// renders like a stock radio message regardless of the player's CCC chat colour.
if (location[0])
{
FormatEx(out, maxlen, "\x01%s%s \x01@ %s (RADIO): %s",
leader ? LEADER_PREFIX : "", sender, location, phrase);
}
else
{
FormatEx(out, maxlen, "\x01%s%s \x01(RADIO): %s",
leader ? LEADER_PREFIX : "", sender, phrase);
}
}
+132
View File
@@ -0,0 +1,132 @@
//----------------------------------------------------------------------------------------------------
// Grenade announce: throwing a grenade makes the game itself emit "Fire in the hole!" via the
// RadioText usermessage (it is not a client command), so intercept it and re-send the line
// styled a frame later. The game's SendAudio (Radio.FireInTheHole) is likewise intercepted and
// re-emitted through EmitSoundToClient so each listener's radio volume preference applies.
//----------------------------------------------------------------------------------------------------
#define FIRE_IN_THE_HOLE_TOKEN "#Cstrike_TitlesTXT_Fire_in_the_hole"
void Grenade_Init()
{
UserMsg RadioText = GetUserMessageId("RadioText");
if (RadioText == INVALID_MESSAGE_ID)
SetFailState("RadioText usermessage unavailable.");
HookUserMessage(RadioText, Hook_RadioText, true);
HookUserMessage(GetUserMessageId("SendAudio"), Hook_SendAudio, true);
}
public Action Hook_SendAudio(UserMsg msg_id, Handle bf, const int[] players, int playersNum, bool reliable, bool init)
{
if (!g_cvEnabled.BoolValue)
return Plugin_Continue;
char sound[32];
BfReadString(bf, sound, sizeof(sound), false);
// Grenade announces are the only radio SendAudio the engine still emits (the command
// listeners block the rest); round events (bomb/win jingles) pass through untouched.
if (!StrEqual(sound, "Radio.FireInTheHole"))
return Plugin_Continue;
// Sounds cannot be emitted from inside a usermessage hook either, so re-emit next frame.
DataPack pack = new DataPack();
pack.WriteCell(playersNum);
for (int i = 0; i < playersNum; i++)
{
int target = players[i];
pack.WriteCell((target >= 1 && target <= MaxClients && IsClientInGame(target)) ? GetClientUserId(target) : 0);
}
RequestFrame(OnFireInTheHoleResend, pack);
return Plugin_Handled;
}
public void OnFireInTheHoleResend(DataPack pack)
{
pack.Reset();
int num = pack.ReadCell();
int[] players = new int[MaxClients];
int playersNum = 0;
for (int i = 0; i < num; i++)
{
int target = GetClientOfUserId(pack.ReadCell());
if (target && !IsFakeClient(target))
players[playersNum++] = target;
}
delete pack;
Radio_EmitToPlayers(players, playersNum, SOUND_FIREINTHEHOLE);
}
public Action Hook_RadioText(UserMsg msg_id, Handle bf, const int[] players, int playersNum, bool reliable, bool init)
{
if (!g_cvEnabled.BoolValue)
return Plugin_Continue;
BfReadByte(bf); // msg_dest
int client = BfReadByte(bf);
if (client < 1 || client > MaxClients || !IsClientInGame(client))
return Plugin_Continue;
// Layout: msg_name, then up to four params. "#Game_radio" carries the phrase token in
// param2, "#Game_radio_location" in param3 (param2 is the nav place name).
char msgName[64], param[64], token[64];
BfReadString(bf, msgName, sizeof(msgName), false); // msg_name
BfReadString(bf, param, sizeof(param), false); // param1 = player name
BfReadString(bf, token, sizeof(token), false); // param2
BfReadString(bf, param, sizeof(param), false); // param3
if (StrEqual(msgName, "#Game_radio_location"))
strcopy(token, sizeof(token), param);
else if (!StrEqual(msgName, "#Game_radio"))
return Plugin_Continue;
if (!StrEqual(token, FIRE_IN_THE_HOLE_TOKEN))
return Plugin_Continue;
if (Bans_IsBanned(client))
return Plugin_Handled;
// Usermessages cannot be sent from inside a usermessage hook, so stash the data and
// recipients (as userids) and re-send next frame.
DataPack pack = new DataPack();
pack.WriteCell(GetClientUserId(client));
pack.WriteCell(playersNum);
for (int i = 0; i < playersNum; i++)
{
int target = players[i];
pack.WriteCell((target >= 1 && target <= MaxClients && IsClientInGame(target)) ? GetClientUserId(target) : 0);
}
RequestFrame(OnGrenadeResend, pack);
return Plugin_Handled;
}
public void OnGrenadeResend(DataPack pack)
{
pack.Reset();
int client = GetClientOfUserId(pack.ReadCell());
int num = pack.ReadCell();
int[] players = new int[MaxClients];
int playersNum = 0;
for (int i = 0; i < num; i++)
{
int target = GetClientOfUserId(pack.ReadCell());
if (target && !IsFakeClient(target))
players[playersNum++] = target;
}
delete pack;
if (!client || !IsClientInGame(client) || !playersNum)
return;
char text[256];
Format_RadioLine(client, Radio_IsLeader(client), "Fire in the hole!", text, sizeof(text));
Radio_SendChat(client, players, playersNum, text);
}
+191
View File
@@ -0,0 +1,191 @@
//----------------------------------------------------------------------------------------------------
// Radio settings: per-player preferences stored as clientprefs cookies, exposed as a "Radio" entry
// in the stock Client Preferences menu (sm_settings / !settings) that opens a sub-menu with the
// voice-line volume and a toggle for receiving the custom radio commands (thanks, cheer).
//----------------------------------------------------------------------------------------------------
static Cookie g_hVolumeCookie;
static Cookie g_hCustomCookie;
static int g_iVolume[MAXPLAYERS + 1];
static bool g_bCustom[MAXPLAYERS + 1];
static const int g_iVolumeSteps[] = {100, 75, 50, 25, 0};
#define RADIO_VOLUME_DEFAULT 100
void Settings_Init()
{
g_hVolumeCookie = new Cookie("radio_volume", "Radio voice line volume in percent (0 = off)", CookieAccess_Protected);
g_hCustomCookie = new Cookie("radio_custom", "Receive the custom radio commands (thanks, cheer)", CookieAccess_Protected);
SetCookieMenuItem(CookieMenu_Radio, 0, "Radio");
// Late load: clients may already be in game with cached cookies.
for (int i = 1; i <= MaxClients; i++)
{
g_iVolume[i] = RADIO_VOLUME_DEFAULT;
g_bCustom[i] = true;
if (IsClientConnected(i) && AreClientCookiesCached(i))
OnClientCookiesCached(i);
}
}
void Settings_OnClientConnected(int client)
{
// until the cookies arrive
g_iVolume[client] = RADIO_VOLUME_DEFAULT;
g_bCustom[client] = true;
}
public void OnClientCookiesCached(int client)
{
char val[8];
g_hVolumeCookie.Get(client, val, sizeof(val));
if (val[0]) // never set = keep the default
{
int volume = StringToInt(val);
if (volume < 0)
volume = 0;
else if (volume > 100)
volume = 100;
g_iVolume[client] = volume;
}
g_hCustomCookie.Get(client, val, sizeof(val));
if (val[0])
g_bCustom[client] = StringToInt(val) != 0;
}
float Settings_GetVolume(int client)
{
return g_iVolume[client] / 100.0;
}
bool Settings_GetCustom(int client)
{
return g_bCustom[client];
}
public void CookieMenu_Radio(int client, CookieMenuAction action, any info, char[] buffer, int maxlen)
{
switch (action)
{
case CookieMenuAction_DisplayOption:
{
FormatEx(buffer, maxlen, "Radio");
}
case CookieMenuAction_SelectOption:
{
Settings_ShowMainMenu(client);
}
}
}
static void Settings_ShowMainMenu(int client)
{
Menu menu = new Menu(Menu_RadioMain);
menu.SetTitle("Radio Settings");
char display[64];
FormatEx(display, sizeof(display), "Sound volume: %d%%", g_iVolume[client]);
menu.AddItem("volume", display);
FormatEx(display, sizeof(display), "Custom Radio (thanks, cheer): %s",
g_bCustom[client] ? "Enabled" : "Disabled");
menu.AddItem("custom", display);
menu.ExitBackButton = true; // back to sm_settings
menu.Display(client, MENU_TIME_FOREVER);
}
public int Menu_RadioMain(Menu menu, MenuAction action, int client, int param)
{
switch (action)
{
case MenuAction_Select:
{
char item[8];
menu.GetItem(param, item, sizeof(item));
if (StrEqual(item, "volume"))
{
Settings_ShowVolumeMenu(client);
}
else if (StrEqual(item, "custom"))
{
g_bCustom[client] = !g_bCustom[client];
g_hCustomCookie.Set(client, g_bCustom[client] ? "1" : "0");
Settings_ShowMainMenu(client); // re-show with the new state
}
}
case MenuAction_Cancel:
{
if (param == MenuCancel_ExitBack)
ShowCookieMenu(client);
}
case MenuAction_End:
{
delete menu;
}
}
return 0;
}
static void Settings_ShowVolumeMenu(int client)
{
Menu menu = new Menu(Menu_RadioVolume);
menu.SetTitle("Radio Sound Volume");
char item[8], display[16];
for (int i = 0; i < sizeof(g_iVolumeSteps); i++)
{
int volume = g_iVolumeSteps[i];
IntToString(volume, item, sizeof(item));
if (volume > 0)
FormatEx(display, sizeof(display), "%d%%", volume);
else
strcopy(display, sizeof(display), "Off");
if (volume == g_iVolume[client])
StrCat(display, sizeof(display), " <<");
menu.AddItem(item, display);
}
menu.ExitBackButton = true; // back to the Radio sub-menu
menu.Display(client, MENU_TIME_FOREVER);
}
public int Menu_RadioVolume(Menu menu, MenuAction action, int client, int param)
{
switch (action)
{
case MenuAction_Select:
{
char item[8];
menu.GetItem(param, item, sizeof(item));
int volume = StringToInt(item);
g_iVolume[client] = volume;
char val[8];
IntToString(volume, val, sizeof(val));
g_hVolumeCookie.Set(client, val);
Settings_ShowVolumeMenu(client); // re-show with the new pick marked
}
case MenuAction_Cancel:
{
if (param == MenuCancel_ExitBack)
Settings_ShowMainMenu(client);
}
case MenuAction_End:
{
delete menu;
}
}
return 0;
}
+158 -5
View File
@@ -2,6 +2,7 @@
#include <sdktools>
#include <cstrike>
#include <zombiereloaded>
#include <ripext>
#pragma semicolon 1
#pragma newdecls required
@@ -14,7 +15,19 @@ bool g_bWarmup = false;
ConVar g_CVar_sm_warmuptime;
ConVar g_CVar_sm_warmupratio;
ConVar g_CVar_sm_warmupmaxtime;
ConVar g_CVar_sm_warmupmintime;
ConVar g_CVar_sm_warmupbase;
ConVar g_CVar_sm_warmupperplayer;
ConVar g_CVar_sm_warmuppermib;
ConVar g_CVar_sm_warmupdefaultmib;
ConVar g_CVar_sm_warmupfastdl;
bool g_bLateLoad = false;
// Max warmup inputs: compressed map size on fastdl and the most players seen still connecting
float g_fMapMiB = 0.0;
int g_iPeakConnecting = 0;
int g_iMapSerial = 0;
bool g_bTriedLowercase = false;
Handle g_hWarmupEndFwd;
bool g_bRoundEnded = false;
@@ -40,8 +53,14 @@ public void OnPluginStart()
HookEvent("round_end", OnRoundEnd, EventHookMode_PostNoCopy);
g_CVar_sm_warmuptime = CreateConVar("sm_warmuptime", "10", "Warmup timer.", 0, true, 0.0, true, 60.0);
g_CVar_sm_warmupratio = CreateConVar("sm_warmupratio", "0.60", "Ratio of connected players that need to be in game to start warmup timer.", 0, true, 0.0, true, 1.0);
g_CVar_sm_warmupmaxtime = CreateConVar("sm_warmupmaxtime", "45", "Max Warmup timer.", 0, true, 0.0, true, 120.0);
g_CVar_sm_warmupratio = CreateConVar("sm_warmupratio", "0.90", "Ratio of connected players that need to be in game to start warmup timer.", 0, true, 0.0, true, 1.0);
g_CVar_sm_warmupmaxtime = CreateConVar("sm_warmupmaxtime", "150", "Upper limit for the computed max warmup time.", 0, true, 0.0, true, 600.0);
g_CVar_sm_warmupmintime = CreateConVar("sm_warmupmintime", "30", "Lower limit for the computed max warmup time.", 0, true, 0.0, true, 600.0);
g_CVar_sm_warmupbase = CreateConVar("sm_warmupbase", "20", "Base seconds of the computed max warmup time.", 0, true, 0.0);
g_CVar_sm_warmupperplayer = CreateConVar("sm_warmupperplayer", "0.0", "Max warmup seconds added per player still connecting.", 0, true, 0.0);
g_CVar_sm_warmuppermib = CreateConVar("sm_warmuppermib", "0.5", "Max warmup seconds added per MiB of the compressed map on fastdl.", 0, true, 0.0);
g_CVar_sm_warmupdefaultmib = CreateConVar("sm_warmupdefaultmib", "60", "Map size in MiB to assume until, or if, the fastdl lookup fails.", 0, true, 0.0);
g_CVar_sm_warmupfastdl = CreateConVar("sm_warmupfastdl", "https://fastdl.unloze.com/css_ze/maps/", "FastDL maps URL, <map>.bsp.bz2 is appended.");
g_hWarmupEndFwd = CreateGlobalForward("TeamManager_WarmupEnd", ET_Ignore);
@@ -81,17 +100,151 @@ public void OnConfigsExecuted()
g_bRoundEnded = false;
g_bZombieSpawned = false;
if(g_CVar_sm_warmuptime.IntValue > 0 || g_CVar_sm_warmupratio.FloatValue > 0.0)
g_iMapSerial++;
g_iPeakConnecting = 0;
g_fMapMiB = g_CVar_sm_warmupdefaultmib.FloatValue;
g_bTriedLowercase = false;
// sm_warmuptime 0 disables warmup entirely, some maps break if the round is restarted
if(g_CVar_sm_warmuptime.IntValue > 0)
{
char sMap[PLATFORM_MAX_PATH];
GetCurrentMap(sMap, sizeof(sMap));
RequestMapSize(sMap);
g_bWarmup = true;
CreateTimer(1.0, OnWarmupTimer, 0, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE);
}
}
void RequestMapSize(const char[] sMap)
{
char sBaseURL[256];
g_CVar_sm_warmupfastdl.GetString(sBaseURL, sizeof(sBaseURL));
char sURL[512];
Format(sURL, sizeof(sURL), "%s%s.bsp.bz2", sBaseURL, sMap);
// rip.ext has no HEAD, a one byte range request returns the full size in Content-Range instead
HTTPRequest request = new HTTPRequest(sURL);
request.SetHeader("Range", "bytes=0-0");
request.Timeout = 10;
request.Get(OnMapSizeResponse, g_iMapSerial);
}
public void OnMapSizeResponse(HTTPResponse response, any serial, const char[] error)
{
if(serial != g_iMapSerial)
return;
char sMap[PLATFORM_MAX_PATH];
GetCurrentMap(sMap, sizeof(sMap));
if(error[0])
{
LogMessage("Map size lookup for %s failed (%s), assuming %.1f MiB.", sMap, error, g_fMapMiB);
return;
}
char sSize[64];
bool bFound = false;
if(response.Status == HTTPStatus_PartialContent && response.GetHeader("Content-Range", sSize, sizeof(sSize)))
{
int iSlash = FindCharInString(sSize, '/', true);
if(iSlash != -1)
{
strcopy(sSize, sizeof(sSize), sSize[iSlash + 1]);
bFound = true;
}
}
else if(response.Status == HTTPStatus_OK && response.GetHeader("Content-Length", sSize, sizeof(sSize)))
bFound = true;
else if(response.Status == HTTPStatus_NotFound && !g_bTriedLowercase)
{
// fastdl is case sensitive and mostly stores lowercase names
g_bTriedLowercase = true;
char sLower[PLATFORM_MAX_PATH];
strcopy(sLower, sizeof(sLower), sMap);
for(int i = 0; sLower[i]; i++)
sLower[i] = CharToLower(sLower[i]);
if(!StrEqual(sLower, sMap))
{
RequestMapSize(sLower);
return;
}
}
// float, a large file would overflow an int byte count
float fBytes = bFound ? StringToFloat(sSize) : 0.0;
if(fBytes <= 0.0)
{
LogMessage("Map size lookup for %s got no size (HTTP %d), assuming %.1f MiB.", sMap, response.Status, g_fMapMiB);
return;
}
g_fMapMiB = fBytes / 1048576.0;
LogMessage("Map %s is %.1f MiB on fastdl.", sMap, g_fMapMiB);
}
int GetMaxWarmupTime()
{
float fTime = g_CVar_sm_warmupbase.FloatValue
+ g_CVar_sm_warmupperplayer.FloatValue * g_iPeakConnecting
+ g_CVar_sm_warmuppermib.FloatValue * g_fMapMiB;
int iTime = RoundToCeil(fTime);
int iMin = g_CVar_sm_warmupmintime.IntValue;
int iMax = g_CVar_sm_warmupmaxtime.IntValue;
if(iTime < iMin)
iTime = iMin;
if(iTime > iMax)
iTime = iMax;
return iTime;
}
public Action OnWarmupTimer(Handle timer)
{
if(g_iMaxWarmup >= g_CVar_sm_warmupmaxtime.IntValue || g_iWarmup >= g_CVar_sm_warmuptime.IntValue)
// Disabled after warmup started (e.g. a map config ran late): stop without restarting the round
if(g_CVar_sm_warmuptime.IntValue <= 0)
{
g_iMaxWarmup = 0;
g_bWarmup = false;
Call_StartForward(g_hWarmupEndFwd);
Call_Finish();
return Plugin_Stop;
}
// Only players still loading in count, anyone who left during the map change is already gone
int iConnecting = 0;
int iReady = 0;
for(int client = 1; client <= MaxClients; client++)
{
if(!IsClientConnected(client) || IsFakeClient(client))
continue;
if(IsClientInGame(client))
iReady++;
else
iConnecting++;
}
if(iConnecting > g_iPeakConnecting)
g_iPeakConnecting = iConnecting;
int iMaxWarmupTime = GetMaxWarmupTime();
if(g_iMaxWarmup >= iMaxWarmupTime || g_iWarmup >= g_CVar_sm_warmuptime.IntValue)
{
char sMap[PLATFORM_MAX_PATH];
GetCurrentMap(sMap, sizeof(sMap));
LogMessage("Warmup on %s ended after %ds with %d ready players: %.1f MiB, %d peak connecting, max warmup %ds.",
sMap, g_iMaxWarmup + g_iWarmup, iReady, g_fMapMiB, g_iPeakConnecting, iMaxWarmupTime);
g_iMaxWarmup = 0;
g_bWarmup = false;
float fDelay = 3.0;
@@ -110,7 +263,7 @@ public Action OnWarmupTimer(Handle timer)
if(ClientsInGame < ClientsNeeded)
{
g_iMaxWarmup++;
PrintCenterTextAll("Warmup: Waiting for %d more players to join or %d seconds.", ClientsNeeded - ClientsInGame, g_CVar_sm_warmupmaxtime.IntValue - g_iMaxWarmup);
PrintCenterTextAll("Warmup: Waiting for %d more players to join or %d seconds.", ClientsNeeded - ClientsInGame, iMaxWarmupTime - g_iMaxWarmup);
return Plugin_Continue;
}
else
+13 -6
View File
@@ -256,6 +256,17 @@ public Action ZR_OnClientMotherZombieEligible(int client)
return Plugin_Continue;
}
stock bool IsNemesisMap()
{
ConVar cvar = FindConVar("sm_info_message_file"); // not every server runs the info message plugin
if (cvar == null)
return false;
char sNemesis[32];
cvar.GetString(sNemesis, sizeof(sNemesis));
return StrEqual(sNemesis, "nemesis");
}
stock bool IsValidClient(int client)
{
if (client > 0 && client <= MaxClients && IsClientConnected(client) && IsClientInGame(client))
@@ -420,9 +431,7 @@ public Action Command_Testround(int client, int args)
public Action Command_ZShield(int client, int args)
{
char sNemesis[32];
GetConVarString(FindConVar("sm_info_message_file"), sNemesis, sizeof(sNemesis));
bool bNemesis = StrEqual(sNemesis, "nemesis");
bool bNemesis = IsNemesisMap();
if(bNemesis)
{
@@ -520,9 +529,7 @@ public Action Timer_LockMotherZMNames(Handle timer)
public Action Timer_HandleHPShield(Handle timer)
{
char sNemesis[32];
GetConVarString(FindConVar("sm_info_message_file"), sNemesis, sizeof(sNemesis));
bool bNemesis = StrEqual(sNemesis, "nemesis");
bool bNemesis = IsNemesisMap();
for(int i = 1; i <= MaxClients; i++)
{
+1 -3
View File
@@ -12,7 +12,6 @@ bool g_bHasSomebodyToHide[MAXPLAYERS + 1];
/* INTEGERS */
int g_iHideRange[MAXPLAYERS+1];
int g_iLeader = 0;
int g_iHideCapacity = 8;
/* HANDLES */
@@ -203,7 +202,7 @@ public Action UpdateHide(Handle timer)
{
GetClientAbsOrigin(target, fOriginTarget);
//PrintToChatAll("%N--%N::::::%f", client, target, GetVectorDistance(fOriginTarget, fOriginClient, false));
if((GetVectorDistance(fOriginTarget, fOriginClient, true) <= float(g_iHideRange[client])) && (g_iLeader != target))
if((GetVectorDistance(fOriginTarget, fOriginClient, true) <= float(g_iHideRange[client])) && !Leader_Is(target))
{
g_bHidePlayers[client][target] = true;
g_bHasSomebodyToHide[client] = true;
@@ -219,7 +218,6 @@ public Action UpdateHide(Handle timer)
}
}
}
g_iLeader = Leader_CurrentLeader()
return Plugin_Continue;
}
+167 -108
View File
@@ -13,10 +13,22 @@ bool g_bHasSomebodyToHide[MAXPLAYERS + 1];
bool g_bHasTobeVisible[MAXPLAYERS + 1];
bool g_bHidePlayers[MAXPLAYERS + 1][MAXPLAYERS + 1];
/* PER-TICK CACHED PLAYER STATE, refreshed at the top of UpdateHide so the pair loops are plain array reads */
bool g_bInGame[MAXPLAYERS + 1];
bool g_bFake[MAXPLAYERS + 1];
bool g_bAlive[MAXPLAYERS + 1];
bool g_bHuman[MAXPLAYERS + 1];
bool g_bZombie[MAXPLAYERS + 1];
bool g_bLeader[MAXPLAYERS + 1];
bool g_bHasItem[MAXPLAYERS + 1];
bool g_bHooked[MAXPLAYERS + 1];
bool g_bTargetEligible[MAXPLAYERS + 1];
float g_fOrigin[MAXPLAYERS + 1][3];
/* INTEGERS */
int g_iHideRange[MAXPLAYERS+1];
int g_iLeader = 0;
int g_iTierHide = 0;
int g_iNextClient = 1;
/* HANDLES */
Handle g_hTimer;
@@ -48,6 +60,12 @@ public Plugin myinfo =
url = "www.unloze.com"
};
public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
MarkNativeAsOptional("GetPlayerTier_native");
return APLRes_Success;
}
//----------------------------------------------------------------------------------------------------
// Purpose:
//----------------------------------------------------------------------------------------------------
@@ -73,7 +91,9 @@ public void OnPluginStart()
}
SetCookieMenuItem(MenuHandler_CookieMenu, 0, "Hide");
g_hTimer = CreateTimer(1.0, UpdateHide, INVALID_HANDLE, TIMER_REPEAT);
//0.1s ticks each processing a tenth of the slots: every client still refreshes about once a second,
//but the cost is spread over ten frames instead of one spike
g_hTimer = CreateTimer(0.1, UpdateHide, INVALID_HANDLE, TIMER_REPEAT);
g_iTierHide = FindTierForCommandOverride("sm_hide");
}
@@ -108,129 +128,166 @@ public Action UpdateHide(Handle timer)
if (!g_hCVar_HideEnabled.BoolValue)
return Plugin_Continue;
g_iLeader = Leader_CurrentLeader();
for (int client = 1; client <= MaxClients; client++)
//early-out when nobody has hide enabled and no hide state is left to clean up
bool bAnyWork = false;
for (int i = 1; i <= MaxClients; i++)
{
if (!IsValidClient(client) || IsClientSourceTV(client) || IsFakeClient(client))
if ((g_iHideRange[i] != DISABLED || g_bHasSomebodyToHide[i]) && IsClientInGame(i))
{
bAnyWork = true;
break;
}
}
if (!bAnyWork)
return Plugin_Continue;
//cache the per-player state once per tick. everything the pair loops need depends on a single
//player, so this costs natives once per player instead of once per pair.
for (int i = 1; i <= MaxClients; i++)
{
g_bInGame[i] = IsClientInGame(i) && !IsClientSourceTV(i);
if (!g_bInGame[i])
{
g_bTargetEligible[i] = false;
continue;
}
g_bFake[i] = IsFakeClient(i);
g_bHooked[i] = TransmitManager_IsEntityHooked(i);
g_bAlive[i] = IsPlayerAlive(i);
g_bHuman[i] = g_bAlive[i] && ZR_IsClientHuman(i);
g_bZombie[i] = g_bAlive[i] && ZR_IsClientZombie(i);
g_bLeader[i] = Leader_Is(i);
g_bHasItem[i] = EW_ClientHasItem(i);
if (g_bAlive[i])
GetClientAbsOrigin(i, g_fOrigin[i]);
if (g_iHideRange[client] == DISABLED) //client just disabled hide right now. the bool will pass once until they enabled hide again.
{
if (g_bHasSomebodyToHide[client] && TransmitManager_IsEntityHooked(client))
{
for (int target = 1; target <= MaxClients; target++)
{
if (IsValidClient(target) && client != target && TransmitManager_IsEntityHooked(target))
{
TransmitManager_SetEntityState(target, client, true); //client can see all players again
g_bHidePlayers[client][target] = false;
}
}
g_bHasSomebodyToHide[client] = false;
}
continue;
}
//the target is not the leader, has no item, is alive and is a human. so consider him for the distance check.
g_bTargetEligible[i] = g_bHuman[i] && !g_bLeader[i] && !g_bHasItem[i];
}
//lazy fix for invisible zm
if (TransmitManager_IsEntityHooked(client))
{
for (int target = 1; target <= MaxClients; target++)
{
if (IsValidClient(target) && client != target && g_bHidePlayers[client][target]
&& IsPlayerAlive(target) && ZR_IsClientZombie(target) && TransmitManager_IsEntityHooked(target))
{
g_bHidePlayers[client][target] = false;
TransmitManager_SetEntityState(target, client, true); //client can see player again because the player is a zm
}
}
}
bool ClientIsAlive = IsPlayerAlive(client);
bool ClientIsHuman = false;
if (ClientIsAlive)
{
ClientIsHuman = ZR_IsClientHuman(client);
}
bool ClientHasItem = EW_ClientHasItem(client);
//player is leader, has an item, is not alive or is not human, set the transmit state once via the bool so all can see the player.
if (g_iLeader == client || ClientHasItem || !ClientIsAlive || !ClientIsHuman)
{
if (!g_bHasTobeVisible[client] && TransmitManager_IsEntityHooked(client))
{
for (int target = 1; target <= MaxClients; target++)
{
if (IsValidClient(target) && client != target && TransmitManager_IsEntityHooked(target))
{
//all must see the leader, the zombie and the item user.
TransmitManager_SetEntityState(client, target, true); //all can see the player again
//if the player is dead or not human they must see all players again
if (!ClientIsAlive || !ClientIsHuman)
{
TransmitManager_SetEntityState(target, client, true); //client can see all players again
}
}
}
g_bHasTobeVisible[client] = true;
}
//but the human leader and human item users may still want to hide the remaining human players.
if (ClientIsAlive && ClientIsHuman && (g_iLeader == client || ClientHasItem))
{
DoTheTargetCheck(client);
}
continue;
}
g_bHasTobeVisible[client] = false;
//all the remaining checks for clients with hide enabled that are alive humans and not leaders or item users.
DoTheTargetCheck(client);
//process a tenth of the slots per tick; combined with the 0.1s interval every client is
//refreshed about once a second, like the old 1.0s full scan, without the frame spike.
int iSlice = (MaxClients + 9) / 10;
for (int n = 0; n < iSlice; n++)
{
int client = g_iNextClient;
g_iNextClient = (g_iNextClient % MaxClients) + 1;
ProcessHideClient(client);
}
return Plugin_Continue;
}
void ProcessHideClient(int client)
{
if (!g_bInGame[client] || g_bFake[client])
return;
if (g_iHideRange[client] == DISABLED) //client just disabled hide right now. the bool will pass once until they enabled hide again.
{
if (g_bHasSomebodyToHide[client] && g_bHooked[client])
{
for (int target = 1; target <= MaxClients; target++)
{
if (client != target && g_bInGame[target] && g_bHooked[target])
{
TransmitManager_SetEntityState(target, client, true); //client can see all players again
g_bHidePlayers[client][target] = false;
}
}
g_bHasSomebodyToHide[client] = false;
}
return;
}
//lazy fix for invisible zm
if (g_bHooked[client])
{
for (int target = 1; target <= MaxClients; target++)
{
if (g_bHidePlayers[client][target] && client != target && g_bInGame[target]
&& g_bZombie[target] && g_bHooked[target])
{
g_bHidePlayers[client][target] = false;
TransmitManager_SetEntityState(target, client, true); //client can see player again because the player is a zm
}
}
}
bool ClientIsAlive = g_bAlive[client];
bool ClientIsHuman = g_bHuman[client];
bool ClientIsLeader = g_bLeader[client];
bool ClientHasItem = g_bHasItem[client];
//player is leader, has an item, is not alive or is not human, set the transmit state once via the bool so all can see the player.
if (ClientIsLeader || ClientHasItem || !ClientIsAlive || !ClientIsHuman)
{
if (!g_bHasTobeVisible[client] && g_bHooked[client])
{
for (int target = 1; target <= MaxClients; target++)
{
if (client != target && g_bInGame[target] && g_bHooked[target])
{
//all must see the leader, the zombie and the item user.
TransmitManager_SetEntityState(client, target, true); //all can see the player again
//if the player is dead or not human they must see all players again
if (!ClientIsAlive || !ClientIsHuman)
{
TransmitManager_SetEntityState(target, client, true); //client can see all players again
}
}
}
g_bHasTobeVisible[client] = true;
}
//but the human leader and human item users may still want to hide the remaining human players.
if (ClientIsAlive && ClientIsHuman && (ClientIsLeader || ClientHasItem))
{
DoTheTargetCheck(client);
}
return;
}
g_bHasTobeVisible[client] = false;
//all the remaining checks for clients with hide enabled that are alive humans and not leaders or item users.
DoTheTargetCheck(client);
}
public void DoTheTargetCheck(int client)
{
float fOriginClient[3];
float fOriginTarget[3];
GetClientAbsOrigin(client, fOriginClient);
g_bHasSomebodyToHide[client] = false;
float fRangeSquared = float(g_iHideRange[client]);
float fClientX = g_fOrigin[client][0];
float fClientY = g_fOrigin[client][1];
float fClientZ = g_fOrigin[client][2];
bool bClientHooked = g_bHooked[client];
for (int target = 1; target <= MaxClients; target++)
{
if (!IsValidClient(target) || IsClientSourceTV(target))
if (target == client || !g_bTargetEligible[target])
{
continue;
}
//the target is not the leader, has no item, is alive and is a human. so consider him for the distance check.
if (g_iLeader != target && !EW_ClientHasItem(target) && IsPlayerAlive(target) && ZR_IsClientHuman(target) && target != client)
float fDeltaX = g_fOrigin[target][0] - fClientX;
float fDeltaY = g_fOrigin[target][1] - fClientY;
float fDeltaZ = g_fOrigin[target][2] - fClientZ;
if (fDeltaX * fDeltaX + fDeltaY * fDeltaY + fDeltaZ * fDeltaZ <= fRangeSquared)
{
GetClientAbsOrigin(target, fOriginTarget);
if (GetVectorDistance(fOriginTarget, fOriginClient, true) <= float(g_iHideRange[client]))
//we only have to check the hook when the state of the hidePlayers boolean changed
if (!g_bHidePlayers[client][target] && bClientHooked && g_bHooked[target])
{
//we only have to check the hook when the state of the hidePlayers boolean changed
if (!g_bHidePlayers[client][target] && TransmitManager_IsEntityHooked(client) && TransmitManager_IsEntityHooked(target))
{
//the client here cannot be a fakeclient. thats already checked.
TransmitManager_SetEntityState(target, client, false); //the player is hiding the teammate due to distance
g_bHidePlayers[client][target] = true;
//LogMessage("client: %N is %s. target: %N is %s", client, ZR_IsClientHuman(client) ? "Human" : "Zombie", target, ZR_IsClientHuman(target) ? "Human" : "Zombie");
}
g_bHasSomebodyToHide[client] = true; //theres at least one target to hide for the player, the teammate
}
else
{
//we only have to check the hook when the state of the hidePlayers boolean changed
if (g_bHidePlayers[client][target] && TransmitManager_IsEntityHooked(client) && TransmitManager_IsEntityHooked(target))
{
TransmitManager_SetEntityState(target, client, true); //the player is seeing the teammate due to distance
g_bHidePlayers[client][target] = false;
}
//the client here cannot be a fakeclient. thats already checked.
TransmitManager_SetEntityState(target, client, false); //the player is hiding the teammate due to distance
g_bHidePlayers[client][target] = true;
}
g_bHasSomebodyToHide[client] = true; //theres at least one target to hide for the player, the teammate
}
else if (g_bHidePlayers[client][target] && bClientHooked && g_bHooked[target])
{
TransmitManager_SetEntityState(target, client, true); //the player is seeing the teammate due to distance
g_bHidePlayers[client][target] = false;
}
}
}
@@ -393,10 +450,13 @@ stock int FindTierForCommandOverride(const char[] command)
//----------------------------------------------------------------------------------------------------
public void MenuHandler_CookieMenu(int client, CookieMenuAction action, any info, char[] buffer, int maxlen)
{
int client_tier = GetPlayerTier_native(client);
if (g_iTierHide > client_tier)
if (GetFeatureStatus(FeatureType_Native, "GetPlayerTier_native") == FeatureStatus_Available)
{
return;
int client_tier = GetPlayerTier_native(client);
if (g_iTierHide > client_tier)
{
return;
}
}
switch(action)
{
@@ -452,7 +512,6 @@ public void OnClientCookiesCached(int client)
stock bool IsValidClient(int client)
{
if (client > 0 && client <= MaxClients && IsClientConnected(client) && IsClientInGame(client))
return true;
return false;
//IsClientInGame implies IsClientConnected
return (client > 0 && client <= MaxClients && IsClientInGame(client));
}
@@ -342,6 +342,7 @@ public void OnPluginStart()
RegAdminCmd("sm_mapvote", Command_Mapvote, ADMFLAG_CHANGEMAP, "sm_mapvote - Forces MapChooser to attempt to run a map vote now.");
RegAdminCmd("sm_setnextmap", Command_SetNextmap, ADMFLAG_CHANGEMAP, "sm_setnextmap <map>");
RegAdminCmd("sm_unsetnextmap", Command_UnsetNextmap, ADMFLAG_CHANGEMAP, "sm_unsetnextmap - Unsets an admin-set next map and re-enables the end of map vote.");
// Mapchooser Extended Commands
RegAdminCmd("mce_reload_maplist", Command_ReloadMaps, ADMFLAG_CHANGEMAP, "mce_reload_maplist - Reload the Official Maplist file.");
@@ -885,6 +886,23 @@ public Action Command_SetNextmap(int client, int args)
return Plugin_Handled;
}
public Action Command_UnsetNextmap(int client, int args)
{
if(g_iNextmapState != 2)
{
CReplyToCommand(client, "[MCE] No admin-set next map to unset.");
return Plugin_Handled;
}
g_iNextmapState = 0;
g_MapVoteCompleted = false;
FindConVar("sm_nextmap").SetString("");
ShowActivity(client, "unset the next map");
LogAction(client, -1, "\"%L\" unset the nextmap", client);
return Plugin_Handled;
}
public Action Command_ReloadMaps(int client, int args)
{
InitializeOfficialMapList();