Add Couch plugins

This commit is contained in:
2026-09-29 02:24:47 +01:00
parent 41f86102e5
commit 867b3bfbba
6 changed files with 1882 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
"Games"
{
"cstrike"
{
"Offsets"
{
// CBaseEntity::ForceVPhysicsCollide(CBaseEntity *pEntity). Same offsets as PropSpawner.
"ForceVPhysicsCollide"
{
"linux" "160"
"windows" "159"
}
}
"Signatures"
{
// CCollisionEvent::ShouldCollide(IPhysicsObject *, IPhysicsObject *, void *, void *).
// The vphysics collision filter, detoured for couch bans. Local symbol from .symtab,
// so the server binary must be unstripped (as for LagCompensation).
"CCollisionEvent::ShouldCollide"
{
"library" "server"
"linux" "@_ZN15CCollisionEvent13ShouldCollideEP14IPhysicsObjectS1_PvS2_"
}
// CBaseEntity::CollisionRulesChanged(). Makes vphysics re-ask ShouldCollide for cached pairs.
"CBaseEntity::CollisionRulesChanged"
{
"library" "server"
"linux" "@_ZN11CBaseEntity21CollisionRulesChangedEv"
}
}
}
}
+892
View File
@@ -0,0 +1,892 @@
#include <sourcemod>
#include <sdktools>
#include <sdkhooks>
#include <cstrike>
#include <dhooks>
#include <unloze_playtime>
#define COUCH_CORE
#include <Couch>
#pragma semicolon 1
#pragma newdecls required
#define COUCH_DEFAULT_MODEL "models/props/cs_office/sofa.mdl"
// Lifted off the traced surface; vphysics settles it.
#define COUCH_Z_OFFSET 10.0
// Fixed so it can be addressed with ent_fire.
#define COUCH_TARGETNAME "ze_couch"
// Delay after the round-start respawn before the announcement.
#define COUCH_ANNOUNCE_DELAY 1.0
// game_text announcement
#define COUCH_TEXT_CHANNEL "3"
#define COUCH_TEXT_Y "0.1"
#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
#define COUCH_COLOUR_TICKS 4
#define COUCH_PULSE_TICKS 10
// light_dynamic parented to the couch; the pulse is its radius breathing between these.
#define COUCH_LIGHT_HEIGHT 40.0
#define COUCH_LIGHT_RADIUS_MIN 150.0
#define COUCH_LIGHT_RADIUS_MAX 320.0
static const int g_iCouchColours[][3] =
{
{ 255, 48, 48 }, // red
{ 255, 144, 32 }, // orange
{ 255, 240, 64 }, // yellow
{ 64, 255, 96 }, // green
{ 64, 192, 255 }, // cyan
{ 128, 96, 255 }, // violet
{ 255, 80, 224 }, // magenta
};
// Allowed to move the couch in pistol-only mode, along with the knife.
static const char g_sPistols[][] =
{
"weapon_glock",
"weapon_usp",
"weapon_p228",
"weapon_deagle",
"weapon_elite",
"weapon_fiveseven",
};
ConVar g_cvModel;
ConVar g_cvPushScale;
ConVar g_cvPistolOnly;
ConVar g_cvEffect;
ConVar g_cvIgnoreClip;
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;
GlobalForward g_fwdOnCouchSpawned;
GlobalForward g_fwdOnCouchRemoved;
GlobalForward g_fwdOnCouchDamaged;
GlobalForward g_fwdOnClientBlocked;
int g_iCouchRef = INVALID_ENT_REFERENCE;
int g_iLightRef = INVALID_ENT_REFERENCE;
// Admin placement; the couch respawns here every round start. sm_bringcouch does not move it.
bool g_bPlaced;
float g_vecPlaceOrigin[3];
float g_vecPlaceAngles[3];
Handle g_hBeaconTimer;
int g_iBeaconTick;
Handle g_hForceVPhysicsCollide;
// Couch bans: hits are refused in OnCouchTakeDamage, 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;
bool g_bBlocked[MAXPLAYERS + 1];
int g_iBlockedCount;
bool g_bDetourEnabled;
public Plugin myinfo =
{
name = "Couch",
author = "marisakirisame.net",
description = "ZE couch: spawns, respawns and lights the couch",
version = "2.0",
url = "https://marisakirisame.net"
};
// ---------------------------------------------------------------------------------------------
// Plugin lifecycle
// ---------------------------------------------------------------------------------------------
public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
CreateNative("Couch_GetCouch", Native_GetCouch);
CreateNative("Couch_IsPlaced", Native_IsPlaced);
CreateNative("Couch_Spawn", Native_Spawn);
CreateNative("Couch_Bring", Native_Bring);
CreateNative("Couch_Remove", Native_Remove);
CreateNative("Couch_IsEffectEnabled", Native_IsEffectEnabled);
CreateNative("Couch_SetEffectEnabled", Native_SetEffectEnabled);
CreateNative("Couch_IsPistolOnly", Native_IsPistolOnly);
CreateNative("Couch_SetPistolOnly", Native_SetPistolOnly);
CreateNative("Couch_SetClientBlocked", Native_SetClientBlocked);
CreateNative("Couch_IsClientBlocked", Native_IsClientBlocked);
RegPluginLibrary("Couch");
MarkNativeAsOptional("GetPlayerTier_native");
return APLRes_Success;
}
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_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_cvEffect = CreateConVar("sm_couch_effect", "0", "Colour cycle and pulsing light on the couch.", FCVAR_NOTIFY, true, 0.0, true, 1.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_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);
g_cvEffect.AddChangeHook(OnEffectChanged);
AutoExecConfig(true, "Couch");
HookEvent("round_start", Event_RoundStart, EventHookMode_Post);
LoadGameData();
// Late load: the clip brushes already exist, so hook them now.
if (g_hForceVPhysicsCollide != null)
{
int entity = -1;
while ((entity = FindEntityByClassname(entity, "func_clip_vphysics")) != -1)
DHookEntity(g_hForceVPhysicsCollide, false, entity);
}
}
public void OnPluginEnd()
{
// Don't leave a couch with a dead damage hook and an orphaned light behind.
KillCouch();
}
public void OnMapStart()
{
PrecacheCouchModel();
// Entities and no-mapchange timers went with the map.
g_iCouchRef = INVALID_ENT_REFERENCE;
g_iLightRef = INVALID_ENT_REFERENCE;
g_bPlaced = false;
g_hBeaconTimer = null;
g_iBeaconTick = 0;
ResetBlockedClients();
}
public void OnEntityCreated(int entity, const char[] classname)
{
// The cvar is checked in the callback so it can be flipped at runtime.
if (g_hForceVPhysicsCollide != null && StrEqual(classname, "func_clip_vphysics"))
DHookEntity(g_hForceVPhysicsCollide, false, entity);
}
public void OnEntityDestroyed(int entity)
{
if (g_iCouchRef == INVALID_ENT_REFERENCE || entity != EntRefToEntIndex(g_iCouchRef))
return;
// Removed by something else (round restart purge, trigger_remove, ent_fire). Stays gone
// until the next round start.
g_iCouchRef = INVALID_ENT_REFERENCE;
KillLight();
StopBeacon();
Call_StartForward(g_fwdOnCouchRemoved);
Call_Finish();
}
// Slot flag only; persistence across reconnects is the ban plugin's job.
public void OnClientDisconnect(int client)
{
if (g_bBlocked[client])
SetClientBlocked(client, false);
}
public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
if (!g_bPlaced || GetCouchIndex() != -1)
return;
if (SpawnCouchAt(g_vecPlaceOrigin, g_vecPlaceAngles) == -1)
return;
// Delayed so the round-start HUD reset doesn't wipe it.
CreateTimer(COUCH_ANNOUNCE_DELAY, Timer_AnnounceCouchRound, _, TIMER_FLAG_NO_MAPCHANGE);
}
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.");
return Plugin_Stop;
}
// game_text to everyone, killed straight after Display; the message has already gone out.
void ShowCouchRoundText()
{
int text = CreateEntityByName("game_text");
if (text == -1)
return;
DispatchKeyValue(text, "targetname", COUCH_TARGETNAME ... "_text");
DispatchKeyValue(text, "spawnflags", "1"); // All players.
DispatchKeyValue(text, "channel", COUCH_TEXT_CHANNEL);
DispatchKeyValue(text, "x", "-1"); // Centred.
DispatchKeyValue(text, "y", COUCH_TEXT_Y);
DispatchKeyValue(text, "effect", "0"); // Fade in and out.
DispatchKeyValue(text, "color", COUCH_TEXT_COLOUR);
DispatchKeyValue(text, "color2", "255 255 255");
DispatchKeyValue(text, "fadein", "0.5");
DispatchKeyValue(text, "fadeout", "3.0");
DispatchKeyValue(text, "holdtime", COUCH_TEXT_HOLD);
DispatchKeyValue(text, "fxtime", "0.25");
DispatchKeyValue(text, "message", "COUCH ROUND!\nHumans must bring the couch with them\nthroughout the map else they will fail the round.");
DispatchSpawn(text);
AcceptEntityInput(text, "Display");
AcceptEntityInput(text, "Kill");
}
// ---------------------------------------------------------------------------------------------
// Model
// ---------------------------------------------------------------------------------------------
public void OnModelChanged(ConVar convar, const char[] oldValue, const char[] newValue)
{
PrecacheCouchModel();
}
void PrecacheCouchModel()
{
char sModel[PLATFORM_MAX_PATH];
GetCouchModel(sModel, sizeof(sModel));
PrecacheModel(sModel, true);
}
// Falls back to the default if the cvar's model doesn't exist; spawning with a missing model is a hard error.
void GetCouchModel(char[] buffer, int maxlen)
{
g_cvModel.GetString(buffer, maxlen);
if (buffer[0] != '\0' && FileExists(buffer, true))
return;
if (buffer[0] != '\0')
LogError("sm_couch_model \"%s\" does not exist, using %s", buffer, COUCH_DEFAULT_MODEL);
strcopy(buffer, maxlen, COUCH_DEFAULT_MODEL);
}
// ---------------------------------------------------------------------------------------------
// Couch entity
// ---------------------------------------------------------------------------------------------
int GetCouchIndex()
{
if (g_iCouchRef == INVALID_ENT_REFERENCE)
return -1;
int couch = EntRefToEntIndex(g_iCouchRef);
if (couch == INVALID_ENT_REFERENCE || !IsValidEntity(couch))
return -1;
return couch;
}
int GetLightIndex()
{
if (g_iLightRef == INVALID_ENT_REFERENCE)
return -1;
int light = EntRefToEntIndex(g_iLightRef);
if (light == INVALID_ENT_REFERENCE || !IsValidEntity(light))
return -1;
return light;
}
// Crosshair hit position: upright, facing the client's way, lifted off the surface.
void TraceCrosshair(int client, float vecOrigin[3], float vecAngles[3])
{
GetClientEyePosition(client, vecOrigin);
GetClientEyeAngles(client, vecAngles);
TR_TraceRayFilter(vecOrigin, vecAngles, MASK_SOLID, RayType_Infinite, TraceFilter_NotSelfOrCouch, client);
TR_GetEndPosition(vecOrigin);
vecOrigin[2] += COUCH_Z_OFFSET;
vecAngles[0] = 0.0;
vecAngles[2] = 0.0;
}
// Skips the couch too, so bringing it while looking at it doesn't stack it on itself.
bool TraceFilter_NotSelfOrCouch(int entity, int contentsMask, any client)
{
return entity != client && entity != GetCouchIndex();
}
// The only place the couch is created. Returns the entity or -1.
int SpawnCouchAt(const float vecOrigin[3], const float vecAngles[3])
{
int couch = CreateEntityByName("prop_physics_override");
if (couch == -1)
return -1;
char sModel[PLATFORM_MAX_PATH];
GetCouchModel(sModel, sizeof(sModel));
if (!IsModelPrecached(sModel))
PrecacheModel(sModel, true);
DispatchKeyValue(couch, "targetname", COUCH_TARGETNAME);
DispatchKeyValue(couch, "model", sModel);
SetEntProp(couch, Prop_Data, "m_CollisionGroup", g_cvCollisionGroup.IntValue);
TeleportEntity(couch, vecOrigin, vecAngles, NULL_VECTOR);
DispatchSpawn(couch);
// FL_CLIENT and a team let the couch trip "Clients only" and team-filtered triggers.
SetEntityFlags(couch, FL_CLIENT);
SetEntProp(couch, Prop_Data, "m_iTeamNum", CS_TEAM_CT);
// SDKHooks drops the hook with the entity.
SDKHook(couch, SDKHook_OnTakeDamage, OnCouchTakeDamage);
// Takes the beacon's colour modulation at full opacity.
SetEntityRenderMode(couch, RENDER_NORMAL);
g_iCouchRef = EntIndexToEntRef(couch);
ApplyEffectState();
Call_StartForward(g_fwdOnCouchSpawned);
Call_PushCell(couch);
Call_Finish();
return couch;
}
// Returns true if there was a couch to remove.
bool KillCouch()
{
int couch = GetCouchIndex();
// Cleared first so OnEntityDestroyed doesn't run the external-removal path too.
g_iCouchRef = INVALID_ENT_REFERENCE;
StopBeacon();
KillLight();
if (couch == -1)
return false;
AcceptEntityInput(couch, "Kill");
Call_StartForward(g_fwdOnCouchRemoved);
Call_Finish();
return true;
}
// ---------------------------------------------------------------------------------------------
// Damage
// ---------------------------------------------------------------------------------------------
// Plugin_Handled skips the engine's impulse too, so a refused hit doesn't nudge the couch.
public Action OnCouchTakeDamage(int victim, int &attacker, int &inflictor, float &damage, int &damagetype, int &weapon, float damageForce[3], float damagePosition[3], int damagecustom)
{
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();
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;
Action result = Plugin_Continue;
float fScale = g_cvPushScale.FloatValue;
if (fScale != 1.0)
{
ScaleVector(damageForce, fScale);
result = Plugin_Changed;
}
if (bFromClient)
{
Call_StartForward(g_fwdOnCouchDamaged);
Call_PushCell(victim);
Call_PushCell(attacker);
Call_PushFloat(damage);
Call_Finish();
}
return result;
}
// Without the playtime plugin nobody is refused.
bool MeetsTierRequirement(int client)
{
int required = g_cvTierRequirement.IntValue;
if (required <= 0)
return true;
if (GetFeatureStatus(FeatureType_Native, "GetPlayerTier_native") != FeatureStatus_Available)
return true;
return GetPlayerTier_native(client) >= required;
}
bool IsPistolOrKnifeHit(int attacker, int weapon, int damagetype)
{
// Knife is slash/club; nothing else a player carries deals either.
if (damagetype & (DMG_SLASH | DMG_CLUB))
return true;
// Grenades are blast, so only bullets from here on.
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));
// In case a knife hit ever arrives typed as a bullet.
if (StrEqual(sClass, "weapon_knife"))
return true;
for (int i = 0; i < sizeof(g_sPistols); i++)
{
if (StrEqual(sClass, g_sPistols[i]))
return true;
}
return false;
}
// ---------------------------------------------------------------------------------------------
// func_clip_vphysics
// ---------------------------------------------------------------------------------------------
void LoadGameData()
{
GameData gd = new GameData("Couch");
if (gd == null)
{
LogError("Missing gamedata/Couch.txt; the couch will collide with func_clip_vphysics and couch-banned players can still push it.");
return;
}
int offset = gd.GetOffset("ForceVPhysicsCollide");
if (offset == -1)
{
LogError("Missing \"ForceVPhysicsCollide\" offset in gamedata/Couch.txt; the couch will collide with func_clip_vphysics.");
}
else
{
g_hForceVPhysicsCollide = DHookCreate(offset, HookType_Entity, ReturnType_Bool, ThisPointer_CBaseEntity, Hook_ForceVPhysicsCollide);
DHookAddParam(g_hForceVPhysicsCollide, HookParamType_CBaseEntity);
}
// Collision filter detour for couch bans; enabled only while someone is blocked.
g_hShouldCollideDetour = DHookCreateDetour(Address_Null, CallConv_THISCALL, ReturnType_Int, ThisPointer_Address);
if (!DHookSetFromConf(g_hShouldCollideDetour, gd, SDKConf_Signature, "CCollisionEvent::ShouldCollide"))
{
LogError("Missing \"CCollisionEvent::ShouldCollide\" signature in gamedata/Couch.txt; couch-banned players can still push the couch.");
delete g_hShouldCollideDetour;
}
else
{
DHookAddParam(g_hShouldCollideDetour, HookParamType_Int); // IPhysicsObject *pObj0
DHookAddParam(g_hShouldCollideDetour, HookParamType_Int); // IPhysicsObject *pObj1
DHookAddParam(g_hShouldCollideDetour, HookParamType_CBaseEntity); // void *pGameData0: entity behind pObj0
DHookAddParam(g_hShouldCollideDetour, HookParamType_CBaseEntity); // void *pGameData1: entity behind pObj1
}
// Makes vphysics re-ask ShouldCollide for existing pairs, so a ban applies while the player
// is already touching the couch.
StartPrepSDKCall(SDKCall_Entity);
if (PrepSDKCall_SetFromConf(gd, SDKConf_Signature, "CBaseEntity::CollisionRulesChanged"))
g_hCollisionRulesChanged = EndPrepSDKCall();
if (g_hCollisionRulesChanged == null)
LogError("Missing \"CBaseEntity::CollisionRulesChanged\" signature in gamedata/Couch.txt; couch bans stop pushing only once the player and couch separate.");
delete gd;
}
// Lets the couch pass through func_clip_vphysics.
public MRESReturn Hook_ForceVPhysicsCollide(int pThis, Handle hReturn, Handle hParams)
{
if (!g_cvIgnoreClip.BoolValue)
return MRES_Ignored;
int entity = DHookGetParam(hParams, 1);
if (entity == -1 || entity != GetCouchIndex())
return MRES_Ignored;
DHookSetReturn(hReturn, false);
return MRES_Supercede;
}
// ---------------------------------------------------------------------------------------------
// Couch ban
// ---------------------------------------------------------------------------------------------
void SetClientBlocked(int client, bool blocked)
{
if (g_bBlocked[client] == blocked)
return;
g_bBlocked[client] = blocked;
g_iBlockedCount += blocked ? 1 : -1;
// Detour goes on before the recheck so vphysics sees the new rule.
if (g_iBlockedCount > 0)
EnableCollisionDetour();
else
DisableCollisionDetour();
RecheckCouchCollision(client);
}
void ResetBlockedClients()
{
for (int i = 1; i <= MAXPLAYERS; i++)
g_bBlocked[i] = false;
g_iBlockedCount = 0;
DisableCollisionDetour();
}
void EnableCollisionDetour()
{
if (g_hShouldCollideDetour == null || g_bDetourEnabled)
return;
if (!DHookEnableDetour(g_hShouldCollideDetour, false, Detour_ShouldCollide))
{
LogError("Failed to detour CCollisionEvent::ShouldCollide; couch-banned players can still push the couch.");
return;
}
g_bDetourEnabled = true;
}
void DisableCollisionDetour()
{
if (!g_bDetourEnabled)
return;
DHookDisableDetour(g_hShouldCollideDetour, false, Detour_ShouldCollide);
g_bDetourEnabled = false;
}
// vphysics caches the ShouldCollide answer per pair until the objects separate, so both halves
// of the pair are rechecked.
void RecheckCouchCollision(int client)
{
if (g_hCollisionRulesChanged == null)
return;
int couch = GetCouchIndex();
if (couch != -1)
SDKCall(g_hCollisionRulesChanged, couch);
if (client >= 1 && client <= MaxClients && IsClientInGame(client))
SDKCall(g_hCollisionRulesChanged, client);
}
// A blocked player's physics shadow never touches the couch. Movement traces are separate, so
// the couch stays solid to them: they can stand on it, just not push it.
public MRESReturn Detour_ShouldCollide(Address pThis, Handle hReturn, Handle hParams)
{
int couch = GetCouchIndex();
if (couch == -1)
return MRES_Ignored;
// -1 when the physics object has no entity behind it.
int ent0 = DHookGetParam(hParams, 3);
int ent1 = DHookGetParam(hParams, 4);
int other;
if (ent0 == couch)
other = ent1;
else if (ent1 == couch)
other = ent0;
else
return MRES_Ignored;
if (other < 1 || other > MaxClients || !g_bBlocked[other])
return MRES_Ignored;
DHookSetReturn(hReturn, 0);
return MRES_Supercede;
}
// ---------------------------------------------------------------------------------------------
// Effect
// ---------------------------------------------------------------------------------------------
public void OnEffectChanged(ConVar convar, const char[] oldValue, const char[] newValue)
{
ApplyEffectState();
}
// Brings the light and beacon in line with sm_couch_effect. Safe to call at any time.
void ApplyEffectState()
{
int couch = GetCouchIndex();
if (couch == -1 || !g_cvEffect.BoolValue)
{
StopBeacon();
KillLight();
if (couch != -1)
SetEntityRenderColor(couch, 255, 255, 255, 255);
return;
}
if (GetLightIndex() == -1)
g_iLightRef = SpawnCouchLight(couch);
StartBeacon();
}
// light_dynamic parented to the couch. Returns INVALID_ENT_REFERENCE on failure.
int SpawnCouchLight(int couch)
{
int light = CreateEntityByName("light_dynamic");
if (light == -1)
return INVALID_ENT_REFERENCE;
DispatchKeyValue(light, "targetname", COUCH_TARGETNAME ... "_light");
DispatchKeyValue(light, "spawnflags", "0"); // Light the world and models both.
DispatchKeyValue(light, "style", "0");
DispatchKeyValue(light, "pitch", "-90");
DispatchKeyValue(light, "angles", "0 0 0");
DispatchKeyValue(light, "_light", "255 255 255 255");
DispatchKeyValue(light, "brightness", "3");
DispatchKeyValue(light, "distance", "130");
DispatchKeyValue(light, "spotlight_radius", "70");
// Zero cones make it a point light, so it doesn't swing off when the couch tips over.
DispatchKeyValue(light, "_inner_cone", "0");
DispatchKeyValue(light, "_cone", "0");
float vecOrigin[3];
GetEntPropVector(couch, Prop_Send, "m_vecOrigin", vecOrigin);
vecOrigin[2] += COUCH_LIGHT_HEIGHT;
TeleportEntity(light, vecOrigin, NULL_VECTOR, NULL_VECTOR);
DispatchSpawn(light);
ActivateEntity(light);
// Parenting keeps the position set above.
SetVariantString("!activator");
AcceptEntityInput(light, "SetParent", couch, couch);
return EntIndexToEntRef(light);
}
// Source doesn't remove children with their parent.
void KillLight()
{
int light = GetLightIndex();
g_iLightRef = INVALID_ENT_REFERENCE;
if (light != -1)
AcceptEntityInput(light, "Kill");
}
void StartBeacon()
{
if (g_hBeaconTimer == null)
g_hBeaconTimer = CreateTimer(COUCH_BEACON_INTERVAL, Timer_Beacon, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);
}
// Not called from inside Timer_Beacon; that returns Plugin_Stop instead.
void StopBeacon()
{
delete g_hBeaconTimer;
}
public Action Timer_Beacon(Handle timer)
{
int couch = GetCouchIndex();
if (couch == -1)
{
g_hBeaconTimer = null;
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)
return Plugin_Continue;
SetEntityRenderColor(light, r, g, b, 255);
// 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;
SetVariantFloat(COUCH_LIGHT_RADIUS_MIN + fPulse * (COUCH_LIGHT_RADIUS_MAX - COUCH_LIGHT_RADIUS_MIN));
AcceptEntityInput(light, "distance");
return Plugin_Continue;
}
// ---------------------------------------------------------------------------------------------
// Natives
// ---------------------------------------------------------------------------------------------
public any Native_GetCouch(Handle plugin, int numParams)
{
return GetCouchIndex();
}
public any Native_IsPlaced(Handle plugin, int numParams)
{
return g_bPlaced;
}
public any Native_Spawn(Handle plugin, int numParams)
{
int client = GetNativeCell(1);
if (client < 1 || client > MaxClients || !IsClientInGame(client))
return ThrowNativeError(SP_ERROR_NATIVE, "Client %d is not in game", client);
float vecOrigin[3];
float vecAngles[3];
TraceCrosshair(client, vecOrigin, vecAngles);
// Replaced rather than moved so a changed sm_couch_model applies.
KillCouch();
int couch = SpawnCouchAt(vecOrigin, vecAngles);
if (couch == -1)
return -1;
g_bPlaced = true;
g_vecPlaceOrigin = vecOrigin;
g_vecPlaceAngles = vecAngles;
return couch;
}
public any Native_Bring(Handle plugin, int numParams)
{
int client = GetNativeCell(1);
if (client < 1 || client > MaxClients || !IsClientInGame(client))
return ThrowNativeError(SP_ERROR_NATIVE, "Client %d is not in game", client);
int couch = GetCouchIndex();
if (couch == -1)
return false;
float vecOrigin[3];
float vecAngles[3];
TraceCrosshair(client, vecOrigin, vecAngles);
// Arrives still, then woken so vphysics drops it onto the surface. The light follows its parent.
float vecStill[3];
TeleportEntity(couch, vecOrigin, vecAngles, vecStill);
AcceptEntityInput(couch, "Wake");
// Not recorded as the placement; next round it returns to where it was spawned.
return true;
}
public any Native_Remove(Handle plugin, int numParams)
{
bool bHadSomething = g_bPlaced || GetCouchIndex() != -1;
KillCouch();
g_bPlaced = false;
return bHadSomething;
}
public any Native_IsEffectEnabled(Handle plugin, int numParams)
{
return g_cvEffect.BoolValue;
}
// The cvar's change hook applies the state, so console and menu do the same thing.
public any Native_SetEffectEnabled(Handle plugin, int numParams)
{
g_cvEffect.SetBool(view_as<bool>(GetNativeCell(1)));
return 0;
}
public any Native_IsPistolOnly(Handle plugin, int numParams)
{
return g_cvPistolOnly.BoolValue;
}
public any Native_SetPistolOnly(Handle plugin, int numParams)
{
g_cvPistolOnly.SetBool(view_as<bool>(GetNativeCell(1)));
return 0;
}
public any Native_SetClientBlocked(Handle plugin, int numParams)
{
int client = GetNativeCell(1);
if (client < 1 || client > MaxClients || !IsClientInGame(client))
return ThrowNativeError(SP_ERROR_NATIVE, "Client %d is not in game", client);
SetClientBlocked(client, view_as<bool>(GetNativeCell(2)));
return 0;
}
public any Native_IsClientBlocked(Handle plugin, int numParams)
{
int client = GetNativeCell(1);
if (client < 1 || client > MaxClients)
return false;
return g_bBlocked[client];
}
+250
View File
@@ -0,0 +1,250 @@
#include <sourcemod>
#include <Couch>
#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;
public Plugin myinfo =
{
name = "Couch Admin",
author = "marisakirisame.net",
description = "Admin commands and menu for the ZE couch",
version = "2.0",
url = "https://marisakirisame.net"
};
public void OnPluginStart()
{
RegAdminCmd("sm_couch", Command_Couch, ADMFLAG_CHANGEMAP, "Opens the couch menu");
RegAdminCmd("sm_spawncouch", Command_SpawnCouch, ADMFLAG_CHANGEMAP, "Spawns the couch where you are aiming");
RegAdminCmd("sm_bringcouch", Command_BringCouch, ADMFLAG_CHANGEMAP, "Brings the couch to where you are aiming");
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");
}
public void OnAllPluginsLoaded()
{
g_bCoreLoaded = LibraryExists("Couch");
}
public void OnLibraryAdded(const char[] name)
{
if (StrEqual(name, "Couch"))
g_bCoreLoaded = true;
}
public void OnLibraryRemoved(const char[] name)
{
if (StrEqual(name, "Couch"))
g_bCoreLoaded = false;
}
// ---------------------------------------------------------------------------------------------
// Commands
// ---------------------------------------------------------------------------------------------
public Action Command_Couch(int client, int args)
{
if (!RequireInGame(client) || !RequireCore(client))
return Plugin_Handled;
ShowCouchMenu(client);
return Plugin_Handled;
}
public Action Command_SpawnCouch(int client, int args)
{
if (!RequireInGame(client) || !RequireCore(client))
return Plugin_Handled;
DoSpawn(client);
return Plugin_Handled;
}
public Action Command_BringCouch(int client, int args)
{
if (!RequireInGame(client) || !RequireCore(client))
return Plugin_Handled;
DoBring(client);
return Plugin_Handled;
}
// Needs no position, so it also works from the server console.
public Action Command_RemoveCouch(int client, int args)
{
if (!RequireCore(client))
return Plugin_Handled;
DoRemove(client);
return Plugin_Handled;
}
public Action Command_PistolOnly(int client, int args)
{
if (!RequireCore(client))
return Plugin_Handled;
bool bEnable = !Couch_IsPistolOnly();
if (args >= 1)
bEnable = (GetCmdArgInt(1) != 0);
DoSetPistolOnly(client, bEnable);
return Plugin_Handled;
}
bool RequireInGame(int client)
{
if (client >= 1 && IsClientInGame(client))
return true;
ReplyToCommand(client, "[SM] This command can only be used in-game.");
return false;
}
bool RequireCore(int client)
{
if (g_bCoreLoaded)
return true;
ReplyToCommand(client, "[SM] The Couch core plugin is not loaded.");
return false;
}
// ---------------------------------------------------------------------------------------------
// Actions
// ---------------------------------------------------------------------------------------------
void DoSpawn(int client)
{
if (Couch_Spawn(client) == -1)
{
ReplyToCommand(client, "[SM] Could not create the couch.");
return;
}
ShowActivity2(client, PREFIX, "\x01Spawned the \x04couch\x01.");
LogAction(client, -1, "\"%L\" spawned the couch.", client);
}
void DoBring(int client)
{
if (!Couch_Bring(client))
{
ReplyToCommand(client, "[SM] There is no couch to bring. Use sm_spawncouch first.");
return;
}
ShowActivity2(client, PREFIX, "\x01Brought the \x04couch\x01.");
LogAction(client, -1, "\"%L\" brought the couch.", client);
}
void DoRemove(int client)
{
if (!Couch_Remove())
{
ReplyToCommand(client, "[SM] There is no couch to remove.");
return;
}
ShowActivity2(client, PREFIX, "\x01Removed the \x04couch\x01.");
LogAction(client, -1, "\"%L\" removed the couch.", client);
}
void DoToggleEffect(int client)
{
bool bEnable = !Couch_IsEffectEnabled();
Couch_SetEffectEnabled(bEnable);
ShowActivity2(client, PREFIX, "\x01Turned the couch effect \x04%s\x01.", bEnable ? "on" : "off");
LogAction(client, -1, "\"%L\" turned the couch effect %s.", client, bEnable ? "on" : "off");
}
// Announced to everyone so players know why their rifle stopped moving the couch.
void DoSetPistolOnly(int client, bool bEnable)
{
Couch_SetPistolOnly(bEnable);
ShowActivity2(client, PREFIX, "\x01Turned couch pistol-only mode \x04%s\x01.", bEnable ? "on" : "off");
LogAction(client, -1, "\"%L\" turned couch pistol-only mode %s.", client, bEnable ? "on" : "off");
}
// ---------------------------------------------------------------------------------------------
// Menu
// ---------------------------------------------------------------------------------------------
// Rebuilt on every display so the labels always reflect the current state.
void ShowCouchMenu(int client)
{
bool bHasCouch = (Couch_GetCouch() != -1);
bool bPlaced = Couch_IsPlaced();
Menu menu = new Menu(MenuHandler_Couch);
menu.SetTitle("Couch");
// With a couch out, bring is the way to move it; without one there is nothing to bring.
menu.AddItem("spawn", "Spawn couch at crosshair", bHasCouch ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
menu.AddItem("bring", "Bring couch to crosshair", bHasCouch ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED);
if (bHasCouch)
menu.AddItem("remove", "Remove couch");
else if (bPlaced)
menu.AddItem("remove", "Remove couch (respawns next round)");
else
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);
menu.ExitButton = true;
menu.Display(client, MENU_TIME_FOREVER);
}
public int MenuHandler_Couch(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, "spawn"))
DoSpawn(client);
else if (StrEqual(sInfo, "bring"))
DoBring(client);
else if (StrEqual(sInfo, "remove"))
DoRemove(client);
else if (StrEqual(sInfo, "effect"))
DoToggleEffect(client);
else if (StrEqual(sInfo, "pistol"))
DoSetPistolOnly(client, !Couch_IsPistolOnly());
ShowCouchMenu(client);
}
case MenuAction_End:
{
delete menu;
}
}
return 0;
}
+246
View File
@@ -0,0 +1,246 @@
#include <sourcemod>
#include <Couch>
#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;
// Account ids banned this map. This is the ban itself, so leaving and rejoining does not clear it.
ArrayList g_alBannedAccounts;
// Slot mirror. Also carries bans of unauthenticated clients (account id 0), which last the session only.
bool g_bBanned[MAXPLAYERS + 1];
public Plugin myinfo =
{
name = "Couch Ban",
author = "marisakirisame.net",
description = "Bans players from the ZE couch until the end of the map",
version = "2.0",
url = "https://marisakirisame.net"
};
// ---------------------------------------------------------------------------------------------
// Plugin lifecycle
// ---------------------------------------------------------------------------------------------
public void OnPluginStart()
{
LoadTranslations("common.phrases"); // FindTarget's replies.
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_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_cunban", Command_CouchUnban, ADMFLAG_BAN, "Alias of sm_couchunban");
}
public void OnAllPluginsLoaded()
{
g_bCoreLoaded = LibraryExists("Couch");
// After a reload of this plugin the core still knows who is blocked.
if (!g_bCoreLoaded)
return;
for (int i = 1; i <= MaxClients; i++)
{
if (!IsClientInGame(i) || !Couch_IsClientBlocked(i))
continue;
g_bBanned[i] = true;
RememberAccount(i);
}
}
public void OnLibraryAdded(const char[] name)
{
if (!StrEqual(name, "Couch"))
return;
g_bCoreLoaded = true;
// A core reload wiped its flags. A frame later so its natives are bound.
RequestFrame(Frame_ReapplyBans);
}
public void OnLibraryRemoved(const char[] name)
{
if (StrEqual(name, "Couch"))
g_bCoreLoaded = false;
}
void Frame_ReapplyBans(any data)
{
if (!g_bCoreLoaded)
return;
for (int i = 1; i <= MaxClients; i++)
{
if (g_bBanned[i] && IsClientInGame(i))
Couch_SetClientBlocked(i, true);
}
}
public void OnMapStart()
{
for (int i = 1; i <= MAXPLAYERS; i++)
g_bBanned[i] = false;
}
public void OnMapEnd()
{
g_alBannedAccounts.Clear();
}
// The account id stays on the list so a rejoin picks the ban back up.
public void OnClientDisconnect(int client)
{
g_bBanned[client] = false;
}
// Either "in game" or "authorized" can come last, so both are hooked.
public void OnClientPutInServer(int client)
{
TryReapplyBan(client);
}
public void OnClientAuthorized(int client, const char[] auth)
{
TryReapplyBan(client);
}
void TryReapplyBan(int client)
{
if (g_bBanned[client] || !IsClientInGame(client))
return;
int account = GetSteamAccountID(client);
if (account == 0 || g_alBannedAccounts.FindValue(account) == -1)
return;
g_bBanned[client] = true;
if (g_bCoreLoaded)
Couch_SetClientBlocked(client, true);
PrintToChat(client, PREFIX ... "You are couch-banned\x01 until the end of the map.");
}
// ---------------------------------------------------------------------------------------------
// Commands
// ---------------------------------------------------------------------------------------------
public Action Command_CouchBan(int client, int args)
{
if (!RequireCore(client))
return Plugin_Handled;
if (args < 1)
{
ReplyToCommand(client, "[SM] Usage: sm_couchban <#userid|name>");
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);
return Plugin_Handled;
}
public Action Command_CouchUnban(int client, int args)
{
if (!RequireCore(client))
return Plugin_Handled;
if (args < 1)
{
ReplyToCommand(client, "[SM] Usage: sm_couchunban <#userid|name>");
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 not couch-banned.", target);
return Plugin_Handled;
}
DoUnban(client, target);
return Plugin_Handled;
}
bool RequireCore(int client)
{
if (g_bCoreLoaded)
return true;
ReplyToCommand(client, "[SM] The Couch core plugin is not loaded.");
return false;
}
// ---------------------------------------------------------------------------------------------
// Actions
// ---------------------------------------------------------------------------------------------
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);
}
void DoUnban(int client, int target)
{
g_bBanned[target] = false;
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);
}
// An unauthenticated client has no account id, so their ban ends with the session.
void RememberAccount(int client)
{
int account = GetSteamAccountID(client);
if (account != 0 && g_alBannedAccounts.FindValue(account) == -1)
g_alBannedAccounts.Push(account);
}
void ForgetAccount(int client)
{
int account = GetSteamAccountID(client);
if (account == 0)
return;
int index = g_alBannedAccounts.FindValue(account);
if (index != -1)
g_alBannedAccounts.Erase(index);
}
+316
View File
@@ -0,0 +1,316 @@
#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
// 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;
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_fNextExpiry; // Soonest expiry among the names currently on screen.
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_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;
for (int i = 1; i <= MAXPLAYERS; i++)
{
g_fExpire[i] = 0.0;
g_fLastHit[i] = 0.0;
}
}
public void OnClientPutInServer(int client)
{
g_fExpire[client] = 0.0;
g_fLastHit[client] = 0.0;
}
public void OnClientDisconnect(int client)
{
bool bListed = (g_fExpire[client] > GetGameTime());
g_fExpire[client] = 0.0;
g_fLastHit[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;
float fNow = GetGameTime();
g_fLastHit[attacker] = fNow;
g_fExpire[attacker] = fNow + g_cvTime.FloatValue;
QueueFlush();
}
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()
{
// 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);
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;
// Selection sort, most recent hit first. The soonest expiry is tracked over everyone listed,
// since a hidden player lapsing still changes the "+ N" count.
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;
if (g_fNextExpiry == 0.0 || g_fExpire[best] < g_fNextExpiry)
g_fNextExpiry = 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 + 2];
FormatEx(sLine, sizeof(sLine), "\n%s", 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;
}
ClearAll();
}
+144
View File
@@ -0,0 +1,144 @@
#if defined _Couch_included
#endinput
#endif
#define _Couch_included
// The core defines COUCH_CORE; it creates the forwards and natives itself.
#if !defined COUCH_CORE
/**
* Called after the couch has been created, for admin spawns and round-start respawns.
*
* @param couch Entity index of the new couch.
*/
forward void Couch_OnCouchSpawned(int couch);
/**
* Called once the couch entity is gone, for whatever reason. Couch_IsPlaced() tells whether
* it will come back at the next round start.
*/
forward void Couch_OnCouchRemoved();
/**
* Called for every hit the couch accepts from a player. Shotguns report once per pellet.
*
* @param couch Entity index of the couch.
* @param attacker Client index of the player who dealt the damage.
* @param damage Damage dealt by this hit.
*/
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.
*
* @param couch Entity index of the couch.
* @param client Client index of the blocked player.
*/
forward void Couch_OnClientBlocked(int couch, int client);
/**
* @return Entity index of the couch, or -1 if there is none.
*/
native int Couch_GetCouch();
/**
* Whether an admin has placed a couch this map. While true the couch respawns at the placed
* spot every round start.
*
* @return True if a placement is remembered.
*/
native bool Couch_IsPlaced();
/**
* Spawns the couch where the client is aiming, replacing any existing couch. The spot becomes
* the respawn point.
*
* @param client Client whose crosshair places the couch. Must be in game.
* @return Entity index of the couch, or -1 on failure.
*/
native int Couch_Spawn(int client);
/**
* Teleports the couch to where the client is aiming. The respawn point is left as it was.
*
* @param client Client whose crosshair places the couch. Must be in game.
* @return True on success, false if there is no couch.
*/
native bool Couch_Bring(int client);
/**
* Removes the couch and forgets its placement.
*
* @return True if a couch or a placement was removed.
*/
native bool Couch_Remove();
/**
* Whether the colour cycle and light are on. Mirrors sm_couch_effect.
*/
native bool Couch_IsEffectEnabled();
/**
* Switches the colour cycle and light on or off. Writes sm_couch_effect.
*/
native void Couch_SetEffectEnabled(bool enabled);
/**
* Whether only pistols and knives may move the couch. Mirrors sm_couch_pistol_only.
*/
native bool Couch_IsPistolOnly();
/**
* Restricts couch movement to pistols and knives, or lifts the restriction. Writes sm_couch_pistol_only.
*/
native void Couch_SetPistolOnly(bool enabled);
/**
* 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.
*
* @param client Client to block or unblock. Must be in game.
* @param blocked True to block, false to lift the block.
*/
native void Couch_SetClientBlocked(int client, bool blocked);
/**
* Whether the client is currently blocked from the couch.
*
* @param client Client index.
* @return True if blocked.
*/
native bool Couch_IsClientBlocked(int client);
public SharedPlugin __pl_Couch =
{
name = "Couch",
file = "Couch.smx",
#if defined REQUIRE_PLUGIN
required = 1
#else
required = 0
#endif
};
#if !defined REQUIRE_PLUGIN
public void __pl_Couch_SetNTVOptional()
{
MarkNativeAsOptional("Couch_GetCouch");
MarkNativeAsOptional("Couch_IsPlaced");
MarkNativeAsOptional("Couch_Spawn");
MarkNativeAsOptional("Couch_Bring");
MarkNativeAsOptional("Couch_Remove");
MarkNativeAsOptional("Couch_IsEffectEnabled");
MarkNativeAsOptional("Couch_SetEffectEnabled");
MarkNativeAsOptional("Couch_IsPistolOnly");
MarkNativeAsOptional("Couch_SetPistolOnly");
MarkNativeAsOptional("Couch_SetClientBlocked");
MarkNativeAsOptional("Couch_IsClientBlocked");
}
#endif
#endif // COUCH_CORE