Compare commits

..
6 Commits
Author SHA1 Message Date
marisa ac3ffd5be2 lots of changes 2026-10-22 16:17:56 +01:00
Irineu e4a75367aa New plugin ScoreboardUnlocker: Increases the frags limit in the scoreboard 2026-10-20 14:51:54 -03:00
jenz 71d14c492e moving fakes from team back to spec if too many 2026-10-16 14:42:41 +02:00
marisa 373e8ba1f2 Cleanup 2026-10-14 16:53:45 +01:00
marisa 16b1e11bc4 Normalise push scale to server's value 2026-10-14 16:53:45 +01:00
marisa fb971adf6d Couch changes 2026-10-14 16:53:45 +01:00
60 changed files with 9193 additions and 291 deletions
+563 -28
View File
@@ -8,6 +8,11 @@
#define COUCH_CORE
#include <Couch>
// Optional: without Zombie:Reloaded nobody counts as a zombie, so couch HP never changes.
#undef REQUIRE_PLUGIN
#include <zombiereloaded>
#define REQUIRE_PLUGIN
#pragma semicolon 1
#pragma newdecls required
@@ -28,8 +33,8 @@
#define COUCH_TEXT_HOLD "8.0"
#define COUCH_TEXT_COLOUR "255 144 32"
// Beacon timer interval, ticks per colour, ticks per light pulse.
#define COUCH_BEACON_INTERVAL 0.1
// Effect timer interval, ticks per colour, ticks per light pulse.
#define COUCH_EFFECT_INTERVAL 0.1
#define COUCH_COLOUR_TICKS 4
#define COUCH_PULSE_TICKS 10
@@ -38,6 +43,17 @@
#define COUCH_LIGHT_RADIUS_MIN 150.0
#define COUCH_LIGHT_RADIUS_MAX 320.0
// Beacon, after sm_beacon: two beam rings a second from just above the couch's origin. No sound.
#define COUCH_BEACON_INTERVAL 1.0
#define COUCH_BEACON_HEIGHT 10.0
#define COUCH_BEACON_SPRITE_BEAM "sprites/laser.vmt"
#define COUCH_BEACON_SPRITE_HALO "sprites/halo01.vmt"
// Outer ring is sm_beacon's grey; the inner ring is the couch's own orange, or the effect's
// current colour while the effect is on.
static const int g_iBeaconOuterColour[4] = { 128, 128, 128, 255 };
static const int g_iBeaconInnerColour[4] = { 255, 144, 32, 255 };
static const int g_iCouchColours[][3] =
{
{ 255, 48, 48 }, // red
@@ -49,6 +65,39 @@ static const int g_iCouchColours[][3] =
{ 255, 80, 224 }, // magenta
};
// Couch HP: regen is checked this often while the couch is below full.
#define COUCH_REGEN_INTERVAL 1.0
// Stock sounds, so they only need precaching. A zombie hit plays one of the first set at the couch;
// the break plays one of each of the other two, the explosion for everyone regardless of distance.
static const char g_sHitSounds[][] =
{
"physics/wood/wood_furniture_break1.wav",
"physics/wood/wood_furniture_break2.wav",
"physics/wood/wood_box_break1.wav",
"physics/wood/wood_box_break2.wav",
"physics/wood/wood_plank_break1.wav",
"physics/wood/wood_plank_break2.wav",
"physics/wood/wood_plank_break3.wav",
"physics/wood/wood_plank_break4.wav",
};
static const char g_sBreakSounds[][] =
{
"physics/wood/wood_crate_break1.wav",
"physics/wood/wood_crate_break2.wav",
"physics/wood/wood_crate_break3.wav",
"physics/wood/wood_crate_break4.wav",
"physics/wood/wood_crate_break5.wav",
};
static const char g_sExplodeSounds[][] =
{
"weapons/hegrenade/explode3.wav",
"weapons/hegrenade/explode4.wav",
"weapons/hegrenade/explode5.wav",
};
// Allowed to move the couch in pistol-only mode, along with the knife.
static const char g_sPistols[][] =
{
@@ -62,10 +111,15 @@ static const char g_sPistols[][] =
ConVar g_cvModel;
ConVar g_cvPushScale;
// The engine's own phys_pushscale. Maps change it through point_servercommand, and the engine has
// already multiplied it into the damage force by the time OnTakeDamage runs, so it is divided out.
ConVar g_cvPhysPushScale;
ConVar g_cvPistolOnly;
ConVar g_cvAllowUse;
ConVar g_cvEffect;
ConVar g_cvEffectIntensity;
ConVar g_cvBeacon;
ConVar g_cvBeaconRadius;
ConVar g_cvIgnoreClip;
ConVar g_cvCollisionGroup;
@@ -74,11 +128,26 @@ ConVar g_cvCollisionGroup;
ConVar g_cvMoveTier;
ConVar g_cvButtonTier;
// Couch HP. Only zombie hits lower it; it is kept here rather than in the entity's health so the
// engine never breaks the prop itself.
ConVar g_cvHp;
ConVar g_cvHpMax;
ConVar g_cvHpRegenDelay;
ConVar g_cvHpRegen;
GlobalForward g_fwdOnCouchSpawned;
GlobalForward g_fwdOnCouchRemoved;
GlobalForward g_fwdOnCouchDamaged;
GlobalForward g_fwdOnCouchInteraction;
GlobalForward g_fwdOnClientBlocked;
GlobalForward g_fwdOnCouchHpChanged;
GlobalForward g_fwdOnCouchBroken;
int g_iHp;
float g_fLastZombieHit; // GetGameTime() of the last hit that lowered the HP; regen waits on it.
Handle g_hRegenTimer;
bool g_bBroken; // Set by the breaking hit so the rest of its pellets, and anything else until the next spawn, is ignored.
bool g_bZRLoaded;
int g_iCouchRef = INVALID_ENT_REFERENCE;
int g_iLightRef = INVALID_ENT_REFERENCE;
@@ -88,8 +157,19 @@ bool g_bPlaced;
float g_vecPlaceOrigin[3];
float g_vecPlaceAngles[3];
Handle g_hEffectTimer;
int g_iEffectTick;
// Colour the effect last painted the couch; the beacon's inner ring follows it.
int g_iEffectColour[4] = { 255, 255, 255, 255 };
Handle g_hBeaconTimer;
int g_iBeaconTick;
int g_iBeaconBeamSprite = -1;
int g_iBeaconHaloSprite = -1;
// Accepted hits this round, announced at round end. Shotguns count once per pellet.
int g_iRoundHits;
float g_fRoundDamage;
Handle g_hForceVPhysicsCollide;
@@ -127,12 +207,20 @@ public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max
CreateNative("Couch_Remove", Native_Remove);
CreateNative("Couch_IsEffectEnabled", Native_IsEffectEnabled);
CreateNative("Couch_SetEffectEnabled", Native_SetEffectEnabled);
CreateNative("Couch_IsBeaconEnabled", Native_IsBeaconEnabled);
CreateNative("Couch_SetBeaconEnabled", Native_SetBeaconEnabled);
CreateNative("Couch_IsPistolOnly", Native_IsPistolOnly);
CreateNative("Couch_SetPistolOnly", Native_SetPistolOnly);
CreateNative("Couch_IsUseAllowed", Native_IsUseAllowed);
CreateNative("Couch_SetUseAllowed", Native_SetUseAllowed);
CreateNative("Couch_SetClientBlocked", Native_SetClientBlocked);
CreateNative("Couch_IsClientBlocked", Native_IsClientBlocked);
CreateNative("Couch_IsHpEnabled", Native_IsHpEnabled);
CreateNative("Couch_SetHpEnabled", Native_SetHpEnabled);
CreateNative("Couch_GetHp", Native_GetHp);
CreateNative("Couch_SetHp", Native_SetHp);
CreateNative("Couch_GetMaxHp", Native_GetMaxHp);
CreateNative("Couch_SetMaxHp", Native_SetMaxHp);
RegPluginLibrary("Couch");
MarkNativeAsOptional("GetPlayerTier_native");
@@ -147,25 +235,38 @@ public void OnPluginStart()
g_fwdOnCouchDamaged = new GlobalForward("Couch_OnCouchDamaged", ET_Ignore, Param_Cell, Param_Cell, Param_Float);
g_fwdOnCouchInteraction = new GlobalForward("Couch_OnCouchInteraction", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_String);
g_fwdOnClientBlocked = new GlobalForward("Couch_OnClientBlocked", ET_Ignore, Param_Cell, Param_Cell);
g_fwdOnCouchHpChanged = new GlobalForward("Couch_OnCouchHpChanged", ET_Ignore, Param_Cell, Param_Cell, Param_Cell);
g_fwdOnCouchBroken = new GlobalForward("Couch_OnCouchBroken", ET_Ignore, Param_Cell, Param_Cell);
g_cvModel = CreateConVar("sm_couch_model", COUCH_DEFAULT_MODEL, "Model used for the couch. Applies to the next spawn.", FCVAR_NOTIFY);
g_cvPushScale = CreateConVar("sm_couch_push_scale", "6.0", "Multiplier on the force bullets apply to the couch. 1.0 is the engine's own push.", FCVAR_NOTIFY, true, 0.0, true, 100.0);
g_cvPushScale = CreateConVar("sm_couch_push_scale", "6.0", "Multiplier on the force bullets apply to the couch. 1.0 is the engine's own push at phys_pushscale 1; maps changing phys_pushscale don't change the couch.", FCVAR_NOTIFY, true, 0.0, true, 100.0);
g_cvPhysPushScale = FindConVar("phys_pushscale");
g_cvPistolOnly = CreateConVar("sm_couch_pistol_only", "0", "Only pistols and knives move the couch; other guns and grenades are ignored.", FCVAR_NOTIFY, true, 0.0, true, 1.0);
g_cvAllowUse = CreateConVar("sm_allow_couch_use", "1", "Players may push the couch by holding +use on it. 0 means it can only be moved by shooting it.", FCVAR_NOTIFY, true, 0.0, true, 1.0);
g_cvEffect = CreateConVar("sm_couch_effect", "1", "Colour cycle and pulsing light on the couch.", FCVAR_NOTIFY, true, 0.0, true, 1.0);
g_cvEffectIntensity = CreateConVar("sm_couch_effect_intensity", "2", "Brightness of the couch effect light. 0 is off, 2 is the default, 8 is very bright. Applies immediately.", FCVAR_NOTIFY, true, 0.0, true, 10.0);
g_cvBeacon = CreateConVar("sm_couch_beacon", "0", "Beacon on the couch, like sm_beacon: expanding rings every second.", FCVAR_NOTIFY, true, 0.0, true, 1.0);
g_cvBeaconRadius = CreateConVar("sm_couch_beacon_radius", "375", "Radius of the couch beacon's rings.", FCVAR_NOTIFY, true, 50.0, true, 1500.0);
g_cvIgnoreClip = CreateConVar("sm_couch_ignore_clip", "1", "Let the couch pass through func_clip_vphysics brushes.", FCVAR_NOTIFY, true, 0.0, true, 1.0);
g_cvMoveTier = CreateConVar("sm_couch_move_tier", "0", "Minimum playtime tier needed to shoot or +use the couch.", FCVAR_NOTIFY, true, 0.0);
g_cvButtonTier = CreateConVar("sm_couch_button_tier", "0", "Minimum playtime tier needed to press or shoot map buttons (func_button).", FCVAR_NOTIFY, true, 0.0);
g_cvCollisionGroup = CreateConVar("sm_couch_collision_group", "5", "Collision group given to the couch at spawn. 5 (player), 0 (prop)", FCVAR_NOTIFY, true, 0.0, true, 20.0);
g_cvHp = CreateConVar("sm_couch_hp", "1", "Couch HP: zombie hits wear the couch down and at 0 HP it breaks, killing every human.", FCVAR_NOTIFY, true, 0.0, true, 1.0);
g_cvHpMax = CreateConVar("sm_couch_hp_max", "4000", "Maximum couch HP. The couch spawns with this much every round.", FCVAR_NOTIFY, true, 1.0);
g_cvHpRegenDelay = CreateConVar("sm_couch_hp_regen_delay", "10","Seconds without a zombie hit before the couch starts regenerating HP.", FCVAR_NOTIFY, true, 0.0);
g_cvHpRegen = CreateConVar("sm_couch_hp_regen", "25", "HP the couch regenerates per second once the regen delay has passed.", FCVAR_NOTIFY, true, 0.0);
g_cvModel.AddChangeHook(OnModelChanged);
g_cvEffect.AddChangeHook(OnEffectChanged);
g_cvEffectIntensity.AddChangeHook(OnEffectIntensityChanged);
g_cvBeacon.AddChangeHook(OnBeaconChanged);
g_cvHp.AddChangeHook(OnHpEnabledChanged);
g_cvHpMax.AddChangeHook(OnHpMaxChanged);
AutoExecConfig(true, "Couch");
HookEvent("round_start", Event_RoundStart, EventHookMode_Post);
HookEvent("round_end", Event_RoundEnd, EventHookMode_Post);
g_cvTurboPhysics = FindConVar("sv_turbophysics");
@@ -196,16 +297,42 @@ public void OnPluginEnd()
KillCouch();
}
public void OnAllPluginsLoaded()
{
g_bZRLoaded = LibraryExists("zombiereloaded");
}
public void OnLibraryAdded(const char[] name)
{
if (StrEqual(name, "zombiereloaded"))
g_bZRLoaded = true;
}
public void OnLibraryRemoved(const char[] name)
{
if (StrEqual(name, "zombiereloaded"))
g_bZRLoaded = false;
}
public void OnMapStart()
{
PrecacheCouchModel();
PrecacheBeacon();
PrecacheHpSounds();
// Entities and no-mapchange timers went with the map.
g_iCouchRef = INVALID_ENT_REFERENCE;
g_iLightRef = INVALID_ENT_REFERENCE;
g_bPlaced = false;
g_hEffectTimer = null;
g_iEffectTick = 0;
g_hBeaconTimer = null;
g_iBeaconTick = 0;
g_iRoundHits = 0;
g_fRoundDamage = 0.0;
g_hRegenTimer = null;
g_iHp = 0;
g_fLastZombieHit = 0.0;
g_bBroken = false;
ResetBlockedClients();
}
@@ -228,7 +355,9 @@ public void OnEntityDestroyed(int entity)
// until the next round start.
g_iCouchRef = INVALID_ENT_REFERENCE;
KillLight();
StopEffect();
StopBeacon();
StopRegen();
Call_StartForward(g_fwdOnCouchRemoved);
Call_Finish();
@@ -250,6 +379,16 @@ public void OnClientDisconnect(int client)
public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
g_iRoundHits = 0;
g_fRoundDamage = 0.0;
// A couch that survived the restart starts the round at full HP too.
if (GetCouchIndex() != -1)
{
ResetHp();
UpdateRegen();
}
if (!g_bPlaced || GetCouchIndex() != -1)
return;
@@ -260,13 +399,33 @@ public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
CreateTimer(COUCH_ANNOUNCE_DELAY, Timer_AnnounceCouchRound, _, TIMER_FLAG_NO_MAPCHANGE);
}
// Only for rounds that had a couch: placed, or spawned mid-round and shot.
public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
{
if (!g_bPlaced && g_iRoundHits == 0)
return;
if (g_iRoundHits == 0)
{
PrintToChatAll(PREFIX ... "\x01The couch took \x04no damage\x01 this round.");
return;
}
char sDamage[16];
char sHits[16];
FormatThousands(RoundToNearest(g_fRoundDamage), sDamage, sizeof(sDamage));
FormatThousands(g_iRoundHits, sHits, sizeof(sHits));
PrintToChatAll(PREFIX ... "\x01The couch took \x04%s damage\x01 from \x04%s hit%s\x01 this round.", sDamage, sHits, (g_iRoundHits == 1) ? "" : "s");
}
public Action Timer_AnnounceCouchRound(Handle timer)
{
if (GetCouchIndex() == -1)
return Plugin_Stop;
ShowCouchRoundText();
PrintToChatAll("\x01[SM] \x04Couch round!\x01 Humans must bring the couch with them throughout the map or they fail the round.");
PrintToChatAll(PREFIX ... "Couch round!\x01 Humans must bring the couch with them throughout the map or they fail the round.");
return Plugin_Stop;
}
@@ -450,12 +609,14 @@ int SpawnCouchAt(const float vecOrigin[3], const float vecAngles[3])
// SDKHooks drops the hooks with the entity.
SDKHook(couch, SDKHook_OnTakeDamage, OnCouchTakeDamage);
// Takes the beacon's colour modulation at full opacity.
// Takes the effect's colour modulation at full opacity.
SetEntityRenderMode(couch, RENDER_NORMAL);
g_iCouchRef = EntIndexToEntRef(couch);
ResetHp();
ApplyEffectState();
ApplyBeaconState();
Call_StartForward(g_fwdOnCouchSpawned);
Call_PushCell(couch);
@@ -471,7 +632,9 @@ bool KillCouch()
// Cleared first so OnEntityDestroyed doesn't run the external-removal path too.
g_iCouchRef = INVALID_ENT_REFERENCE;
StopEffect();
StopBeacon();
StopRegen();
KillLight();
if (couch == -1)
@@ -494,15 +657,44 @@ public Action OnCouchTakeDamage(int victim, int &attacker, int &inflictor, float
{
bool bFromClient = (attacker >= 1 && attacker <= MaxClients && IsClientInGame(attacker));
// With couch HP on, a zombie's hit always costs HP: couch bans, the tier and pistol-only mode don't
// stop it. A banned zombie's hit still counts and is reported, but is refused at the end so it
// doesn't move the couch.
bool bRefusePush = false;
if (bFromClient && g_cvHp.BoolValue && IsZombie(attacker))
{
if (g_bBroken)
return Plugin_Handled;
if (ApplyZombieHit(victim, attacker, damage))
return Plugin_Handled;
bRefusePush = g_bBlocked[attacker];
}
else
{
if (bFromClient && IsClientRefused(victim, attacker))
return Plugin_Handled;
if (bFromClient && g_cvPistolOnly.BoolValue && !IsPistolOrKnifeHit(attacker, weapon, damagetype))
return Plugin_Handled;
}
// Counted only once the hit is accepted, so refused shots don't show in the round total.
g_iRoundHits++;
g_fRoundDamage += damage;
Action result = Plugin_Continue;
// damageForce already carries phys_pushscale (CalculateBulletDamageForce and the melee and
// explosive variants all multiply it in), so divide it back out. At 0 the force is already zero
// and there is nothing to undo.
float fScale = g_cvPushScale.FloatValue;
float fEnginePush = (g_cvPhysPushScale != null) ? g_cvPhysPushScale.FloatValue : 1.0;
if (fEnginePush > 0.0)
fScale /= fEnginePush;
if (fScale != 1.0)
{
ScaleVector(damageForce, fScale);
@@ -520,7 +712,215 @@ public Action OnCouchTakeDamage(int victim, int &attacker, int &inflictor, float
ReportHit(victim, attacker, inflictor, weapon, damagetype);
}
return result;
return bRefusePush ? Plugin_Handled : result;
}
// ---------------------------------------------------------------------------------------------
// Couch HP
// ---------------------------------------------------------------------------------------------
bool IsZombie(int client)
{
return g_bZRLoaded && ZR_IsClientZombie(client);
}
void PrecacheHpSounds()
{
for (int i = 0; i < sizeof(g_sHitSounds); i++)
PrecacheSound(g_sHitSounds[i], true);
for (int i = 0; i < sizeof(g_sBreakSounds); i++)
PrecacheSound(g_sBreakSounds[i], true);
for (int i = 0; i < sizeof(g_sExplodeSounds); i++)
PrecacheSound(g_sExplodeSounds[i], true);
}
public void OnHpEnabledChanged(ConVar convar, const char[] oldValue, const char[] newValue)
{
if (convar.BoolValue)
{
ResetHp();
UpdateRegen();
return;
}
// Announced so the HUD drops its HP line.
StopRegen();
FireHpChanged();
}
// A full couch stays full; anything else is clamped to the new maximum.
public void OnHpMaxChanged(ConVar convar, const char[] oldValue, const char[] newValue)
{
int iOldMax = StringToInt(oldValue);
int iNewMax = convar.IntValue;
if (g_iHp >= iOldMax || g_iHp > iNewMax)
g_iHp = iNewMax;
FireHpChanged();
UpdateRegen();
}
// Full HP; on spawn, round start and when the feature is switched on.
void ResetHp()
{
g_iHp = g_cvHpMax.IntValue;
g_bBroken = false;
g_fLastZombieHit = 0.0;
FireHpChanged();
}
// Clamped to 0..max. Doesn't touch the regen timer; callers decide that.
void SetHp(int hp)
{
int iMax = g_cvHpMax.IntValue;
if (hp < 0)
hp = 0;
else if (hp > iMax)
hp = iMax;
if (hp == g_iHp)
return;
g_iHp = hp;
FireHpChanged();
}
void FireHpChanged()
{
Call_StartForward(g_fwdOnCouchHpChanged);
Call_PushCell(GetCouchIndex());
Call_PushCell(g_iHp);
Call_PushCell(g_cvHpMax.IntValue);
Call_Finish();
}
// Brings the regen timer in line with the state: it runs only while a live couch is below full HP.
// Not called from inside Timer_Regen; that returns Plugin_Stop instead.
void UpdateRegen()
{
if (!g_cvHp.BoolValue || g_bBroken || GetCouchIndex() == -1 || g_iHp >= g_cvHpMax.IntValue)
{
StopRegen();
return;
}
StartRegen();
}
void StartRegen()
{
if (g_hRegenTimer == null)
g_hRegenTimer = CreateTimer(COUCH_REGEN_INTERVAL, Timer_Regen, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);
}
void StopRegen()
{
delete g_hRegenTimer;
}
public Action Timer_Regen(Handle timer)
{
int iMax = g_cvHpMax.IntValue;
if (!g_cvHp.BoolValue || g_bBroken || GetCouchIndex() == -1 || g_iHp >= iMax)
{
g_hRegenTimer = null;
return Plugin_Stop;
}
// Waiting out the delay since the last hit; a new hit pushes it back.
if (GetGameTime() - g_fLastZombieHit < g_cvHpRegenDelay.FloatValue)
return Plugin_Continue;
SetHp(g_iHp + g_cvHpRegen.IntValue);
if (g_iHp >= iMax)
{
g_hRegenTimer = null;
return Plugin_Stop;
}
return Plugin_Continue;
}
// Takes the hit off the couch's HP and plays a break sound at it. Returns true if this hit broke the couch.
bool ApplyZombieHit(int couch, int attacker, float damage)
{
// Every hit costs at least 1 HP, whatever the knife does to props.
int iDamage = RoundToNearest(damage);
if (iDamage < 1)
iDamage = 1;
g_fLastZombieHit = GetGameTime();
SetHp(g_iHp - iDamage);
EmitSoundToAll(g_sHitSounds[GetRandomInt(0, sizeof(g_sHitSounds) - 1)], couch, SNDCHAN_STATIC, SNDLEVEL_NORMAL);
if (g_iHp > 0)
{
UpdateRegen();
return false;
}
BreakCouch(couch, attacker);
return true;
}
// 0 HP: the couch breaks loudly, every human dies and the couch is removed; it comes back at full HP
// at the next round start if it was placed. attacker is 0 for an admin set.
void BreakCouch(int couch, int attacker)
{
g_bBroken = true;
StopRegen();
// The explosion is heard by everyone at full volume; the splintering is placed at the couch.
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);
// 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);
Call_PushCell(couch);
Call_PushCell(attacker);
Call_Finish();
if (g_bZRLoaded)
{
for (int i = 1; i <= MaxClients; i++)
{
if (IsClientInGame(i) && IsPlayerAlive(i) && ZR_IsClientHuman(i))
KillHuman(i);
}
}
// Last: fires Couch_OnCouchRemoved. The Kill input is deferred, so this is safe from inside the
// couch's own damage hook; g_bBroken swallows the rest of that hit's pellets.
KillCouch();
}
// Kills a human when the couch breaks. Lethal world damage is an ordinary death: it runs the damage
// path, fires player_death and leaves a ragdoll, where CommitSuicide can be refused or skip the
// ragdoll. The damage is well over health plus kevlar so armour cannot save them; suicide is the
// fallback if another plugin swallowed the damage.
void KillHuman(int client)
{
float fDamage = float(GetClientHealth(client) + GetClientArmor(client)) * 2.0 + 1000.0;
SDKHooks_TakeDamage(client, 0, 0, fDamage, DMG_GENERIC);
if (IsPlayerAlive(client))
ForcePlayerSuicide(client);
}
// Reach of the engine's +use push.
@@ -934,14 +1334,14 @@ void ApplyLightIntensity()
AcceptEntityInput(light, "brightness");
}
// Brings the light and beacon in line with sm_couch_effect. Safe to call at any time.
// Brings the light and colour cycle in line with sm_couch_effect. Safe to call at any time.
void ApplyEffectState()
{
int couch = GetCouchIndex();
if (couch == -1 || !g_cvEffect.BoolValue)
{
StopBeacon();
StopEffect();
KillLight();
if (couch != -1)
@@ -953,7 +1353,7 @@ void ApplyEffectState()
if (GetLightIndex() == -1)
g_iLightRef = SpawnCouchLight(couch);
StartBeacon();
StartEffect();
}
// light_dynamic parented to the couch. Returns INVALID_ENT_REFERENCE on failure.
@@ -1002,6 +1402,84 @@ void KillLight()
AcceptEntityInput(light, "Kill");
}
void StartEffect()
{
if (g_hEffectTimer == null)
g_hEffectTimer = CreateTimer(COUCH_EFFECT_INTERVAL, Timer_Effect, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);
}
// Not called from inside Timer_Effect; that returns Plugin_Stop instead.
void StopEffect()
{
delete g_hEffectTimer;
}
public Action Timer_Effect(Handle timer)
{
int couch = GetCouchIndex();
if (couch == -1)
{
g_hEffectTimer = null;
return Plugin_Stop;
}
g_iEffectTick++;
int colour = (g_iEffectTick / COUCH_COLOUR_TICKS) % sizeof(g_iCouchColours);
int r = g_iCouchColours[colour][0];
int g = g_iCouchColours[colour][1];
int b = g_iCouchColours[colour][2];
SetEntityRenderColor(couch, r, g, b, 255);
g_iEffectColour[0] = r;
g_iEffectColour[1] = g;
g_iEffectColour[2] = b;
int light = GetLightIndex();
if (light == -1)
return Plugin_Continue;
SetEntityRenderColor(light, r, g, b, 255);
// One sine period per pulse cycle, mapped onto the radius range.
float fPhase = float(g_iEffectTick % COUCH_PULSE_TICKS) / float(COUCH_PULSE_TICKS);
float fPulse = (Sine(fPhase * 2.0 * FLOAT_PI) + 1.0) / 2.0;
SetVariantFloat(COUCH_LIGHT_RADIUS_MIN + fPulse * (COUCH_LIGHT_RADIUS_MAX - COUCH_LIGHT_RADIUS_MIN));
AcceptEntityInput(light, "distance");
return Plugin_Continue;
}
// ---------------------------------------------------------------------------------------------
// Beacon
// ---------------------------------------------------------------------------------------------
public void OnBeaconChanged(ConVar convar, const char[] oldValue, const char[] newValue)
{
ApplyBeaconState();
}
// The sprites are stock CS:S content, the same ones sm_beacon uses.
void PrecacheBeacon()
{
g_iBeaconBeamSprite = PrecacheModel(COUCH_BEACON_SPRITE_BEAM, true);
g_iBeaconHaloSprite = PrecacheModel(COUCH_BEACON_SPRITE_HALO, true);
}
// Brings the beacon in line with sm_couch_beacon. Safe to call at any time.
void ApplyBeaconState()
{
if (GetCouchIndex() == -1 || !g_cvBeacon.BoolValue)
{
StopBeacon();
return;
}
StartBeacon();
}
void StartBeacon()
{
if (g_hBeaconTimer == null)
@@ -1014,6 +1492,7 @@ void StopBeacon()
delete g_hBeaconTimer;
}
// Same rings as sm_beacon's Timer_Beacon, from the couch instead of a player, without its blip.
public Action Timer_Beacon(Handle timer)
{
int couch = GetCouchIndex();
@@ -1023,27 +1502,26 @@ public Action Timer_Beacon(Handle timer)
return Plugin_Stop;
}
g_iBeaconTick++;
int colour = (g_iBeaconTick / COUCH_COLOUR_TICKS) % sizeof(g_iCouchColours);
int r = g_iCouchColours[colour][0];
int g = g_iCouchColours[colour][1];
int b = g_iCouchColours[colour][2];
SetEntityRenderColor(couch, r, g, b, 255);
int light = GetLightIndex();
if (light == -1)
if (g_iBeaconBeamSprite <= 0 || g_iBeaconHaloSprite <= 0)
return Plugin_Continue;
SetEntityRenderColor(light, r, g, b, 255);
float vecOrigin[3];
GetEntPropVector(couch, Prop_Send, "m_vecOrigin", vecOrigin);
vecOrigin[2] += COUCH_BEACON_HEIGHT;
// One sine period per pulse cycle, mapped onto the radius range.
float fPhase = float(g_iBeaconTick % COUCH_PULSE_TICKS) / float(COUCH_PULSE_TICKS);
float fPulse = (Sine(fPhase * 2.0 * FLOAT_PI) + 1.0) / 2.0;
int iInnerColour[4];
if (g_cvEffect.BoolValue)
iInnerColour = g_iEffectColour;
else
iInnerColour = g_iBeaconInnerColour;
SetVariantFloat(COUCH_LIGHT_RADIUS_MIN + fPulse * (COUCH_LIGHT_RADIUS_MAX - COUCH_LIGHT_RADIUS_MIN));
AcceptEntityInput(light, "distance");
float fRadius = g_cvBeaconRadius.FloatValue;
TE_SetupBeamRingPoint(vecOrigin, 10.0, fRadius, g_iBeaconBeamSprite, g_iBeaconHaloSprite, 0, 15, 0.5, 5.0, 0.0, g_iBeaconOuterColour, 10, 0);
TE_SendToAll();
TE_SetupBeamRingPoint(vecOrigin, 10.0, fRadius, g_iBeaconBeamSprite, g_iBeaconHaloSprite, 0, 10, 0.6, 10.0, 0.5, iInnerColour, 10, 0);
TE_SendToAll();
return Plugin_Continue;
}
@@ -1151,6 +1629,17 @@ public any Native_SetEffectEnabled(Handle plugin, int numParams)
return 0;
}
public any Native_IsBeaconEnabled(Handle plugin, int numParams)
{
return g_cvBeacon.BoolValue;
}
public any Native_SetBeaconEnabled(Handle plugin, int numParams)
{
g_cvBeacon.SetBool(view_as<bool>(GetNativeCell(1)));
return 0;
}
public any Native_IsPistolOnly(Handle plugin, int numParams)
{
return g_cvPistolOnly.BoolValue;
@@ -1191,3 +1680,49 @@ public any Native_IsClientBlocked(Handle plugin, int numParams)
return g_bBlocked[client];
}
public any Native_IsHpEnabled(Handle plugin, int numParams)
{
return g_cvHp.BoolValue;
}
public any Native_SetHpEnabled(Handle plugin, int numParams)
{
g_cvHp.SetBool(view_as<bool>(GetNativeCell(1)));
return 0;
}
public any Native_GetHp(Handle plugin, int numParams)
{
return (GetCouchIndex() == -1) ? 0 : g_iHp;
}
// 0 breaks the couch like a zombie hit would, which gives admins a way to test the break.
public any Native_SetHp(Handle plugin, int numParams)
{
SetHp(GetNativeCell(1));
int couch = GetCouchIndex();
if (g_iHp == 0 && g_cvHp.BoolValue && !g_bBroken && couch != -1)
BreakCouch(couch, 0);
else
UpdateRegen();
return 0;
}
public any Native_GetMaxHp(Handle plugin, int numParams)
{
return g_cvHpMax.IntValue;
}
// The cvar's change hook keeps a full couch full and clamps the rest.
public any Native_SetMaxHp(Handle plugin, int numParams)
{
int maxhp = GetNativeCell(1);
if (maxhp < 1)
maxhp = 1;
g_cvHpMax.SetInt(maxhp);
return 0;
}
+218 -19
View File
@@ -4,8 +4,6 @@
#pragma semicolon 1
#pragma newdecls required
#define PREFIX "\x01[SM] \x04"
// A player who keeps moving the couch the same way is reported at most once per this many seconds.
#define LOG_REPEAT_INTERVAL 2.0
@@ -15,7 +13,8 @@
// So commands fail with a message instead of an unbound-native error while the core is unloaded.
bool g_bCoreLoaded;
// Admins who asked to see couch interactions. Per slot, so it survives map changes.
// Who sees couch interactions in their console. On by default for anyone who can run sm_couchlog,
// off for everyone else. Per slot, so it survives map changes.
bool g_bLogging[MAXPLAYERS + 1];
// Per player: what was last reported, and when the same thing may be reported again.
@@ -39,8 +38,32 @@ public void OnPluginStart()
RegAdminCmd("sm_bringcouch", Command_BringCouch, ADMFLAG_CHANGEMAP, "Brings the couch to where you are aiming");
RegAdminCmd("sm_gotocouch", Command_GotoCouch, ADMFLAG_CHANGEMAP, "Teleports you to the couch");
RegAdminCmd("sm_removecouch", Command_RemoveCouch, ADMFLAG_CHANGEMAP, "Removes the couch");
RegAdminCmd("sm_couchbeacon", Command_Beacon, ADMFLAG_CHANGEMAP, "sm_couchbeacon [0|1] - beacon rings on the couch; no argument toggles");
RegAdminCmd("sm_couchpistolonly", Command_PistolOnly, ADMFLAG_CHANGEMAP, "sm_couchpistolonly [0|1] - only pistols and knives move the couch; no argument toggles");
RegAdminCmd("sm_couchlog", Command_Log, ADMFLAG_CHANGEMAP, "sm_couchlog [0|1] - show who moves the couch and how in your console; no argument toggles");
RegAdminCmd("sm_couchhp", Command_Hp, ADMFLAG_CHANGEMAP, "sm_couchhp [hp] - sets the couch's HP; no argument shows it");
RegAdminCmd("sm_couchmaxhp", Command_MaxHp, ADMFLAG_CHANGEMAP, "sm_couchmaxhp <hp> - sets the couch's maximum HP");
// Late load: admins are already in, so give them their default.
for (int client = 1; client <= MaxClients; client++)
{
if (IsClientInGame(client) && !IsFakeClient(client))
g_bLogging[client] = IsLoggingAdmin(client);
}
}
// Admin access is only known once the admin check has run, so the default is set here rather
// than on connect.
public void OnClientPostAdminCheck(int client)
{
if (!IsFakeClient(client))
g_bLogging[client] = IsLoggingAdmin(client);
}
// Follows the command's access, so an admin override on sm_couchlog changes who gets the default.
bool IsLoggingAdmin(int client)
{
return CheckCommandAccess(client, "sm_couchlog", ADMFLAG_CHANGEMAP);
}
// Game time restarts with the map.
@@ -123,6 +146,19 @@ public Action Command_RemoveCouch(int client, int args)
return Plugin_Handled;
}
public Action Command_Beacon(int client, int args)
{
if (!RequireCore(client))
return Plugin_Handled;
bool bEnable = !Couch_IsBeaconEnabled();
if (args >= 1)
bEnable = (GetCmdArgInt(1) != 0);
DoSetBeacon(client, bEnable);
return Plugin_Handled;
}
public Action Command_PistolOnly(int client, int args)
{
if (!RequireCore(client))
@@ -136,6 +172,52 @@ public Action Command_PistolOnly(int client, int args)
return Plugin_Handled;
}
public Action Command_Hp(int client, int args)
{
if (!RequireCore(client))
return Plugin_Handled;
if (Couch_GetCouch() == -1)
{
ReplyToCommand(client, PREFIX_PLAIN ... " There is no couch. Use sm_spawncouch first.");
return Plugin_Handled;
}
if (args < 1)
{
char sHp[16], sMax[16];
FormatThousands(Couch_GetHp(), sHp, sizeof(sHp));
FormatThousands(Couch_GetMaxHp(), sMax, sizeof(sMax));
ReplyToCommand(client, PREFIX_PLAIN ... " Couch HP: %s/%s (couch HP is %s).", sHp, sMax, Couch_IsHpEnabled() ? "on" : "off");
return Plugin_Handled;
}
DoSetHp(client, GetCmdArgInt(1));
return Plugin_Handled;
}
public Action Command_MaxHp(int client, int args)
{
if (!RequireCore(client))
return Plugin_Handled;
if (args < 1)
{
ReplyToCommand(client, PREFIX_PLAIN ... " Usage: sm_couchmaxhp <hp>");
return Plugin_Handled;
}
int hp = GetCmdArgInt(1);
if (hp < 1)
{
ReplyToCommand(client, PREFIX_PLAIN ... " The maximum HP must be at least 1.");
return Plugin_Handled;
}
DoSetMaxHp(client, hp);
return Plugin_Handled;
}
public Action Command_Log(int client, int args)
{
if (!RequireInGame(client))
@@ -154,7 +236,7 @@ bool RequireInGame(int client)
if (client >= 1 && IsClientInGame(client))
return true;
ReplyToCommand(client, "[SM] This command can only be used in-game.");
ReplyToCommand(client, PREFIX_PLAIN ... " This command can only be used in-game.");
return false;
}
@@ -163,7 +245,7 @@ bool RequireCore(int client)
if (g_bCoreLoaded)
return true;
ReplyToCommand(client, "[SM] The Couch core plugin is not loaded.");
ReplyToCommand(client, PREFIX_PLAIN ... " The Couch core plugin is not loaded.");
return false;
}
@@ -175,7 +257,7 @@ void DoSpawn(int client)
{
if (Couch_Spawn(client) == -1)
{
ReplyToCommand(client, "[SM] Could not create the couch.");
ReplyToCommand(client, PREFIX_PLAIN ... " Could not create the couch.");
return;
}
@@ -187,7 +269,7 @@ void DoBring(int client)
{
if (!Couch_Bring(client))
{
ReplyToCommand(client, "[SM] There is no couch to bring. Use sm_spawncouch first.");
ReplyToCommand(client, PREFIX_PLAIN ... " There is no couch to bring. Use sm_spawncouch first.");
return;
}
@@ -199,13 +281,13 @@ void DoGoto(int client)
{
if (!IsPlayerAlive(client))
{
ReplyToCommand(client, "[SM] You must be alive to go to the couch.");
ReplyToCommand(client, PREFIX_PLAIN ... " You must be alive to go to the couch.");
return;
}
if (!Couch_Goto(client))
{
ReplyToCommand(client, "[SM] There is no couch to go to. Use sm_spawncouch first.");
ReplyToCommand(client, PREFIX_PLAIN ... " There is no couch to go to. Use sm_spawncouch first.");
return;
}
@@ -217,7 +299,7 @@ void DoRemove(int client)
{
if (!Couch_Remove())
{
ReplyToCommand(client, "[SM] There is no couch to remove.");
ReplyToCommand(client, PREFIX_PLAIN ... " There is no couch to remove.");
return;
}
@@ -234,6 +316,14 @@ void DoToggleEffect(int client)
LogAction(client, -1, "\"%L\" turned the couch effect %s.", client, bEnable ? "on" : "off");
}
void DoSetBeacon(int client, bool bEnable)
{
Couch_SetBeaconEnabled(bEnable);
ShowActivity2(client, PREFIX, "\x01Turned the couch beacon \x04%s\x01.", bEnable ? "on" : "off");
LogAction(client, -1, "\"%L\" turned the couch beacon %s.", client, bEnable ? "on" : "off");
}
// Announced to everyone so players know why their rifle stopped moving the couch.
void DoSetPistolOnly(int client, bool bEnable)
{
@@ -257,6 +347,45 @@ void DoToggleUse(int client)
LogAction(client, -1, "\"%L\" %s couch use.", client, bAllow ? "allowed" : "disallowed");
}
void DoSetHpEnabled(int client, bool bEnable)
{
Couch_SetHpEnabled(bEnable);
if (bEnable)
ShowActivity2(client, PREFIX, "\x01Turned couch HP \x04on\x01, zombies can break the couch now.");
else
ShowActivity2(client, PREFIX, "\x01Turned couch HP \x04off\x01.");
LogAction(client, -1, "\"%L\" turned couch HP %s.", client, bEnable ? "on" : "off");
}
// Read back afterwards so the announcement shows the clamped value. 0 breaks the couch.
void DoSetHp(int client, int hp)
{
Couch_SetHp(hp);
hp = Couch_GetHp();
char sHp[16], sMax[16];
FormatThousands(hp, sHp, sizeof(sHp));
FormatThousands(Couch_GetMaxHp(), sMax, sizeof(sMax));
ShowActivity2(client, PREFIX, "\x01Set the couch's \x04HP\x01 to \x04%s/%s\x01.", sHp, sMax);
LogAction(client, -1, "\"%L\" set the couch's HP to %d.", client, hp);
}
void DoSetMaxHp(int client, int hp)
{
Couch_SetMaxHp(hp);
hp = Couch_GetMaxHp();
char sHp[16], sMax[16];
FormatThousands(Couch_GetHp(), sHp, sizeof(sHp));
FormatThousands(hp, sMax, sizeof(sMax));
ShowActivity2(client, PREFIX, "\x01Set the couch's \x04maximum HP\x01 to \x04%s\x01, it is now at \x04%s/%s\x01.", sMax, sHp, sMax);
LogAction(client, -1, "\"%L\" set the couch's maximum HP to %d.", client, hp);
}
// The tier cvars belong to the core plugin, so they are looked up by name. sName is "move" or "button".
void DoChangeTier(int client, const char[] sName, int iDelta)
{
@@ -308,7 +437,8 @@ void DoSetPushScale(int client, float fScale)
LogAction(client, -1, "\"%L\" set the couch push scale to %.2f.", client, fScale);
}
// Only affects what this admin sees, so it is neither announced nor logged.
// Only affects what this admin sees, so it is neither announced nor logged. The choice lasts
// until they disconnect; on their next connect the log is on again.
void DoSetLogging(int client, bool bEnable)
{
g_bLogging[client] = bEnable;
@@ -316,7 +446,7 @@ void DoSetLogging(int client, bool bEnable)
if (bEnable)
PrintToChat(client, PREFIX ... "\x01Couch log \x04on\x01, see your console for who moves the couch and how.");
else
PrintToChat(client, PREFIX ... "\x01Couch log \x04off\x01.");
PrintToChat(client, PREFIX ... "\x01Couch log \x04off\x01 until you reconnect.");
}
// ---------------------------------------------------------------------------------------------
@@ -328,7 +458,7 @@ public void Couch_OnCouchInteraction(int couch, int client, CouchMethod method,
// Grenades are rare and knock the couch far, so everyone is told. One blast is one report, so no
// rate limit is needed. The userid is there so an admin can put it straight into sm_couchban.
if (method == CouchMethod_Grenade)
PrintToChatAll("\x01[Couch] \x04%N\x01 (#%d) hit the couch with a \x04grenade", client, GetClientUserId(client));
PrintToChatAll(PREFIX ... "%N\x01 (#%d) hit the couch with a \x04grenade", client, GetClientUserId(client));
if (!AnyoneLogging())
return;
@@ -366,7 +496,7 @@ public void Couch_OnCouchInteraction(int couch, int client, CouchMethod method,
if (!g_bLogging[i] || !IsClientInGame(i))
continue;
PrintToConsole(i, "[Couch] %N (#%d) %s", client, userid, sHow);
PrintToConsole(i, PREFIX_PLAIN ... " %N (#%d) %s", client, userid, sHow);
}
}
@@ -407,18 +537,19 @@ void ShowCouchMenu(int client)
menu.AddItem("remove", "Remove couch", ITEMDRAW_DISABLED);
char sItem[64];
FormatEx(sItem, sizeof(sItem), "Effect: %s", Couch_IsEffectEnabled() ? "ON" : "OFF");
menu.AddItem("effect", sItem);
FormatEx(sItem, sizeof(sItem), "Pistol only: %s", Couch_IsPistolOnly() ? "ON" : "OFF");
menu.AddItem("pistol", sItem);
FormatEx(sItem, sizeof(sItem), "Allow USE: %s", Couch_IsUseAllowed() ? "ON" : "OFF");
menu.AddItem("use", sItem);
FormatEx(sItem, sizeof(sItem), "Couch HP: %s", Couch_IsHpEnabled() ? "ON" : "OFF");
menu.AddItem("hp", sItem);
FormatEx(sItem, sizeof(sItem), "Couch logs (for me): %s", g_bLogging[client] ? "ON" : "OFF");
menu.AddItem("log", sItem);
menu.AddItem("effects", "Effects");
menu.AddItem("tiers", "Tier requirements");
menu.AddItem("pushscale", "Push scale");
@@ -451,14 +582,19 @@ public int MenuHandler_Couch(Menu menu, MenuAction action, int param1, int param
DoGoto(client);
else if (StrEqual(sInfo, "remove"))
DoRemove(client);
else if (StrEqual(sInfo, "effect"))
DoToggleEffect(client);
else if (StrEqual(sInfo, "pistol"))
DoSetPistolOnly(client, !Couch_IsPistolOnly());
else if (StrEqual(sInfo, "use"))
DoToggleUse(client);
else if (StrEqual(sInfo, "hp"))
DoSetHpEnabled(client, !Couch_IsHpEnabled());
else if (StrEqual(sInfo, "log"))
DoSetLogging(client, !g_bLogging[client]);
else if (StrEqual(sInfo, "effects"))
{
ShowEffectsMenu(client);
return 0;
}
else if (StrEqual(sInfo, "tiers"))
{
ShowTierMenu(client);
@@ -482,6 +618,69 @@ public int MenuHandler_Couch(Menu menu, MenuAction action, int param1, int param
return 0;
}
// Rebuilt on every display so the labels always reflect the current state.
void ShowEffectsMenu(int client)
{
Menu menu = new Menu(MenuHandler_Effects);
menu.SetTitle("Couch - Effects");
char sItem[64];
FormatEx(sItem, sizeof(sItem), "Effect: %s", Couch_IsEffectEnabled() ? "ON" : "OFF");
menu.AddItem("effect", sItem);
FormatEx(sItem, sizeof(sItem), "Beacon: %s", Couch_IsBeaconEnabled() ? "ON" : "OFF");
menu.AddItem("beacon", sItem);
menu.ExitBackButton = true;
menu.ExitButton = true;
menu.Display(client, MENU_TIME_FOREVER);
}
public int MenuHandler_Effects(Menu menu, MenuAction action, int param1, int param2)
{
switch (action)
{
case MenuAction_Select:
{
int client = param1;
if (!g_bCoreLoaded)
{
RequireCore(client);
return 0;
}
char sInfo[16];
menu.GetItem(param2, sInfo, sizeof(sInfo));
if (StrEqual(sInfo, "effect"))
DoToggleEffect(client);
else if (StrEqual(sInfo, "beacon"))
DoSetBeacon(client, !Couch_IsBeaconEnabled());
ShowEffectsMenu(client);
}
case MenuAction_Cancel:
{
if (param2 == MenuCancel_ExitBack)
{
if (g_bCoreLoaded)
ShowCouchMenu(param1);
else
RequireCore(param1);
}
}
case MenuAction_End:
{
delete menu;
}
}
return 0;
}
void ShowTierMenu(int client)
{
Menu menu = new Menu(MenuHandler_Tier);
+73 -32
View File
@@ -4,8 +4,6 @@
#pragma semicolon 1
#pragma newdecls required
#define PREFIX "\x01[SM] \x04"
// So commands fail with a message instead of an unbound-native error while the core is unloaded.
bool g_bCoreLoaded;
@@ -30,13 +28,13 @@ public Plugin myinfo =
public void OnPluginStart()
{
LoadTranslations("common.phrases"); // FindTarget's replies.
LoadTranslations("common.phrases"); // Target errors and multi-target names ("all players").
g_alBannedAccounts = new ArrayList();
RegAdminCmd("sm_couchban", Command_CouchBan, ADMFLAG_BAN, "sm_couchban <#userid|name> - bans a player from the couch until the end of the map");
RegAdminCmd("sm_couchban", Command_CouchBan, ADMFLAG_BAN, "sm_couchban <#userid|name|@filter> - bans players from the couch until the end of the map");
RegAdminCmd("sm_cban", Command_CouchBan, ADMFLAG_BAN, "Alias of sm_couchban");
RegAdminCmd("sm_couchunban", Command_CouchUnban, ADMFLAG_BAN, "sm_couchunban <#userid|name> - lifts a couch ban");
RegAdminCmd("sm_couchunban", Command_CouchUnban, ADMFLAG_BAN, "sm_couchunban <#userid|name|@filter> - lifts couch bans");
RegAdminCmd("sm_cunban", Command_CouchUnban, ADMFLAG_BAN, "Alias of sm_couchunban");
}
@@ -143,24 +141,11 @@ public Action Command_CouchBan(int client, int args)
if (args < 1)
{
ReplyToCommand(client, "[SM] Usage: sm_couchban <#userid|name>");
ReplyToCommand(client, PREFIX_PLAIN ... " Usage: sm_couchban <#userid|name|@filter>");
return Plugin_Handled;
}
char sArg[MAX_TARGET_LENGTH];
GetCmdArg(1, sArg, sizeof(sArg));
int target = FindTarget(client, sArg, true, true);
if (target == -1)
return Plugin_Handled;
if (g_bBanned[target])
{
ReplyToCommand(client, "[SM] %N is already couch-banned.", target);
return Plugin_Handled;
}
DoBan(client, target);
ProcessBanCommand(client, true);
return Plugin_Handled;
}
@@ -171,25 +156,83 @@ public Action Command_CouchUnban(int client, int args)
if (args < 1)
{
ReplyToCommand(client, "[SM] Usage: sm_couchunban <#userid|name>");
ReplyToCommand(client, PREFIX_PLAIN ... " Usage: sm_couchunban <#userid|name|@filter>");
return Plugin_Handled;
}
ProcessBanCommand(client, false);
return Plugin_Handled;
}
// Standard SourceMod targeting: a name, #userid or @filter (@all, @t, @zombies ...). Players already in
// the wanted state are skipped. One chat line covers the whole command; each ban is still logged.
void ProcessBanCommand(int client, bool bBan)
{
char sArg[MAX_TARGET_LENGTH];
GetCmdArg(1, sArg, sizeof(sArg));
int target = FindTarget(client, sArg, true, true);
if (target == -1)
return Plugin_Handled;
int targets[MAXPLAYERS];
char sTargetName[MAX_TARGET_LENGTH];
bool bIsML;
if (!g_bBanned[target])
int count = ProcessTargetString(sArg, client, targets, sizeof(targets), COMMAND_FILTER_CONNECTED | COMMAND_FILTER_NO_BOTS, sTargetName, sizeof(sTargetName), bIsML);
if (count <= 0)
{
ReplyToCommand(client, "[SM] %N is not couch-banned.", target);
return Plugin_Handled;
ReplyToTargetError(client, count);
return;
}
int affected = 0;
int last = 0;
for (int i = 0; i < count; i++)
{
int target = targets[i];
if (!IsClientInGame(target) || g_bBanned[target] == bBan)
continue;
if (bBan)
DoBan(client, target);
else
DoUnban(client, target);
return Plugin_Handled;
affected++;
last = target;
}
if (affected == 0)
{
if (count == 1)
ReplyToCommand(client, PREFIX_PLAIN ... " %N is %s couch-banned.", targets[0], bBan ? "already" : "not");
else
ReplyToCommand(client, PREFIX_PLAIN ... " No matching players to couch-%s.", bBan ? "ban" : "unban");
return;
}
// Announced to everyone so players know why the couch stopped reacting to someone. A single
// player is named even when they were picked out of a filter.
if (affected == 1)
{
if (bBan)
ShowActivity2(client, PREFIX, "\x01couch-banned \x04%N\x01 until the end of the map.", last);
else
ShowActivity2(client, PREFIX, "\x01removed couch-ban from \x04%N\x01.", last);
}
else if (bIsML)
{
if (bBan)
ShowActivity2(client, PREFIX, "\x01couch-banned \x04%t\x01 until the end of the map.", sTargetName);
else
ShowActivity2(client, PREFIX, "\x01removed couch-ban from \x04%t\x01.", sTargetName);
}
else
{
if (bBan)
ShowActivity2(client, PREFIX, "\x01couch-banned \x04%s\x01 until the end of the map.", sTargetName);
else
ShowActivity2(client, PREFIX, "\x01removed couch-ban from \x04%s\x01.", sTargetName);
}
}
bool RequireCore(int client)
@@ -197,7 +240,7 @@ bool RequireCore(int client)
if (g_bCoreLoaded)
return true;
ReplyToCommand(client, "[SM] The Couch core plugin is not loaded.");
ReplyToCommand(client, PREFIX_PLAIN ... " The Couch core plugin is not loaded.");
return false;
}
@@ -205,14 +248,13 @@ bool RequireCore(int client)
// Actions
// ---------------------------------------------------------------------------------------------
// The announcement is the command's job, so a filter doesn't produce a line per player.
void DoBan(int client, int target)
{
g_bBanned[target] = true;
RememberAccount(target);
Couch_SetClientBlocked(target, true);
// Announced to everyone so players know why the couch stopped reacting to someone.
PrintToChatAll("\x01[SM] \x04%N\x01 couch-banned \x04%N\x01 until the end of the map.", client, target);
LogAction(client, target, "\"%L\" couch-banned \"%L\" until the end of the map.", client, target);
}
@@ -222,7 +264,6 @@ void DoUnban(int client, int target)
ForgetAccount(target);
Couch_SetClientBlocked(target, false);
PrintToChatAll("\x01[SM] \x04%N\x01 removed couch-ban from \x04%N\x01.", client, target);
LogAction(client, target, "\"%L\" removed couch-ban from \"%L\".", client, target);
}
+147 -15
View File
@@ -2,6 +2,11 @@
#include <sdktools>
#include <Couch>
// Optional: without Zombie:Reloaded nobody is tagged as a zombie.
#undef REQUIRE_PLUGIN
#include <zombiereloaded>
#define REQUIRE_PLUGIN
#pragma semicolon 1
#pragma newdecls required
@@ -18,6 +23,24 @@
// Put in front of the name of anyone whose grenade hit the couch recently.
#define HUD_GRENADE_TAG "<G!> "
// Put in front of the name of anyone who was a zombie when they last hit or pushed the couch.
#define HUD_ZOMBIE_TAG "<Z> "
// First line while the couch is below full HP: "♥ 500/1000". It stays up on its own, so it is resent
// this often; that also catches players who joined after the last send. Held for two refreshes so a
// late timer tick can't let it lapse.
#define HUD_HP_HEART "♥"
#define HUD_HP_REFRESH 5.0
#define HUD_HP_HOLD (HUD_HP_REFRESH * 2.0)
// While the couch is below full and not healing, the heart blinks between "♥" and "♥!" on every
// tick of the expiry timer, so each state shows for HUD_EXPIRE_INTERVAL. Healing means a regen
// tick raised the HP within this long; regen ticks once a second, so a healing couch never lapses
// between ticks. A drop ends the healing state at once, so the blink starts on the hit. (The HUD
// fonts only cover the WGL4 set, which has ♥ but no empty heart, hence the "!" suffix.)
#define HUD_HP_HEART_ALERT "!"
#define HUD_HP_HEAL_HOLD 1.5
// A user message holds 255 bytes; HudMsg's fixed fields take 34 and the string its terminator.
// Overflowing drops the whole message.
#define HUD_MSG_BYTES 255
@@ -35,11 +58,19 @@ float g_fExpire[MAXPLAYERS + 1]; // GetGameTime() at which this client drops of
float g_fLastHit[MAXPLAYERS + 1]; // Ordering key: the most recent shooter is listed first.
float g_fGrenadeUntil[MAXPLAYERS + 1]; // GetGameTime() at which this client loses the grenade tag.
float g_fNextUseSend[MAXPLAYERS + 1]; // GetGameTime() before which this client's +use doesn't resend.
bool g_bZombieHit[MAXPLAYERS + 1]; // This client was a zombie at their last hit or push.
bool g_bZRLoaded;
float g_fNextExpiry; // Soonest expiry or tag drop among the names currently listed.
float g_fLastExpiry; // Latest expiry among them; the text is held until then.
bool g_bFlushQueued; // A send is already scheduled for the next frame.
bool g_bHudShown; // Clients currently have text up, so an empty list must clear it.
bool g_bHpLine; // The text starts with the HP line, so it must stay up even with nobody listed.
float g_fNextHpRefresh; // GetGameTime() at which the HP line is resent.
int g_iLastHp; // HP as of the last Couch_OnCouchHpChanged, or -1 if none seen.
float g_fHealUntil; // GetGameTime() until which the couch counts as healing; the heart is solid until then.
bool g_bHeartAlert; // The "♥!" state is the one currently up.
char g_sHudText[HUD_TEXT_MAX + 1];
@@ -65,6 +96,17 @@ public void OnPluginStart()
AutoExecConfig(true, "CouchHud");
}
public void OnAllPluginsLoaded()
{
g_bZRLoaded = LibraryExists("zombiereloaded");
}
public void OnLibraryAdded(const char[] name)
{
if (StrEqual(name, "zombiereloaded"))
g_bZRLoaded = true;
}
public void OnMapStart()
{
// TIMER_FLAG_NO_MAPCHANGE already killed the timer.
@@ -73,6 +115,11 @@ public void OnMapStart()
g_bHudShown = false;
g_fNextExpiry = 0.0;
g_fLastExpiry = 0.0;
g_bHpLine = false;
g_fNextHpRefresh = 0.0;
g_iLastHp = -1;
g_fHealUntil = 0.0;
g_bHeartAlert = false;
for (int i = 1; i <= MAXPLAYERS; i++)
{
@@ -80,6 +127,7 @@ public void OnMapStart()
g_fLastHit[i] = 0.0;
g_fGrenadeUntil[i] = 0.0;
g_fNextUseSend[i] = 0.0;
g_bZombieHit[i] = false;
}
}
@@ -89,6 +137,7 @@ public void OnClientPutInServer(int client)
g_fLastHit[client] = 0.0;
g_fGrenadeUntil[client] = 0.0;
g_fNextUseSend[client] = 0.0;
g_bZombieHit[client] = false;
}
public void OnClientDisconnect(int client)
@@ -99,6 +148,7 @@ public void OnClientDisconnect(int client)
g_fLastHit[client] = 0.0;
g_fGrenadeUntil[client] = 0.0;
g_fNextUseSend[client] = 0.0;
g_bZombieHit[client] = false;
if (bListed)
QueueFlush();
@@ -114,6 +164,8 @@ public void OnLibraryRemoved(const char[] name)
{
if (StrEqual(name, "Couch"))
ResetAll();
else if (StrEqual(name, "zombiereloaded"))
g_bZRLoaded = false;
}
// ---------------------------------------------------------------------------------------------
@@ -129,6 +181,7 @@ public void Couch_OnCouchDamaged(int couch, int attacker, float damage)
float fNow = GetGameTime();
g_fLastHit[attacker] = fNow;
g_fExpire[attacker] = MaxFloat(g_fExpire[attacker], fNow + g_cvTime.FloatValue);
g_bZombieHit[attacker] = (g_bZRLoaded && ZR_IsClientZombie(attacker));
QueueFlush();
}
@@ -142,6 +195,9 @@ public void Couch_OnCouchInteraction(int couch, int client, CouchMethod method,
float fNow = GetGameTime();
// Every method lands here, so this covers +use and grenades, which never go through damage.
g_bZombieHit[client] = (g_bZRLoaded && ZR_IsClientZombie(client));
if (method == CouchMethod_Use)
{
g_fLastHit[client] = fNow;
@@ -174,6 +230,27 @@ public void Couch_OnCouchRemoved()
ResetAll();
}
// Regen ticks land here once a second; the flush decides whether the line is still wanted. A rise
// means a regen tick (or an admin raising it) and keeps the heart solid; a drop means a zombie hit
// (or an admin lowering it) and starts the blink at once.
public void Couch_OnCouchHpChanged(int couch, int hp, int maxhp)
{
if (!g_cvEnabled.BoolValue)
return;
if (g_iLastHp >= 0)
{
if (hp > g_iLastHp)
g_fHealUntil = GetGameTime() + HUD_HP_HEAL_HOLD;
else if (hp < g_iLastHp)
g_fHealUntil = 0.0;
}
g_iLastHp = hp;
QueueFlush();
}
// ---------------------------------------------------------------------------------------------
// Sending
// ---------------------------------------------------------------------------------------------
@@ -203,7 +280,7 @@ void Flush()
return;
}
if (BuildHudText(g_sHudText, sizeof(g_sHudText)) == 0)
if (!BuildHudText(g_sHudText, sizeof(g_sHudText)))
{
ClearAll();
return;
@@ -215,8 +292,20 @@ void Flush()
void Send()
{
// Held until the last name lapses, so the text leaves on its own if no update ever comes.
// Names lapsing sooner are dropped by a resend from the expiry timer.
SetHudTextParams(0.8, 0.2, g_fLastExpiry - GetGameTime(), 255, 255, 255, 255, 0, 0.0, 0.0, 0.0);
// Names lapsing sooner are dropped by a resend from the expiry timer. The HP line outlives
// the names and is kept up by the refresh instead.
float fNow = GetGameTime();
float fHold = g_fLastExpiry - fNow;
if (g_bHpLine)
{
if (fHold < HUD_HP_HOLD)
fHold = HUD_HP_HOLD;
g_fNextHpRefresh = fNow + HUD_HP_REFRESH;
}
SetHudTextParams(0.8, 0.2, fHold, 255, 255, 255, 255, 0, 0.0, 0.0, 0.0);
for (int i = 1; i <= MaxClients; i++)
{
@@ -231,9 +320,10 @@ void Send()
g_hExpireTimer = CreateTimer(HUD_EXPIRE_INTERVAL, Timer_Expire, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);
}
// Most recent shooters first. Returns how many players are listed, shown or not. Names are
// packed until the message is full, then come off the bottom until a "+ N" line fits.
int BuildHudText(char[] text, int maxlen)
// HP line first when the couch is below full, then the most recent shooters. Returns whether there
// is anything to show. Names are packed until the message is full, then come off the bottom until
// a "+ N" line fits.
bool BuildHudText(char[] text, int maxlen)
{
float fNow = GetGameTime();
bool bTaken[MAXPLAYERS + 1];
@@ -276,13 +366,26 @@ int BuildHudText(char[] text, int maxlen)
g_fLastExpiry = g_fExpire[best];
}
if (total == 0)
{
g_bHpLine = (Couch_IsHpEnabled() && Couch_GetHp() < Couch_GetMaxHp());
text[0] = '\0';
return 0;
if (g_bHpLine)
{
// Solid while healing; the timer only flips it otherwise.
if (fNow < g_fHealUntil)
g_bHeartAlert = false;
char sHp[16], sMax[16];
FormatThousands(Couch_GetHp(), sHp, sizeof(sHp));
FormatThousands(Couch_GetMaxHp(), sMax, sizeof(sMax));
FormatEx(text, maxlen, HUD_HP_HEART ... "%s %s/%s", g_bHeartAlert ? HUD_HP_HEART_ALERT : "", sHp, sMax);
}
strcopy(text, maxlen, "Shooting couch:");
if (total == 0)
return g_bHpLine;
StrCat(text, maxlen, g_bHpLine ? "\nShooting couch:" : "Shooting couch:");
// rowStart[n] is the text length before row n, so a row can be cut back off.
int rowStart[MAXPLAYERS + 1];
@@ -294,8 +397,8 @@ int BuildHudText(char[] text, int maxlen)
GetClientName(order[rows], sName, sizeof(sName));
TruncateUtf8(sName, HUD_NAME_BYTES);
char sLine[HUD_NAME_BYTES + sizeof(HUD_GRENADE_TAG) + 1];
FormatEx(sLine, sizeof(sLine), "\n%s%s", (g_fGrenadeUntil[order[rows]] > fNow) ? HUD_GRENADE_TAG : "", sName);
char sLine[HUD_NAME_BYTES + sizeof(HUD_ZOMBIE_TAG) + sizeof(HUD_GRENADE_TAG) + 1];
FormatEx(sLine, sizeof(sLine), "\n%s%s%s", g_bZombieHit[order[rows]] ? HUD_ZOMBIE_TAG : "", (g_fGrenadeUntil[order[rows]] > fNow) ? HUD_GRENADE_TAG : "", sName);
int len = strlen(text);
if (len + strlen(sLine) >= maxlen)
@@ -321,7 +424,7 @@ int BuildHudText(char[] text, int maxlen)
text[rowStart[rows]] = '\0';
}
return total;
return true;
}
// Cuts to at most maxBytes without leaving a partial multi-byte character on the end.
@@ -347,8 +450,30 @@ public Action Timer_Expire(Handle timer)
return Plugin_Stop;
}
// Only resend when a name has lapsed.
if (GetGameTime() >= g_fNextExpiry)
// Only resend when a name has lapsed, the HP line is due its refresh, or the heart has to flip.
// With nobody listed there is no expiry (0.0), so the guard stops the HP line being resent
// every tick.
float fNow = GetGameTime();
bool bExpiry = (g_fNextExpiry > 0.0 && fNow >= g_fNextExpiry);
bool bRefresh = (g_bHpLine && fNow >= g_fNextHpRefresh);
bool bHeart = false;
if (g_bHpLine)
{
if (fNow >= g_fHealUntil)
{
// Not healing: alternate every tick.
g_bHeartAlert = !g_bHeartAlert;
bHeart = true;
}
else if (g_bHeartAlert)
{
// Healing started while "♥!" was up; put the solid heart back.
bHeart = true;
}
}
if (bExpiry || bRefresh || bHeart)
Flush();
return Plugin_Continue;
@@ -378,7 +503,14 @@ void ResetAll()
g_fLastHit[i] = 0.0;
g_fGrenadeUntil[i] = 0.0;
g_fNextUseSend[i] = 0.0;
g_bZombieHit[i] = false;
}
g_bHpLine = false;
g_fNextHpRefresh = 0.0;
g_iLastHp = -1;
g_fHealUntil = 0.0;
g_bHeartAlert = false;
ClearAll();
}
+100
View File
@@ -4,6 +4,32 @@
#define _Couch_included
// Chat prefix shared by the whole couch plugin set. The trailing colour code tints the token that
// follows it (a name or value); messages switch back with \x01. PREFIX_PLAIN is for console
// replies, which don't render colour codes.
#define PREFIX "\x07CD853F[Couch] \x04"
#define PREFIX_PLAIN "[Couch]"
// Non-negative integer with a comma every three digits: 1532 -> "1,532".
stock void FormatThousands(int value, char[] buffer, int maxlen)
{
char sDigits[16];
int len = IntToString(value, sDigits, sizeof(sDigits));
int out = 0;
for (int i = 0; i < len && out < maxlen - 1; i++)
{
// A comma goes before every digit that starts a new group of three from the right.
if (i > 0 && (len - i) % 3 == 0)
buffer[out++] = ',';
if (out < maxlen - 1)
buffer[out++] = sDigits[i];
}
buffer[out] = '\0';
}
// How a player moved the couch, passed to Couch_OnCouchInteraction.
enum CouchMethod
{
@@ -58,6 +84,25 @@ forward void Couch_OnCouchInteraction(int couch, int client, CouchMethod method,
*/
forward void Couch_OnClientBlocked(int couch, int client);
/**
* Called whenever the couch's HP changes: zombie hits, regen ticks, admin sets, resets to full
* on spawn and round start, and when the HP feature is switched on or off.
*
* @param couch Entity index of the couch, or -1 if the feature was switched off while there was none.
* @param hp Current HP.
* @param maxhp Maximum HP, sm_couch_hp_max.
*/
forward void Couch_OnCouchHpChanged(int couch, int hp, int maxhp);
/**
* Called when the couch's HP reaches 0, before the humans are killed and the couch is removed.
* Couch_OnCouchRemoved follows.
*
* @param couch Entity index of the couch; still valid during this call.
* @param attacker Client index of the zombie whose hit broke it, or 0 when an admin set the HP to 0.
*/
forward void Couch_OnCouchBroken(int couch, int attacker);
/**
* @return Entity index of the couch, or -1 if there is none.
*/
@@ -114,6 +159,16 @@ native bool Couch_IsEffectEnabled();
*/
native void Couch_SetEffectEnabled(bool enabled);
/**
* Whether the sm_beacon-style rings on the couch are on. Mirrors sm_couch_beacon.
*/
native bool Couch_IsBeaconEnabled();
/**
* Switches the beacon rings on or off. Writes sm_couch_beacon.
*/
native void Couch_SetBeaconEnabled(bool enabled);
/**
* Whether only pistols and knives may move the couch. Mirrors sm_couch_pistol_only.
*/
@@ -151,6 +206,43 @@ native void Couch_SetClientBlocked(int client, bool blocked);
*/
native bool Couch_IsClientBlocked(int client);
/**
* Whether couch HP is on: zombie hits wear the couch down and at 0 HP it breaks, killing every
* human. Mirrors sm_couch_hp.
*/
native bool Couch_IsHpEnabled();
/**
* Switches couch HP on or off. Turning it on sets the HP to full. Writes sm_couch_hp.
*/
native void Couch_SetHpEnabled(bool enabled);
/**
* @return Current couch HP, or 0 if there is no couch.
*/
native int Couch_GetHp();
/**
* Sets the couch's HP, clamped to 0..Couch_GetMaxHp(). Setting 0 breaks the couch as a zombie
* hit would, provided the HP feature is on and a couch exists.
*
* @param hp New HP.
*/
native void Couch_SetHp(int hp);
/**
* @return Maximum couch HP. Mirrors sm_couch_hp_max.
*/
native int Couch_GetMaxHp();
/**
* Sets the maximum couch HP. A couch at full HP stays full; otherwise the HP is clamped to the
* new maximum. Writes sm_couch_hp_max.
*
* @param maxhp New maximum, at least 1.
*/
native void Couch_SetMaxHp(int maxhp);
public SharedPlugin __pl_Couch =
{
@@ -175,12 +267,20 @@ public SharedPlugin __pl_Couch =
MarkNativeAsOptional("Couch_Remove");
MarkNativeAsOptional("Couch_IsEffectEnabled");
MarkNativeAsOptional("Couch_SetEffectEnabled");
MarkNativeAsOptional("Couch_IsBeaconEnabled");
MarkNativeAsOptional("Couch_SetBeaconEnabled");
MarkNativeAsOptional("Couch_IsPistolOnly");
MarkNativeAsOptional("Couch_SetPistolOnly");
MarkNativeAsOptional("Couch_IsUseAllowed");
MarkNativeAsOptional("Couch_SetUseAllowed");
MarkNativeAsOptional("Couch_SetClientBlocked");
MarkNativeAsOptional("Couch_IsClientBlocked");
MarkNativeAsOptional("Couch_IsHpEnabled");
MarkNativeAsOptional("Couch_SetHpEnabled");
MarkNativeAsOptional("Couch_GetHp");
MarkNativeAsOptional("Couch_SetHp");
MarkNativeAsOptional("Couch_GetMaxHp");
MarkNativeAsOptional("Couch_SetMaxHp");
}
#endif
+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,44 +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);
// CreateConVar keeps the old bounds when the cvar already exists (plugin reload),
// so apply the lower bound explicitly.
g_cvInterval.SetBounds(ConVarBound_Lower, true, -1.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();
}
@@ -62,7 +34,7 @@ void RestartAnnounceTimer()
{
delete g_hAnnounceTimer;
float interval = g_cvInterval.FloatValue;
float interval = g_cvAnnounceInterval.FloatValue;
if (interval <= 0.0)
return;
@@ -127,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,22 +162,47 @@ 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);
}
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];
+16
View File
@@ -537,6 +537,10 @@ public void CheckPopulation()
{
SetFakeToTeam();
}
else if (getFakesNeededInTeam() < fakes_in_team)
{
SetFakeToSpec();
}
}
public bool nightTime()
@@ -573,6 +577,18 @@ public void SetFakeToTeam()
}
}
public void SetFakeToSpec()
{
for(int i = 1; i <= MaxClients; i++)
{
if(IsClientConnected(i) && IsClientInGame(i) && IsFakeClient(i) && !IsClientSourceTV(i) && GetClientTeam(i) > CS_TEAM_SPECTATOR)
{
ChangeClientTeam(i, CS_TEAM_SPECTATOR);
break;
}
}
}
public void CreateFake()
{
if (g_hNames.Length == 0 || g_hClanNames.Length == 0)
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;
}
@@ -0,0 +1,97 @@
"Games"
{
"cstrike"
{
"Keys"
{
"INTERFACEVERSION_SERVERGAMEDLL" "ServerGameDLL012"
}
"Signatures"
{
"CreateInterface_Server"
{
"library" "server"
"linux" "@CreateInterface"
"windows" "@CreateInterface"
}
"g_SendTableCRC"
{
"library" "engine"
"linux" "@g_SendTableCRC"
// IDK
//"windows" ""
}
}
"Offsets"
{
"IServerGameDLL::GetAllServerClasses"
{
"linux" "11"
"windows" "11"
}
"ServerClass::m_pNext"
{
"linux" "8"
"windows" "8"
}
"ServerClass::m_pTable"
{
"linux" "4"
"windows" "4"
}
"SendTable::m_pNetTableName"
{
"linux" "8"
"windows" "8"
}
"SendTable::m_nProps"
{
"linux" "4"
"windows" "4"
}
"SendTable::m_pProps"
{
"linux" "0"
"windows" "0"
}
"SendProp::m_Type"
{
"linux" "8"
"windows" "8"
}
"SendProp::m_pDataTable"
{
"linux" "68"
"windows" "68"
}
"SendProp::m_pVarName"
{
"linux" "48"
"windows" "48"
}
"SendProp::m_nBits"
{
"linux" "12"
"windows" "12"
}
"SendProp::sizeof"
{
"linux" "80"
"windows" "80"
}
}
}
}
@@ -0,0 +1,267 @@
#pragma semicolon 1
#pragma newdecls required
#include <sourcemod>
#include <sdktools>
#define PLUGIN_NAME "ScoreboardUnlocker"
#define PLUGIN_AUTHOR "Irineu"
#define PLUGIN_VERSION "1.0"
#define DPT_DataTable 6
public Plugin myinfo =
{
name = PLUGIN_NAME,
author = PLUGIN_AUTHOR,
description = "Increases the frags limit in the scoreboard",
version = PLUGIN_VERSION,
url = ""
};
bool g_bLate = false;
Address serverGameDLL = 0;
Address g_pSendTableCRC = 0;
int g_iServerClass_m_pNext = -1;
int g_iServerClass_m_pTable = -1;
int g_iSendTable_m_pNetTableName = -1;
int g_iSendTable_m_nProps = -1;
int g_iSendTable_m_pProps = -1;
int g_iSendProp_m_Type = -1;
int g_iSendProp_m_pDataTable = -1;
int g_iSendProp_m_pVarName = -1;
int g_iSendProp_m_nBits = -1;
int g_iSendProp_sizeof = -1;
Handle g_hGetAllServerClasses = INVALID_HANDLE;
public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
g_bLate = late;
return APLRes_Success;
}
public void OnPluginStart()
{
GameData gd = new GameData("ScoreboardUnlocker");
if (!gd)
SetFailState("Missing gamedata file");
StartPrepSDKCall(SDKCall_Static);
if (!PrepSDKCall_SetFromConf(gd, SDKConf_Signature, "CreateInterface_Server"))
SetFailState("Failed to get CreateInterface_Server");
PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer);
PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer, VDECODE_FLAG_ALLOWNULL);
PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
Handle call = EndPrepSDKCall();
StartPrepSDKCall(SDKCall_Raw);
if (!PrepSDKCall_SetFromConf(gd, SDKConf_Virtual, "IServerGameDLL::GetAllServerClasses"))
SetFailState("Failed to get IServerGameDLL::GetAllServerClasses");
PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
g_hGetAllServerClasses = EndPrepSDKCall();
char GameDLLInterfaceName[64];
if (!gd.GetKeyValue("INTERFACEVERSION_SERVERGAMEDLL", GameDLLInterfaceName, sizeof(GameDLLInterfaceName)))
SetFailState("Failed to get gamedll interface name");
serverGameDLL = SDKCall(call, GameDLLInterfaceName, 0);
if (serverGameDLL == Address_Null)
SetFailState("Failed to get gamedll interface");
g_pSendTableCRC = gd.GetMemSig("g_SendTableCRC");
if (g_pSendTableCRC == Address_Null)
SetFailState("Failed to get g_SendTableCRC");
g_iServerClass_m_pNext = GetRequiredOffset(gd, "ServerClass::m_pNext");
g_iServerClass_m_pTable = GetRequiredOffset(gd, "ServerClass::m_pTable");
g_iSendTable_m_pNetTableName = GetRequiredOffset(gd, "SendTable::m_pNetTableName");
g_iSendTable_m_nProps = GetRequiredOffset(gd, "SendTable::m_nProps");
g_iSendTable_m_pProps = GetRequiredOffset(gd, "SendTable::m_pProps");
g_iSendProp_m_Type = GetRequiredOffset(gd, "SendProp::m_Type");
g_iSendProp_m_pDataTable = GetRequiredOffset(gd, "SendProp::m_pDataTable");
g_iSendProp_m_pVarName = GetRequiredOffset(gd, "SendProp::m_pVarName");
g_iSendProp_m_nBits = GetRequiredOffset(gd, "SendProp::m_nBits");
g_iSendProp_sizeof = GetRequiredOffset(gd, "SendProp::sizeof");
ConVar sv_sendtables = FindConVar("sv_sendtables");
if (!sv_sendtables)
SetFailState("Failed to find sv_sendtables");
sv_sendtables.BoolValue = true;
if (!g_bLate)
FulfillMyPurpose();
CloseHandle(call);
CloseHandle(gd);
}
public void OnMapInit(const char[] mapName)
{
if (g_bLate)
{
FulfillMyPurpose();
g_bLate = false;
}
}
void FulfillMyPurpose()
{
Address pTable = FindSendTable("DT_PlayerResource");
if (pTable == Address_Null)
SetFailState("Failed to find DT_PlayerResource");
for (int i = 1; i <= MaxClients; i++)
{
char name[64];
Format(name, sizeof(name), "m_iScore.%03d", i);
Address pProp = FindSendProp(pTable, name);
if (pProp != Address_Null)
StoreToAddress(pProp + view_as<Address>(g_iSendProp_m_nBits), 32, NumberType_Int32, false);
else
SetFailState("Failed to find m_iScore.%03d", i);
}
int crc = LoadFromAddress(g_pSendTableCRC, NumberType_Int32);
StoreToAddress(g_pSendTableCRC, crc + 1, NumberType_Int32, false);
}
int GetRequiredOffset(GameData gd, const char[] name)
{
int ret = gd.GetOffset(name);
if (ret == -1)
SetFailState("Missing offset %s from gamedata file", name);
return ret;
}
void LoadStringFromAddress(char[] buf, int bufSize, Address address)
{
for (int i = 0; i < bufSize; i++)
{
char c = LoadFromAddress(address + view_as<Address>(i), NumberType_Int8);
if (c == 0)
break;
buf[i] = c;
}
buf[bufSize - 1] = 0;
}
enum struct bazinga
{
Address pAddr;
}
Address FindSendTable(const char[] name)
{
ArrayList sendTables = new ArrayList();
for (Address pServerClass = SDKCall(g_hGetAllServerClasses, serverGameDLL);
pServerClass != Address_Null;
pServerClass = LoadFromAddress(pServerClass + view_as<Address>(g_iServerClass_m_pNext), NumberType_Int32))
{
Address pTable = LoadFromAddress(pServerClass + view_as<Address>(g_iServerClass_m_pTable), NumberType_Int32);
if (pTable != Address_Null)
{
bazinga thing;
thing.pAddr = pTable;
sendTables.PushArray(thing, sizeof(bazinga));
}
}
while (sendTables.Length > 0)
{
bazinga thing;
sendTables.GetArray(0, thing, sizeof(bazinga));
Address pTable = thing.pAddr;
sendTables.Erase(0);
char tableName[64];
Address pName = LoadFromAddress(pTable + view_as<Address>(g_iSendTable_m_pNetTableName), NumberType_Int32);
LoadStringFromAddress(tableName, sizeof(tableName), pName);
if (strcmp(tableName, name) == 0)
{
delete sendTables;
return pTable;
}
else
{
int nProps = LoadFromAddress(pTable + view_as<Address>(g_iSendTable_m_nProps), NumberType_Int32);
for (int i = 0; i < nProps; i++)
{
Address pProp = LoadFromAddress(pTable + view_as<Address>(g_iSendTable_m_pProps), NumberType_Int32) + (i * g_iSendProp_sizeof);
Address pPropTable = LoadFromAddress(pProp + view_as<Address>(g_iSendProp_m_pDataTable), NumberType_Int32);
int propType = LoadFromAddress(pProp + view_as<Address>(g_iSendProp_m_Type), NumberType_Int32);
if (propType == DPT_DataTable && pPropTable != Address_Null)
{
thing.pAddr = pPropTable;
sendTables.PushArray(thing, sizeof(bazinga));
}
}
}
}
delete sendTables;
return Address_Null;
}
Address FindInSendTable(Address pTable, const char[] name, int requiredType = -1)
{
int nProps = LoadFromAddress(pTable + view_as<Address>(g_iSendTable_m_nProps), NumberType_Int32);
for (int i = 0; i < nProps; i++)
{
Address pProp = LoadFromAddress(pTable + view_as<Address>(g_iSendTable_m_pProps), NumberType_Int32) + (i * g_iSendProp_sizeof);
char propName[64];
Address pName = LoadFromAddress(pProp + view_as<Address>(g_iSendProp_m_pVarName), NumberType_Int32);
LoadStringFromAddress(propName, sizeof(propName), pName);
int propType = LoadFromAddress(pProp + view_as<Address>(g_iSendProp_m_Type), NumberType_Int32);
if (strcmp(propName, name) == 0 && (propType == requiredType || requiredType == -1))
return pProp;
}
return Address_Null;
}
Address FindSendProp(Address pTable, const char[] name, int requiredType = -1)
{
char propNames[16][64];
int nameCount = ExplodeString(name, ".", propNames, 16, 64);
Address pProp = Address_Null;
// Please don't ever reuse parameters for local variables in this stupid language again, future me.
Address pTable2 = pTable;
for (int i = 0; i < nameCount; )
{
if (pProp != Address_Null)
{
Address pPropTable = LoadFromAddress(pProp + view_as<Address>(g_iSendProp_m_pDataTable), NumberType_Int32);
if (pPropTable != Address_Null)
pTable2 = pPropTable;
}
int type = i == (nameCount - 1) ? requiredType : DPT_DataTable;
pProp = FindInSendTable(pTable2, propNames[i], type);
if (pProp == Address_Null)
{
if (i == 0)
{
pProp = FindInSendTable(pTable2, "baseclass", DPT_DataTable);
if (pProp == Address_Null)
break;
}
else
{
break;
}
}
else
{
i++;
}
}
return pProp;
}
+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;
}
+104 -45
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,22 +128,67 @@ public Action UpdateHide(Handle timer)
if (!g_hCVar_HideEnabled.BoolValue)
return Plugin_Continue;
g_iLeader = Leader_CurrentLeader();
//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 ((g_iHideRange[i] != DISABLED || g_bHasSomebodyToHide[i]) && IsClientInGame(i))
{
bAnyWork = true;
break;
}
}
if (!bAnyWork)
return Plugin_Continue;
for (int client = 1; client <= MaxClients; client++)
//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++)
{
if (!IsValidClient(client) || IsClientSourceTV(client) || IsFakeClient(client))
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]);
//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];
}
//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] && TransmitManager_IsEntityHooked(client))
if (g_bHasSomebodyToHide[client] && g_bHooked[client])
{
for (int target = 1; target <= MaxClients; target++)
{
if (IsValidClient(target) && client != target && TransmitManager_IsEntityHooked(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;
@@ -131,16 +196,16 @@ public Action UpdateHide(Handle timer)
}
g_bHasSomebodyToHide[client] = false;
}
continue;
return;
}
//lazy fix for invisible zm
if (TransmitManager_IsEntityHooked(client))
if (g_bHooked[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))
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
@@ -148,22 +213,19 @@ public Action UpdateHide(Handle timer)
}
}
bool ClientIsAlive = IsPlayerAlive(client);
bool ClientIsHuman = false;
if (ClientIsAlive)
{
ClientIsHuman = ZR_IsClientHuman(client);
}
bool ClientHasItem = EW_ClientHasItem(client);
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 (g_iLeader == client || ClientHasItem || !ClientIsAlive || !ClientIsHuman)
if (ClientIsLeader || ClientHasItem || !ClientIsAlive || !ClientIsHuman)
{
if (!g_bHasTobeVisible[client] && TransmitManager_IsEntityHooked(client))
if (!g_bHasTobeVisible[client] && g_bHooked[client])
{
for (int target = 1; target <= MaxClients; target++)
{
if (IsValidClient(target) && client != target && TransmitManager_IsEntityHooked(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
@@ -178,61 +240,56 @@ public Action UpdateHide(Handle timer)
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))
if (ClientIsAlive && ClientIsHuman && (ClientIsLeader || ClientHasItem))
{
DoTheTargetCheck(client);
}
continue;
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);
}
return Plugin_Continue;
}
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)
{
GetClientAbsOrigin(target, fOriginTarget);
if (GetVectorDistance(fOriginTarget, fOriginClient, true) <= float(g_iHideRange[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)
{
//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))
if (!g_bHidePlayers[client][target] && bClientHooked && g_bHooked[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))
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;
}
}
}
}
}
public Action OnHideSettings(int client, int args)
@@ -393,11 +450,14 @@ stock int FindTierForCommandOverride(const char[] command)
//----------------------------------------------------------------------------------------------------
public void MenuHandler_CookieMenu(int client, CookieMenuAction action, any info, char[] buffer, int maxlen)
{
if (GetFeatureStatus(FeatureType_Native, "GetPlayerTier_native") == FeatureStatus_Available)
{
int client_tier = GetPlayerTier_native(client);
if (g_iTierHide > client_tier)
{
return;
}
}
switch(action)
{
case(CookieMenuAction_DisplayOption):
@@ -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();