893 lines
26 KiB
SourcePawn
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];
|
|
}
|