Add Couch plugins
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user