357 lines
8.9 KiB
SourcePawn
357 lines
8.9 KiB
SourcePawn
#include <sourcemod>
|
|
#include <sdktools>
|
|
#include <Couch>
|
|
|
|
#pragma semicolon 1
|
|
#pragma newdecls required
|
|
|
|
// How often expired names are swept off the list. Only resends if a name actually dropped.
|
|
#define HUD_EXPIRE_INTERVAL 0.25
|
|
|
|
// Longer names are cut so one can't eat the whole message.
|
|
#define HUD_NAME_BYTES 20
|
|
|
|
// Put in front of the name of anyone whose grenade hit the couch recently.
|
|
#define HUD_GRENADE_TAG "<G!> "
|
|
|
|
// 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
|
|
#define HUD_MSG_HEADER_BYTES 34
|
|
#define HUD_TEXT_MAX (HUD_MSG_BYTES - HUD_MSG_HEADER_BYTES - 1)
|
|
|
|
ConVar g_cvEnabled;
|
|
ConVar g_cvTime;
|
|
ConVar g_cvGrenadeTime;
|
|
|
|
Handle g_hHudSync;
|
|
Handle g_hExpireTimer;
|
|
|
|
float g_fExpire[MAXPLAYERS + 1]; // GetGameTime() at which this client drops off the list.
|
|
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_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.
|
|
|
|
char g_sHudText[HUD_TEXT_MAX + 1];
|
|
|
|
public Plugin myinfo =
|
|
{
|
|
name = "Couch HUD",
|
|
author = "marisakirisame.net",
|
|
description = "Shows who is shooting the couch",
|
|
version = "2.0",
|
|
url = "https://marisakirisame.net"
|
|
};
|
|
|
|
public void OnPluginStart()
|
|
{
|
|
g_hHudSync = CreateHudSynchronizer();
|
|
|
|
g_cvEnabled = CreateConVar("sm_couch_hud_enabled", "1", "Show the list of players shooting the couch.", FCVAR_NOTIFY, true, 0.0, true, 1.0);
|
|
g_cvTime = CreateConVar("sm_couch_hud_time", "0.5", "How long a player stays on the list after their last hit, in seconds.", FCVAR_NOTIFY, true, 0.5, true, 30.0);
|
|
g_cvGrenadeTime = CreateConVar("sm_couch_hud_time_grenade", "4.0", "How long a player stays on the list, tagged " ... HUD_GRENADE_TAG ... ", after their grenade hits the couch, in seconds.", FCVAR_NOTIFY, true, 0.5, true, 30.0);
|
|
|
|
g_cvEnabled.AddChangeHook(OnEnabledChanged);
|
|
|
|
AutoExecConfig(true, "CouchHud");
|
|
}
|
|
|
|
public void OnMapStart()
|
|
{
|
|
// TIMER_FLAG_NO_MAPCHANGE already killed the timer.
|
|
g_hExpireTimer = null;
|
|
g_bFlushQueued = false;
|
|
g_bHudShown = false;
|
|
g_fNextExpiry = 0.0;
|
|
g_fLastExpiry = 0.0;
|
|
|
|
for (int i = 1; i <= MAXPLAYERS; i++)
|
|
{
|
|
g_fExpire[i] = 0.0;
|
|
g_fLastHit[i] = 0.0;
|
|
g_fGrenadeUntil[i] = 0.0;
|
|
}
|
|
}
|
|
|
|
public void OnClientPutInServer(int client)
|
|
{
|
|
g_fExpire[client] = 0.0;
|
|
g_fLastHit[client] = 0.0;
|
|
g_fGrenadeUntil[client] = 0.0;
|
|
}
|
|
|
|
public void OnClientDisconnect(int client)
|
|
{
|
|
bool bListed = (g_fExpire[client] > GetGameTime());
|
|
|
|
g_fExpire[client] = 0.0;
|
|
g_fLastHit[client] = 0.0;
|
|
g_fGrenadeUntil[client] = 0.0;
|
|
|
|
if (bListed)
|
|
QueueFlush();
|
|
}
|
|
|
|
public void OnEnabledChanged(ConVar convar, const char[] oldValue, const char[] newValue)
|
|
{
|
|
if (!convar.BoolValue)
|
|
ResetAll();
|
|
}
|
|
|
|
public void OnLibraryRemoved(const char[] name)
|
|
{
|
|
if (StrEqual(name, "Couch"))
|
|
ResetAll();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Couch forwards
|
|
// ---------------------------------------------------------------------------------------------
|
|
|
|
public void Couch_OnCouchDamaged(int couch, int attacker, float damage)
|
|
{
|
|
if (!g_cvEnabled.BoolValue)
|
|
return;
|
|
|
|
// Extended, never shortened, so gunfire right after a grenade doesn't cut its time short.
|
|
float fNow = GetGameTime();
|
|
g_fLastHit[attacker] = fNow;
|
|
g_fExpire[attacker] = MaxFloat(g_fExpire[attacker], fNow + g_cvTime.FloatValue);
|
|
|
|
QueueFlush();
|
|
}
|
|
|
|
// Fires just after Couch_OnCouchDamaged for the same hit, so the ordering is already updated.
|
|
public void Couch_OnCouchInteraction(int couch, int client, CouchMethod method, const char[] weapon)
|
|
{
|
|
if (!g_cvEnabled.BoolValue || method != CouchMethod_Grenade)
|
|
return;
|
|
|
|
g_fGrenadeUntil[client] = GetGameTime() + g_cvGrenadeTime.FloatValue;
|
|
g_fExpire[client] = MaxFloat(g_fExpire[client], g_fGrenadeUntil[client]);
|
|
|
|
QueueFlush();
|
|
}
|
|
|
|
float MaxFloat(float a, float b)
|
|
{
|
|
return (a > b) ? a : b;
|
|
}
|
|
|
|
public void Couch_OnCouchRemoved()
|
|
{
|
|
ResetAll();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Sending
|
|
// ---------------------------------------------------------------------------------------------
|
|
|
|
// Once per frame: every hit in a frame (all shotgun pellets, several shooters) lands here, but
|
|
// only the first schedules a send.
|
|
void QueueFlush()
|
|
{
|
|
if (g_bFlushQueued)
|
|
return;
|
|
|
|
g_bFlushQueued = true;
|
|
RequestFrame(Frame_Flush);
|
|
}
|
|
|
|
void Frame_Flush(any data)
|
|
{
|
|
g_bFlushQueued = false;
|
|
Flush();
|
|
}
|
|
|
|
void Flush()
|
|
{
|
|
if (!g_cvEnabled.BoolValue || Couch_GetCouch() == -1)
|
|
{
|
|
ResetAll();
|
|
return;
|
|
}
|
|
|
|
if (BuildHudText(g_sHudText, sizeof(g_sHudText)) == 0)
|
|
{
|
|
ClearAll();
|
|
return;
|
|
}
|
|
|
|
Send();
|
|
}
|
|
|
|
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);
|
|
|
|
for (int i = 1; i <= MaxClients; i++)
|
|
{
|
|
if (IsClientInGame(i) && !IsFakeClient(i))
|
|
ShowSyncHudText(i, g_hHudSync, "%s", g_sHudText);
|
|
}
|
|
|
|
g_bHudShown = true;
|
|
|
|
// Started here rather than from the hit, so a tick between hit and send can't stop the timer.
|
|
if (g_hExpireTimer == null)
|
|
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)
|
|
{
|
|
float fNow = GetGameTime();
|
|
bool bTaken[MAXPLAYERS + 1];
|
|
int order[MAXPLAYERS + 1];
|
|
int total = 0;
|
|
|
|
g_fNextExpiry = 0.0;
|
|
g_fLastExpiry = 0.0;
|
|
|
|
// Selection sort, most recent hit first. The soonest expiry is tracked over everyone listed,
|
|
// since a hidden player lapsing still changes the "+ N" count. A grenade tag dropping off a
|
|
// name that stays listed changes the text too, so it counts as an expiry.
|
|
for (;;)
|
|
{
|
|
int best = 0;
|
|
|
|
for (int i = 1; i <= MaxClients; i++)
|
|
{
|
|
if (bTaken[i] || g_fExpire[i] <= fNow || !IsClientInGame(i))
|
|
continue;
|
|
|
|
if (best == 0 || g_fLastHit[i] > g_fLastHit[best])
|
|
best = i;
|
|
}
|
|
|
|
if (best == 0)
|
|
break;
|
|
|
|
bTaken[best] = true;
|
|
order[total++] = best;
|
|
|
|
float fChange = g_fExpire[best];
|
|
if (g_fGrenadeUntil[best] > fNow && g_fGrenadeUntil[best] < fChange)
|
|
fChange = g_fGrenadeUntil[best];
|
|
|
|
if (g_fNextExpiry == 0.0 || fChange < g_fNextExpiry)
|
|
g_fNextExpiry = fChange;
|
|
|
|
if (g_fExpire[best] > g_fLastExpiry)
|
|
g_fLastExpiry = g_fExpire[best];
|
|
}
|
|
|
|
if (total == 0)
|
|
{
|
|
text[0] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
strcopy(text, maxlen, "Shooting couch:");
|
|
|
|
// rowStart[n] is the text length before row n, so a row can be cut back off.
|
|
int rowStart[MAXPLAYERS + 1];
|
|
int rows = 0;
|
|
|
|
for (; rows < total; rows++)
|
|
{
|
|
char sName[MAX_NAME_LENGTH];
|
|
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);
|
|
|
|
int len = strlen(text);
|
|
if (len + strlen(sLine) >= maxlen)
|
|
break;
|
|
|
|
rowStart[rows] = len;
|
|
StrCat(text, maxlen, sLine);
|
|
}
|
|
|
|
// Overflow line. A longer count can need another byte, so the fit is rechecked after every drop.
|
|
while (rows < total)
|
|
{
|
|
char sLine[16];
|
|
FormatEx(sLine, sizeof(sLine), "\n+ %d", total - rows);
|
|
|
|
if (strlen(text) + strlen(sLine) < maxlen)
|
|
{
|
|
StrCat(text, maxlen, sLine);
|
|
break;
|
|
}
|
|
|
|
rows--;
|
|
text[rowStart[rows]] = '\0';
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
// Cuts to at most maxBytes without leaving a partial multi-byte character on the end.
|
|
void TruncateUtf8(char[] text, int maxBytes)
|
|
{
|
|
if (strlen(text) <= maxBytes)
|
|
return;
|
|
|
|
int cut = maxBytes;
|
|
|
|
// Back up over continuation bytes so the whole character is dropped.
|
|
while (cut > 0 && (text[cut] & 0xC0) == 0x80)
|
|
cut--;
|
|
|
|
text[cut] = '\0';
|
|
}
|
|
|
|
public Action Timer_Expire(Handle timer)
|
|
{
|
|
if (!g_bHudShown)
|
|
{
|
|
g_hExpireTimer = null;
|
|
return Plugin_Stop;
|
|
}
|
|
|
|
// Only resend when a name has lapsed.
|
|
if (GetGameTime() >= g_fNextExpiry)
|
|
Flush();
|
|
|
|
return Plugin_Continue;
|
|
}
|
|
|
|
void ClearAll()
|
|
{
|
|
if (!g_bHudShown)
|
|
return;
|
|
|
|
for (int i = 1; i <= MaxClients; i++)
|
|
{
|
|
if (IsClientInGame(i) && !IsFakeClient(i))
|
|
ClearSyncHud(i, g_hHudSync);
|
|
}
|
|
|
|
g_bHudShown = false;
|
|
}
|
|
|
|
// The expiry timer sees g_bHudShown clear on its next tick and stops itself, so this is safe
|
|
// to call from inside it.
|
|
void ResetAll()
|
|
{
|
|
for (int i = 1; i <= MAXPLAYERS; i++)
|
|
{
|
|
g_fExpire[i] = 0.0;
|
|
g_fLastHit[i] = 0.0;
|
|
g_fGrenadeUntil[i] = 0.0;
|
|
}
|
|
|
|
ClearAll();
|
|
}
|