Add extra couch features

This commit is contained in:
2026-09-29 17:36:33 +01:00
parent 598dc20ae3
commit 63c218a0e9
4 changed files with 516 additions and 45 deletions
+188 -33
View File
@@ -63,6 +63,7 @@ static const char g_sPistols[][] =
ConVar g_cvModel;
ConVar g_cvPushScale;
ConVar g_cvPistolOnly;
ConVar g_cvAllowUse;
ConVar g_cvEffect;
ConVar g_cvEffectIntensity;
ConVar g_cvIgnoreClip;
@@ -70,11 +71,13 @@ ConVar g_cvCollisionGroup;
// Optional native from the UNLOZE playtime plugin; nobody is refused if it is absent.
// The plugin reports tier -1 until a player's hours have loaded.
ConVar g_cvTierRequirement;
ConVar g_cvMoveTier;
ConVar g_cvButtonTier;
GlobalForward g_fwdOnCouchSpawned;
GlobalForward g_fwdOnCouchRemoved;
GlobalForward g_fwdOnCouchDamaged;
GlobalForward g_fwdOnCouchInteraction;
GlobalForward g_fwdOnClientBlocked;
int g_iCouchRef = INVALID_ENT_REFERENCE;
@@ -90,7 +93,7 @@ int g_iBeaconTick;
Handle g_hForceVPhysicsCollide;
// Couch bans: hits are refused in OnCouchTakeDamage, and the vphysics collision filter is
// Couch bans: hits and +use are refused in OnCouchTakeDamage/OnCouchUse, and the vphysics collision filter is
// detoured so a blocked player can't push it. The detour is only enabled while someone is blocked.
Handle g_hShouldCollideDetour;
Handle g_hCollisionRulesChanged;
@@ -123,6 +126,8 @@ public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max
CreateNative("Couch_SetEffectEnabled", Native_SetEffectEnabled);
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);
RegPluginLibrary("Couch");
@@ -137,15 +142,18 @@ public void OnPluginStart()
g_fwdOnCouchSpawned = new GlobalForward("Couch_OnCouchSpawned", ET_Ignore, Param_Cell);
g_fwdOnCouchRemoved = new GlobalForward("Couch_OnCouchRemoved", ET_Ignore);
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_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", "1.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_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 +use the couch to pick it up. 0 means it can only be moved by shooting it.", FCVAR_NOTIFY, true, 0.0, true, 1.0);
g_cvEffect = CreateConVar("sm_couch_effect", "0", "Colour cycle and pulsing light on the couch.", FCVAR_NOTIFY, true, 0.0, true, 1.0);
g_cvEffectIntensity = CreateConVar("sm_couch_effect_intensity", "3", "Brightness of the couch effect light. 0 is off, 3 is the default, 8 is very bright. Applies immediately.", FCVAR_NOTIFY, true, 0.0, true, 10.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_cvTierRequirement = CreateConVar("sm_couch_tier_requirement", "0", "Minimum playtime tier needed to shoot the couch.", FCVAR_NOTIFY, true, 0.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_cvModel.AddChangeHook(OnModelChanged);
@@ -158,13 +166,17 @@ public void OnPluginStart()
LoadGameData();
// Late load: the clip brushes already exist, so hook them now.
// Late load: the clip brushes and buttons already exist, so hook them now.
int entity = -1;
if (g_hForceVPhysicsCollide != null)
{
int entity = -1;
while ((entity = FindEntityByClassname(entity, "func_clip_vphysics")) != -1)
DHookEntity(g_hForceVPhysicsCollide, false, entity);
}
entity = -1;
while ((entity = FindEntityByClassname(entity, "func_button")) != -1)
HookButton(entity);
}
public void OnPluginEnd()
@@ -189,9 +201,11 @@ public void OnMapStart()
public void OnEntityCreated(int entity, const char[] classname)
{
// The cvar is checked in the callback so it can be flipped at runtime.
// The cvars are checked in the callbacks so they can be flipped at runtime.
if (g_hForceVPhysicsCollide != null && StrEqual(classname, "func_clip_vphysics"))
DHookEntity(g_hForceVPhysicsCollide, false, entity);
else if (StrEqual(classname, "func_button"))
HookButton(entity);
}
public void OnEntityDestroyed(int entity)
@@ -415,8 +429,9 @@ int SpawnCouchAt(const float vecOrigin[3], const float vecAngles[3])
SetEntityFlags(couch, FL_CLIENT);
SetEntProp(couch, Prop_Data, "m_iTeamNum", CS_TEAM_CT);
// SDKHooks drops the hook with the entity.
// SDKHooks drops the hooks with the entity.
SDKHook(couch, SDKHook_OnTakeDamage, OnCouchTakeDamage);
SDKHook(couch, SDKHook_Use, OnCouchUse);
// Takes the beacon's colour modulation at full opacity.
SetEntityRenderMode(couch, RENDER_NORMAL);
@@ -462,22 +477,8 @@ public Action OnCouchTakeDamage(int victim, int &attacker, int &inflictor, float
{
bool bFromClient = (attacker >= 1 && attacker <= MaxClients && IsClientInGame(attacker));
// Couch-banned: the ban plugin gets a forward so it can tell them why.
if (bFromClient && g_bBlocked[attacker])
{
Call_StartForward(g_fwdOnClientBlocked);
Call_PushCell(victim);
Call_PushCell(attacker);
Call_Finish();
if (bFromClient && IsClientRefused(victim, attacker))
return Plugin_Handled;
}
if (bFromClient && !MeetsTierRequirement(attacker))
{
PrintHintText(attacker, "You need to be tier %d or higher to shoot the couch.", g_cvTierRequirement.IntValue);
return Plugin_Handled;
}
if (bFromClient && g_cvPistolOnly.BoolValue && !IsPistolOrKnifeHit(attacker, weapon, damagetype))
return Plugin_Handled;
@@ -498,15 +499,103 @@ public Action OnCouchTakeDamage(int victim, int &attacker, int &inflictor, float
Call_PushCell(attacker);
Call_PushFloat(damage);
Call_Finish();
ReportHit(victim, attacker, inflictor, weapon, damagetype);
}
return result;
}
// Without the playtime plugin nobody is refused.
bool MeetsTierRequirement(int client)
// +use on the couch: same ban and tier rules as shooting it, and off entirely with sm_allow_couch_use 0.
// Plugin_Handled stops the pickup.
public Action OnCouchUse(int entity, int activator, int caller, UseType type, float value)
{
if (activator < 1 || activator > MaxClients || !IsClientInGame(activator))
return Plugin_Continue;
// Ban and tier first; their message is the more useful one.
if (IsClientRefused(entity, activator))
return Plugin_Handled;
if (!g_cvAllowUse.BoolValue)
{
PrintHintText(activator, "The couch can't be picked up. Shoot it to move it.");
return Plugin_Handled;
}
ReportInteraction(entity, activator, CouchMethod_Use, "");
return Plugin_Continue;
}
// Works out how an accepted hit moved the couch. Skipped when nobody listens; it runs per pellet.
void ReportHit(int couch, int attacker, int inflictor, int weapon, int damagetype)
{
if (g_fwdOnCouchInteraction.FunctionCount == 0)
return;
char sWeapon[64];
CouchMethod method = CouchMethod_Other;
if (damagetype & (DMG_SLASH | DMG_CLUB))
{
strcopy(sWeapon, sizeof(sWeapon), "knife");
method = CouchMethod_Knife;
}
else if (damagetype & DMG_BULLET)
{
GetBulletWeapon(attacker, weapon, sWeapon, sizeof(sWeapon));
ReplaceStringEx(sWeapon, sizeof(sWeapon), "weapon_", "");
method = StrEqual(sWeapon, "knife") ? CouchMethod_Knife : CouchMethod_Bullet;
}
else if (inflictor > MaxClients && IsValidEntity(inflictor))
{
// Blast from anything but a grenade (env_explosion and the like) stays Other.
GetEntityClassname(inflictor, sWeapon, sizeof(sWeapon));
if ((damagetype & DMG_BLAST) && StrEqual(sWeapon, "hegrenade_projectile"))
{
strcopy(sWeapon, sizeof(sWeapon), "hegrenade");
method = CouchMethod_Grenade;
}
}
ReportInteraction(couch, attacker, method, sWeapon);
}
void ReportInteraction(int couch, int client, CouchMethod method, const char[] sWeapon)
{
Call_StartForward(g_fwdOnCouchInteraction);
Call_PushCell(couch);
Call_PushCell(client);
Call_PushCell(method);
Call_PushString(sWeapon);
Call_Finish();
}
// Couch ban or tier requirement. Tells the client why, or for a ban lets the ban plugin do so.
bool IsClientRefused(int couch, int client)
{
if (g_bBlocked[client])
{
Call_StartForward(g_fwdOnClientBlocked);
Call_PushCell(couch);
Call_PushCell(client);
Call_Finish();
return true;
}
if (!MeetsTier(client, g_cvMoveTier.IntValue))
{
PrintHintText(client, "You need to be tier %d or higher to move the couch.", g_cvMoveTier.IntValue);
return true;
}
return false;
}
// Without the playtime plugin nobody is refused.
bool MeetsTier(int client, int required)
{
int required = g_cvTierRequirement.IntValue;
if (required <= 0)
return true;
@@ -526,15 +615,9 @@ bool IsPistolOrKnifeHit(int attacker, int weapon, int damagetype)
if (!(damagetype & DMG_BULLET))
return false;
// CS:S rarely names the weapon in bullet damage, so fall back to the active weapon.
if (weapon <= MaxClients || !IsValidEntity(weapon))
weapon = GetEntPropEnt(attacker, Prop_Send, "m_hActiveWeapon");
if (weapon == -1)
return false;
char sClass[32];
GetEntityClassname(weapon, sClass, sizeof(sClass));
if (!GetBulletWeapon(attacker, weapon, sClass, sizeof(sClass)))
return false;
// In case a knife hit ever arrives typed as a bullet.
if (StrEqual(sClass, "weapon_knife"))
@@ -549,6 +632,67 @@ bool IsPistolOrKnifeHit(int attacker, int weapon, int damagetype)
return false;
}
// Classname of the weapon behind a bullet hit. CS:S rarely names it in bullet damage, so this
// falls back to the active weapon. Returns false, with an empty buffer, if there is none.
bool GetBulletWeapon(int attacker, int weapon, char[] buffer, int maxlen)
{
if (weapon <= MaxClients || !IsValidEntity(weapon))
weapon = GetEntPropEnt(attacker, Prop_Send, "m_hActiveWeapon");
if (weapon == -1)
{
buffer[0] = '\0';
return false;
}
GetEntityClassname(weapon, buffer, maxlen);
return true;
}
// ---------------------------------------------------------------------------------------------
// Buttons
// ---------------------------------------------------------------------------------------------
// func_button spawnflag: shooting the button presses it.
#define SF_BUTTON_DAMAGE_ACTIVATES 512
void HookButton(int button)
{
SDKHook(button, SDKHook_Use, OnButtonUse);
SDKHook(button, SDKHook_OnTakeDamage, OnButtonTakeDamage);
}
// A player's own +use has them as both activator and caller. A Press input relayed from map logic
// has the firing entity as caller, so !activator chains through triggers still work.
public Action OnButtonUse(int entity, int activator, int caller, UseType type, float value)
{
if (activator < 1 || activator > MaxClients || activator != caller || !IsClientInGame(activator))
return Plugin_Continue;
return IsClientRefusedButton(activator) ? Plugin_Handled : Plugin_Continue;
}
// Only buttons that shooting presses; hits on any other func_button are left alone.
public Action OnButtonTakeDamage(int victim, int &attacker, int &inflictor, float &damage, int &damagetype, int &weapon, float damageForce[3], float damagePosition[3], int damagecustom)
{
if (attacker < 1 || attacker > MaxClients || !IsClientInGame(attacker))
return Plugin_Continue;
if (!(GetEntProp(victim, Prop_Data, "m_spawnflags") & SF_BUTTON_DAMAGE_ACTIVATES))
return Plugin_Continue;
return IsClientRefusedButton(attacker) ? Plugin_Handled : Plugin_Continue;
}
bool IsClientRefusedButton(int client)
{
if (MeetsTier(client, g_cvButtonTier.IntValue))
return false;
PrintHintText(client, "You need to be tier %d or higher to press buttons.", g_cvButtonTier.IntValue);
return true;
}
// ---------------------------------------------------------------------------------------------
// func_clip_vphysics
// ---------------------------------------------------------------------------------------------
@@ -962,6 +1106,17 @@ public any Native_SetPistolOnly(Handle plugin, int numParams)
return 0;
}
public any Native_IsUseAllowed(Handle plugin, int numParams)
{
return g_cvAllowUse.BoolValue;
}
public any Native_SetUseAllowed(Handle plugin, int numParams)
{
g_cvAllowUse.SetBool(view_as<bool>(GetNativeCell(1)));
return 0;
}
public any Native_SetClientBlocked(Handle plugin, int numParams)
{
int client = GetNativeCell(1);
+243
View File
@@ -6,9 +6,20 @@
#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
// 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.
bool g_bLogging[MAXPLAYERS + 1];
// Per player: what was last reported, and when the same thing may be reported again.
CouchMethod g_iLastMethod[MAXPLAYERS + 1];
char g_sLastWeapon[MAXPLAYERS + 1][64];
float g_fNextReport[MAXPLAYERS + 1];
public Plugin myinfo =
{
name = "Couch Admin",
@@ -26,6 +37,20 @@ public void OnPluginStart()
RegAdminCmd("sm_gotocouch", Command_GotoCouch, ADMFLAG_CHANGEMAP, "Teleports you to the couch");
RegAdminCmd("sm_removecouch", Command_RemoveCouch, ADMFLAG_CHANGEMAP, "Removes the couch");
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, grenades in chat too; no argument toggles");
}
// Game time restarts with the map.
public void OnMapStart()
{
for (int i = 1; i <= MAXPLAYERS; i++)
g_fNextReport[i] = 0.0;
}
public void OnClientDisconnect(int client)
{
g_bLogging[client] = false;
g_fNextReport[client] = 0.0;
}
public void OnAllPluginsLoaded()
@@ -108,6 +133,19 @@ public Action Command_PistolOnly(int client, int args)
return Plugin_Handled;
}
public Action Command_Log(int client, int args)
{
if (!RequireInGame(client))
return Plugin_Handled;
bool bEnable = !g_bLogging[client];
if (args >= 1)
bEnable = (GetCmdArgInt(1) != 0);
DoSetLogging(client, bEnable);
return Plugin_Handled;
}
bool RequireInGame(int client)
{
if (client >= 1 && IsClientInGame(client))
@@ -202,6 +240,124 @@ void DoSetPistolOnly(int client, bool bEnable)
LogAction(client, -1, "\"%L\" turned couch pistol-only mode %s.", client, bEnable ? "on" : "off");
}
// Announced to everyone so players know to shoot the couch instead.
void DoToggleUse(int client)
{
bool bAllow = !Couch_IsUseAllowed();
Couch_SetUseAllowed(bAllow);
if (bAllow)
ShowActivity2(client, PREFIX, "\x01Allowed pushing the \x04couch\x01 with USE.");
else
ShowActivity2(client, PREFIX, "\x01Disabled pushing the \x04couch\x01 with USE, it must be shot to move.");
LogAction(client, -1, "\"%L\" %s couch use.", client, bAllow ? "allowed" : "disallowed");
}
// 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)
{
ConVar cvar = FindTierConVar(sName);
if (cvar == null)
return;
int iTier = cvar.IntValue + iDelta;
if (iTier < 0)
iTier = 0;
cvar.IntValue = iTier;
if (iTier == 0)
{
ShowActivity2(client, PREFIX, "\x01Turned off the couch \x04%s\x01 tier requirement.", sName);
LogAction(client, -1, "\"%L\" turned off the couch %s tier requirement.", client, sName);
}
else
{
ShowActivity2(client, PREFIX, "\x01Set the couch \x04%s\x01 tier requirement to \x04%d\x01.", sName, iTier);
LogAction(client, -1, "\"%L\" set the couch %s tier requirement to %d.", client, sName, iTier);
}
}
ConVar FindTierConVar(const char[] sName)
{
char sCvar[32];
FormatEx(sCvar, sizeof(sCvar), "sm_couch_%s_tier", sName);
return FindConVar(sCvar);
}
// Only affects what this admin sees, so it is neither announced nor logged.
void DoSetLogging(int client, bool bEnable)
{
g_bLogging[client] = bEnable;
if (bEnable)
PrintToChat(client, PREFIX ... "\x01Couch log \x04on\x01, see your console for who moves the couch and how. Grenades also show here.");
else
PrintToChat(client, PREFIX ... "\x01Couch log \x04off\x01.");
}
// ---------------------------------------------------------------------------------------------
// Couch log
// ---------------------------------------------------------------------------------------------
public void Couch_OnCouchInteraction(int couch, int client, CouchMethod method, const char[] weapon)
{
if (!AnyoneLogging())
return;
// Held fire and shotgun pellets would flood the console; a change of weapon or method is always shown.
float fNow = GetGameTime();
if (fNow < g_fNextReport[client] && method == g_iLastMethod[client] && StrEqual(weapon, g_sLastWeapon[client]))
return;
g_iLastMethod[client] = method;
strcopy(g_sLastWeapon[client], sizeof(g_sLastWeapon[]), weapon);
g_fNextReport[client] = fNow + LOG_REPEAT_INTERVAL;
// Plain text: the console doesn't render chat colour codes.
char sHow[96];
switch (method)
{
case CouchMethod_Bullet:
{
if (weapon[0] != '\0')
FormatEx(sHow, sizeof(sHow), "shot the couch with %s", weapon);
else
strcopy(sHow, sizeof(sHow), "shot the couch");
}
case CouchMethod_Knife: strcopy(sHow, sizeof(sHow), "knifed the couch");
case CouchMethod_Grenade: strcopy(sHow, sizeof(sHow), "hit the couch with a grenade");
case CouchMethod_Use: strcopy(sHow, sizeof(sHow), "pushed the couch with USE");
default: FormatEx(sHow, sizeof(sHow), "moved the couch with %s", weapon[0] != '\0' ? weapon : "something");
}
// Everything goes to the console; grenades are rare and knock the couch far, so they go to chat too.
// The userid is there so it can go straight into sm_couchban.
int userid = GetClientUserId(client);
for (int i = 1; i <= MaxClients; i++)
{
if (!g_bLogging[i] || !IsClientInGame(i))
continue;
PrintToConsole(i, "[Couch] %N (#%d) %s", client, userid, sHow);
if (method == CouchMethod_Grenade)
PrintToChat(i, "\x01[Couch] \x04%N\x01 (#%d) hit the couch with a \x04grenade", client, userid);
}
}
bool AnyoneLogging()
{
for (int i = 1; i <= MaxClients; i++)
{
if (g_bLogging[i])
return true;
}
return false;
}
// ---------------------------------------------------------------------------------------------
// Menu
// ---------------------------------------------------------------------------------------------
@@ -234,6 +390,14 @@ void ShowCouchMenu(int client)
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), "Log to my console: %s", g_bLogging[client] ? "ON" : "OFF");
menu.AddItem("log", sItem);
menu.AddItem("tiers", "Tier requirements");
menu.ExitButton = true;
menu.Display(client, MENU_TIME_FOREVER);
}
@@ -267,6 +431,15 @@ public int MenuHandler_Couch(Menu menu, MenuAction action, int param1, int param
DoToggleEffect(client);
else if (StrEqual(sInfo, "pistol"))
DoSetPistolOnly(client, !Couch_IsPistolOnly());
else if (StrEqual(sInfo, "use"))
DoToggleUse(client);
else if (StrEqual(sInfo, "log"))
DoSetLogging(client, !g_bLogging[client]);
else if (StrEqual(sInfo, "tiers"))
{
ShowTierMenu(client);
return 0;
}
ShowCouchMenu(client);
}
@@ -279,3 +452,73 @@ public int MenuHandler_Couch(Menu menu, MenuAction action, int param1, int param
return 0;
}
void ShowTierMenu(int client)
{
Menu menu = new Menu(MenuHandler_Tier);
menu.SetTitle("Couch - Tier requirements");
AddTierItems(menu, "move", "Move");
AddTierItems(menu, "button", "Button");
menu.ExitBackButton = true;
menu.ExitButton = true;
menu.Display(client, MENU_TIME_FOREVER);
}
// Adds "<label> tier: <value>", increase and decrease. Info strings are "+move", "-move" and so on.
void AddTierItems(Menu menu, const char[] sName, const char[] sLabel)
{
ConVar cvar = FindTierConVar(sName);
int iTier = (cvar == null) ? 0 : cvar.IntValue;
char sItem[64], sInfo[16];
if (iTier > 0)
FormatEx(sItem, sizeof(sItem), "%s tier: %d", sLabel, iTier);
else
FormatEx(sItem, sizeof(sItem), "%s tier: OFF", sLabel);
menu.AddItem("", sItem, ITEMDRAW_DISABLED);
FormatEx(sInfo, sizeof(sInfo), "+%s", sName);
FormatEx(sItem, sizeof(sItem), "Increase %s tier", sName);
menu.AddItem(sInfo, sItem, (cvar == null) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
FormatEx(sInfo, sizeof(sInfo), "-%s", sName);
FormatEx(sItem, sizeof(sItem), "Decrease %s tier", sName);
menu.AddItem(sInfo, sItem, (iTier > 0) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED);
}
public int MenuHandler_Tier(Menu menu, MenuAction action, int param1, int param2)
{
switch (action)
{
case MenuAction_Select:
{
int client = param1;
char sInfo[16];
menu.GetItem(param2, sInfo, sizeof(sInfo));
DoChangeTier(client, sInfo[1], (sInfo[0] == '+') ? 1 : -1);
ShowTierMenu(client);
}
case MenuAction_Cancel:
{
if (param2 == MenuCancel_ExitBack)
{
if (g_bCoreLoaded)
ShowCouchMenu(param1);
else
RequireCore(param1);
}
}
case MenuAction_End:
{
delete menu;
}
}
return 0;
}
+51 -11
View File
@@ -11,6 +11,9 @@
// 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
@@ -19,14 +22,17 @@
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_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 among the names currently on screen.
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.
@@ -47,6 +53,7 @@ public void OnPluginStart()
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);
@@ -60,11 +67,13 @@ public void OnMapStart()
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;
}
}
@@ -72,6 +81,7 @@ 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)
@@ -80,6 +90,7 @@ public void OnClientDisconnect(int client)
g_fExpire[client] = 0.0;
g_fLastHit[client] = 0.0;
g_fGrenadeUntil[client] = 0.0;
if (bListed)
QueueFlush();
@@ -106,13 +117,31 @@ 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] = fNow + g_cvTime.FloatValue;
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();
@@ -158,8 +187,9 @@ void Flush()
void Send()
{
// Hold time matches the listing time, so the text leaves on its own if no update ever comes.
SetHudTextParams(0.8, 0.2, g_cvTime.FloatValue, 255, 255, 255, 255, 0, 0.0, 0.0, 0.0);
// 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++)
{
@@ -184,9 +214,11 @@ int BuildHudText(char[] text, int maxlen)
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.
// 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;
@@ -206,8 +238,15 @@ int BuildHudText(char[] text, int maxlen)
bTaken[best] = true;
order[total++] = best;
if (g_fNextExpiry == 0.0 || g_fExpire[best] < g_fNextExpiry)
g_fNextExpiry = g_fExpire[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)
@@ -228,8 +267,8 @@ int BuildHudText(char[] text, int maxlen)
GetClientName(order[rows], sName, sizeof(sName));
TruncateUtf8(sName, HUD_NAME_BYTES);
char sLine[HUD_NAME_BYTES + 2];
FormatEx(sLine, sizeof(sLine), "\n%s", sName);
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)
@@ -310,6 +349,7 @@ void ResetAll()
{
g_fExpire[i] = 0.0;
g_fLastHit[i] = 0.0;
g_fGrenadeUntil[i] = 0.0;
}
ClearAll();
+34 -1
View File
@@ -4,6 +4,16 @@
#define _Couch_included
// How a player moved the couch, passed to Couch_OnCouchInteraction.
enum CouchMethod
{
CouchMethod_Bullet, // Gunfire; weapon is the gun, e.g. "ak47".
CouchMethod_Knife, // weapon is "knife".
CouchMethod_Grenade, // weapon is "hegrenade".
CouchMethod_Use, // +use; weapon is empty.
CouchMethod_Other // Any other damage credited to a player; weapon is the inflictor's classname.
};
// The core defines COUCH_CORE; it creates the forwards and natives itself.
#if !defined COUCH_CORE
@@ -30,7 +40,18 @@ forward void Couch_OnCouchRemoved();
forward void Couch_OnCouchDamaged(int couch, int attacker, float damage);
/**
* Called when a hit from a blocked client is refused. Fires per pellet, so rate-limit anything you send.
* Called for every hit or +use the couch accepts from a player, saying how it was moved. Hits
* fire alongside Couch_OnCouchDamaged, so shotguns report once per pellet.
*
* @param couch Entity index of the couch.
* @param client Client index of the player.
* @param method How the couch was moved.
* @param weapon Weapon without its "weapon_" prefix, see CouchMethod.
*/
forward void Couch_OnCouchInteraction(int couch, int client, CouchMethod method, const char[] weapon);
/**
* Called when a hit or +use from a blocked client is refused. Fires per pellet, so rate-limit anything you send.
*
* @param couch Entity index of the couch.
* @param client Client index of the blocked player.
@@ -103,6 +124,16 @@ native bool Couch_IsPistolOnly();
*/
native void Couch_SetPistolOnly(bool enabled);
/**
* Whether players may +use the couch to pick it up. Mirrors sm_allow_couch_use.
*/
native bool Couch_IsUseAllowed();
/**
* Allows or forbids picking the couch up with +use; when forbidden it can only be shot. Writes sm_allow_couch_use.
*/
native void Couch_SetUseAllowed(bool allowed);
/**
* Blocks or unblocks a client from the couch: their hits are refused and their body can't push
* it. The flag is per slot and cleared on disconnect and map start; the caller owns persistence.
@@ -146,6 +177,8 @@ public SharedPlugin __pl_Couch =
MarkNativeAsOptional("Couch_SetEffectEnabled");
MarkNativeAsOptional("Couch_IsPistolOnly");
MarkNativeAsOptional("Couch_SetPistolOnly");
MarkNativeAsOptional("Couch_IsUseAllowed");
MarkNativeAsOptional("Couch_SetUseAllowed");
MarkNativeAsOptional("Couch_SetClientBlocked");
MarkNativeAsOptional("Couch_IsClientBlocked");
}