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);