Files
sm-plugins/Couch/scripting/Couch.sp
T
2026-09-29 02:24:47 +01:00

893 lines
26 KiB
SourcePawn

#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];
}