#include #include #include #include #include #include #define COUCH_CORE #include #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(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(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(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]; }