627 lines
16 KiB
SourcePawn
627 lines
16 KiB
SourcePawn
//----------------------------------------------------------------------------------------------------
|
|
// Leader3 markers: unloze_ping prop_dynamic markers with point_worldtext titles.
|
|
// The ArrayList is the authoritative store; entities are respawned from it every round.
|
|
//----------------------------------------------------------------------------------------------------
|
|
|
|
enum struct Marker
|
|
{
|
|
int ownerUserId;
|
|
char ownerName[MAX_NAME_LENGTH];
|
|
int skin;
|
|
float pos[3];
|
|
char title[128];
|
|
float placedAt;
|
|
int propRef;
|
|
int textRef;
|
|
}
|
|
|
|
ArrayList g_alMarkers;
|
|
|
|
void Markers_OnPluginStart()
|
|
{
|
|
g_alMarkers = new ArrayList(sizeof(Marker));
|
|
}
|
|
|
|
void SkinName(int skin, char[] buf, int maxlen)
|
|
{
|
|
switch (skin)
|
|
{
|
|
case SKIN_SKULL: strcopy(buf, maxlen, "Skull");
|
|
case SKIN_MOON: strcopy(buf, maxlen, "Moon");
|
|
case SKIN_CROSS: strcopy(buf, maxlen, "Cross");
|
|
default: strcopy(buf, maxlen, "Star");
|
|
}
|
|
}
|
|
|
|
int SkinFromName(const char[] name)
|
|
{
|
|
if (StrEqual(name, "star", false)) return SKIN_STAR;
|
|
if (StrEqual(name, "skull", false)) return SKIN_SKULL;
|
|
if (StrEqual(name, "moon", false)) return SKIN_MOON;
|
|
if (StrEqual(name, "cross", false)) return SKIN_CROSS;
|
|
return -1;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------------------
|
|
// Placement
|
|
//----------------------------------------------------------------------------------------------------
|
|
void Marker_Place(int client, int skin)
|
|
{
|
|
if (!IsLeader(client))
|
|
return;
|
|
|
|
// Dead and spectating leaders may keep placing markers (leading from spec);
|
|
// only living zombies are blocked.
|
|
if (IsLivingZombie(client))
|
|
{
|
|
PrintToChat(client, "%sYou cannot place markers as a zombie.", PREFIX);
|
|
return;
|
|
}
|
|
|
|
float pos[3];
|
|
if (!TracePlayerAngles(client, pos))
|
|
{
|
|
PrintToChat(client, "%sCould not find a spot to place the marker on.", PREFIX);
|
|
return;
|
|
}
|
|
|
|
char skinName[16];
|
|
SkinName(skin, skinName, sizeof(skinName));
|
|
|
|
int userid = GetClientUserId(client);
|
|
int existing = FindMarkerByOwnerSkin(userid, skin);
|
|
|
|
Marker marker;
|
|
if (existing != -1)
|
|
{
|
|
// Moving keeps the title and stays out of chat. The prop and text entities are
|
|
// teleported rather than respawned so repeated moves don't churn edicts.
|
|
g_alMarkers.GetArray(existing, marker);
|
|
marker.pos = pos;
|
|
marker.placedAt = GetGameTime();
|
|
GetClientName(client, marker.ownerName, sizeof(marker.ownerName));
|
|
g_alMarkers.SetArray(existing, marker);
|
|
Marker_MoveProp(existing);
|
|
Marker_UpdateText(existing);
|
|
}
|
|
else
|
|
{
|
|
if (CountMarkersByOwner(userid) >= g_cvMarkerLimit.IntValue)
|
|
{
|
|
PrintToChat(client, "%sMarker limit reached (%d). Replace one of your markers or use \x04!clearmarkers\x01.", PREFIX, g_cvMarkerLimit.IntValue);
|
|
return;
|
|
}
|
|
|
|
marker.ownerUserId = userid;
|
|
GetClientName(client, marker.ownerName, sizeof(marker.ownerName));
|
|
marker.skin = skin;
|
|
marker.pos = pos;
|
|
marker.title[0] = '\0';
|
|
marker.placedAt = GetGameTime();
|
|
marker.propRef = INVALID_ENT_REFERENCE;
|
|
marker.textRef = INVALID_ENT_REFERENCE;
|
|
|
|
int idx = g_alMarkers.PushArray(marker);
|
|
SpawnMarkerProp(idx);
|
|
PrintToChatAll("%s\x04%N\x01 placed a \x04%s\x01 marker.", PREFIX, client, skinName);
|
|
PrintToChat(client, "%sType \x04`your text\x01 in chat to put a title on it.", PREFIX);
|
|
}
|
|
}
|
|
|
|
void Marker_PlaceBySpray(int client)
|
|
{
|
|
Marker_Place(client, SKIN_STAR);
|
|
}
|
|
|
|
int FindMarkerByOwnerSkin(int userid, int skin)
|
|
{
|
|
Marker marker;
|
|
for (int i = 0; i < g_alMarkers.Length; i++)
|
|
{
|
|
g_alMarkers.GetArray(i, marker);
|
|
if (marker.ownerUserId == userid && marker.skin == skin)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
bool Marker_GetTitle(int client, int skin, char[] buf, int maxlen)
|
|
{
|
|
int idx = FindMarkerByOwnerSkin(GetClientUserId(client), skin);
|
|
if (idx == -1)
|
|
return false;
|
|
|
|
Marker marker;
|
|
g_alMarkers.GetArray(idx, marker);
|
|
if (marker.title[0] == '\0')
|
|
return false;
|
|
|
|
strcopy(buf, maxlen, marker.title);
|
|
return true;
|
|
}
|
|
|
|
int CountMarkersByOwner(int userid)
|
|
{
|
|
int count = 0;
|
|
Marker marker;
|
|
for (int i = 0; i < g_alMarkers.Length; i++)
|
|
{
|
|
g_alMarkers.GetArray(i, marker);
|
|
if (marker.ownerUserId == userid)
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------------------
|
|
// Titles
|
|
//----------------------------------------------------------------------------------------------------
|
|
|
|
// Titles typed entirely in lowercase are uppercased; mixed-case titles are kept as typed.
|
|
void UppercaseAllLowerTitle(char[] text)
|
|
{
|
|
bool hasLower = false;
|
|
for (int i = 0; text[i] != '\0'; i++)
|
|
{
|
|
int bytes = IsCharMB(text[i]);
|
|
if (bytes > 1)
|
|
{
|
|
i += bytes - 1;
|
|
continue;
|
|
}
|
|
if (IsCharUpper(text[i]))
|
|
return;
|
|
if (IsCharLower(text[i]))
|
|
hasLower = true;
|
|
}
|
|
|
|
if (!hasLower)
|
|
return;
|
|
|
|
for (int i = 0; text[i] != '\0'; i++)
|
|
{
|
|
int bytes = IsCharMB(text[i]);
|
|
if (bytes > 1)
|
|
{
|
|
i += bytes - 1;
|
|
continue;
|
|
}
|
|
text[i] = CharToUpper(text[i]);
|
|
}
|
|
}
|
|
|
|
void Marker_SetTitle(int client, const char[] text)
|
|
{
|
|
int userid = GetClientUserId(client);
|
|
|
|
// Most recently placed marker owned by this leader.
|
|
int lastIdx = -1;
|
|
float lastTime = -1.0;
|
|
Marker marker;
|
|
for (int i = 0; i < g_alMarkers.Length; i++)
|
|
{
|
|
g_alMarkers.GetArray(i, marker);
|
|
if (marker.ownerUserId == userid && marker.placedAt > lastTime)
|
|
{
|
|
lastTime = marker.placedAt;
|
|
lastIdx = i;
|
|
}
|
|
}
|
|
|
|
if (lastIdx == -1)
|
|
{
|
|
PrintToChat(client, "%sYou have no marker to title. Place one first.", PREFIX);
|
|
return;
|
|
}
|
|
|
|
g_alMarkers.GetArray(lastIdx, marker);
|
|
strcopy(marker.title, sizeof(marker.title), text);
|
|
UppercaseAllLowerTitle(marker.title);
|
|
g_alMarkers.SetArray(lastIdx, marker);
|
|
Marker_UpdateText(lastIdx);
|
|
|
|
char skinName[16];
|
|
SkinName(marker.skin, skinName, sizeof(skinName));
|
|
|
|
if (text[0] == '\0')
|
|
{
|
|
PrintToChat(client, "%sTitle removed from your \x04%s\x01 marker.", PREFIX, skinName);
|
|
return;
|
|
}
|
|
PrintToChatAll("%s\x04%N\x01 marked \x04%s\x01: \x04%s", PREFIX, client, skinName, marker.title);
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------------------
|
|
// Entity lifecycle
|
|
//----------------------------------------------------------------------------------------------------
|
|
void SpawnMarkerProp(int idx)
|
|
{
|
|
Marker marker;
|
|
g_alMarkers.GetArray(idx, marker);
|
|
|
|
int ent = CreateEntityByName("prop_dynamic");
|
|
if (ent == -1)
|
|
return;
|
|
|
|
SetEntityModel(ent, MARKER_MODEL);
|
|
DispatchKeyValueFloat(ent, "modelscale", MARKER_MODEL_SCALE);
|
|
|
|
char skin[4];
|
|
IntToString(marker.skin, skin, sizeof(skin));
|
|
SetVariantString(skin);
|
|
AcceptEntityInput(ent, "Skin");
|
|
|
|
DispatchSpawn(ent);
|
|
TeleportEntity(ent, marker.pos, NULL_VECTOR, NULL_VECTOR);
|
|
|
|
marker.propRef = EntIndexToEntRef(ent);
|
|
g_alMarkers.SetArray(idx, marker);
|
|
}
|
|
|
|
void SpawnMarkerText(int idx)
|
|
{
|
|
Marker marker;
|
|
g_alMarkers.GetArray(idx, marker);
|
|
|
|
if (marker.title[0] == '\0' || g_bWorldTextBroken)
|
|
return;
|
|
|
|
int ent = CreateEntityByName("point_worldtext");
|
|
if (ent == -1)
|
|
{
|
|
// Engine build without point_worldtext: titles stay chat-only for this map.
|
|
g_bWorldTextBroken = true;
|
|
LogMessage("point_worldtext could not be created; marker titles will not render in the world.");
|
|
return;
|
|
}
|
|
|
|
char text[192];
|
|
Format(text, sizeof(text), "%s", marker.title);
|
|
|
|
DispatchKeyValue(ent, "message", text);
|
|
DispatchKeyValue(ent, "font", "10");
|
|
DispatchKeyValue(ent, "textsize", "12");
|
|
DispatchKeyValue(ent, "color", "255 255 255");
|
|
DispatchKeyValue(ent, "orientation", "1");
|
|
DispatchKeyValue(ent, "rainbow", "1");
|
|
DispatchSpawn(ent);
|
|
|
|
float pos[3];
|
|
ComputeMarkerTextPos(marker, pos);
|
|
TeleportEntity(ent, pos, NULL_VECTOR, NULL_VECTOR);
|
|
|
|
marker.textRef = EntIndexToEntRef(ent);
|
|
g_alMarkers.SetArray(idx, marker);
|
|
}
|
|
|
|
void ComputeMarkerTextPos(const Marker marker, float pos[3])
|
|
{
|
|
pos = marker.pos;
|
|
// unloze_ping.mdl is 80 units tall and spawned at modelscale 1.5 (120 units).
|
|
pos[2] += MARKER_MODEL_HEIGHT * MARKER_MODEL_SCALE + 8.0;
|
|
|
|
// Markers placed in low rooms would put the text inside the ceiling: trace up
|
|
// from the marker base and keep the text MARKER_TEXT_CLEARANCE below any hit.
|
|
float up[3];
|
|
up = marker.pos;
|
|
up[2] = pos[2] + MARKER_TEXT_CLEARANCE;
|
|
Handle trace = TR_TraceRayFilterEx(marker.pos, up, MASK_SOLID_BRUSHONLY, RayType_EndPoint, TraceFilter_NoPlayers);
|
|
if (TR_DidHit(trace))
|
|
{
|
|
float hit[3];
|
|
TR_GetEndPosition(hit, trace);
|
|
pos[2] = hit[2] - MARKER_TEXT_CLEARANCE;
|
|
}
|
|
delete trace;
|
|
}
|
|
|
|
// Teleports the existing marker prop to the stored position, spawning one only if the
|
|
// reference went stale.
|
|
void Marker_MoveProp(int idx)
|
|
{
|
|
Marker marker;
|
|
g_alMarkers.GetArray(idx, marker);
|
|
|
|
int ent = LiveEntRef(marker.propRef, "prop_dynamic");
|
|
if (ent == -1)
|
|
{
|
|
SpawnMarkerProp(idx);
|
|
return;
|
|
}
|
|
|
|
TeleportEntity(ent, marker.pos, NULL_VECTOR, NULL_VECTOR);
|
|
}
|
|
|
|
// Retitles/moves the existing point_worldtext in place (SetText is networked via
|
|
// SendPropString) instead of respawning it, so retitles and moves don't churn edicts.
|
|
void Marker_UpdateText(int idx)
|
|
{
|
|
Marker marker;
|
|
g_alMarkers.GetArray(idx, marker);
|
|
|
|
if (marker.title[0] == '\0')
|
|
{
|
|
KillEntRef(marker.textRef, "point_worldtext");
|
|
marker.textRef = INVALID_ENT_REFERENCE;
|
|
g_alMarkers.SetArray(idx, marker);
|
|
return;
|
|
}
|
|
|
|
int ent = LiveEntRef(marker.textRef, "point_worldtext");
|
|
if (ent == -1)
|
|
{
|
|
SpawnMarkerText(idx);
|
|
return;
|
|
}
|
|
|
|
SetVariantString(marker.title);
|
|
AcceptEntityInput(ent, "SetText");
|
|
|
|
float pos[3];
|
|
ComputeMarkerTextPos(marker, pos);
|
|
TeleportEntity(ent, pos, NULL_VECTOR, NULL_VECTOR);
|
|
}
|
|
|
|
void Marker_KillEnts(Marker marker)
|
|
{
|
|
KillEntRef(marker.propRef, "prop_dynamic");
|
|
KillEntRef(marker.textRef, "point_worldtext");
|
|
}
|
|
|
|
void KillEntRef(int ref, const char[] expectedClass)
|
|
{
|
|
int ent = LiveEntRef(ref, expectedClass);
|
|
if (ent != -1)
|
|
AcceptEntityInput(ent, "Kill");
|
|
}
|
|
|
|
// Resolves an entity reference, returning -1 unless it is still a live edict of the
|
|
// expected class.
|
|
int LiveEntRef(int ref, const char[] expectedClass)
|
|
{
|
|
if (ref == INVALID_ENT_REFERENCE)
|
|
return -1;
|
|
|
|
int ent = EntRefToEntIndex(ref);
|
|
if (ent == INVALID_ENT_REFERENCE || !IsValidEdict(ent))
|
|
return -1;
|
|
|
|
char classname[64];
|
|
GetEdictClassname(ent, classname, sizeof(classname));
|
|
return strcmp(classname, expectedClass) == 0 ? ent : -1;
|
|
}
|
|
|
|
// Round transitions delete runtime entities: rebuild every marker from the stored data.
|
|
void Markers_OnRoundStart()
|
|
{
|
|
Marker marker;
|
|
for (int i = 0; i < g_alMarkers.Length; i++)
|
|
{
|
|
g_alMarkers.GetArray(i, marker);
|
|
marker.propRef = INVALID_ENT_REFERENCE;
|
|
marker.textRef = INVALID_ENT_REFERENCE;
|
|
g_alMarkers.SetArray(i, marker);
|
|
|
|
SpawnMarkerProp(i);
|
|
SpawnMarkerText(i);
|
|
}
|
|
}
|
|
|
|
void Markers_ClearAll(bool killEnts = true)
|
|
{
|
|
if (killEnts)
|
|
{
|
|
Marker marker;
|
|
for (int i = 0; i < g_alMarkers.Length; i++)
|
|
{
|
|
g_alMarkers.GetArray(i, marker);
|
|
Marker_KillEnts(marker);
|
|
}
|
|
}
|
|
g_alMarkers.Clear();
|
|
}
|
|
|
|
void Marker_ClearByOwner(int client, bool announce = true)
|
|
{
|
|
int userid = GetClientUserId(client);
|
|
int removed = 0;
|
|
|
|
Marker marker;
|
|
for (int i = g_alMarkers.Length - 1; i >= 0; i--)
|
|
{
|
|
g_alMarkers.GetArray(i, marker);
|
|
if (marker.ownerUserId != userid)
|
|
continue;
|
|
|
|
Marker_KillEnts(marker);
|
|
g_alMarkers.Erase(i);
|
|
removed++;
|
|
}
|
|
|
|
if (!announce)
|
|
return;
|
|
|
|
if (removed > 0)
|
|
PrintToChat(client, "%sRemoved %d of your markers.", PREFIX, removed);
|
|
else
|
|
PrintToChat(client, "%sYou have no markers.", PREFIX);
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------------------
|
|
// Commands
|
|
//----------------------------------------------------------------------------------------------------
|
|
public Action Cmd_Marker(int client, int args)
|
|
{
|
|
if (client == 0)
|
|
{
|
|
ReplyToCommand(client, "%s Cannot use this from the console.", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
if (!IsLeader(client))
|
|
{
|
|
ReplyToCommand(client, "%s Only leaders can place markers.", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
if (args < 1)
|
|
{
|
|
ReplyToCommand(client, "%s Usage: sm_marker <star|skull|moon|cross>", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
char arg[16];
|
|
GetCmdArg(1, arg, sizeof(arg));
|
|
int skin = SkinFromName(arg);
|
|
if (skin == -1)
|
|
{
|
|
ReplyToCommand(client, "%s Unknown marker type '%s'. Use star, skull, moon or cross.", PREFIX_PLAIN, arg);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
Marker_Place(client, skin);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
public Action Cmd_MarkerTitle(int client, int args)
|
|
{
|
|
if (client == 0)
|
|
{
|
|
ReplyToCommand(client, "%s Cannot use this from the console.", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
if (!IsLeader(client))
|
|
{
|
|
ReplyToCommand(client, "%s Only leaders can title markers.", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
char text[128];
|
|
GetCmdArgString(text, sizeof(text));
|
|
StripQuotes(text);
|
|
TrimString(text);
|
|
|
|
Marker_SetTitle(client, text);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
public Action Cmd_ClearMarkers(int client, int args)
|
|
{
|
|
if (client == 0)
|
|
{
|
|
Markers_ClearAll();
|
|
ReplyToCommand(client, "%s All markers cleared.", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
if (args >= 1 && CheckCommandAccess(client, "sm_admin", ADMFLAG_GENERIC, false))
|
|
{
|
|
char arg[65];
|
|
GetCmdArg(1, arg, sizeof(arg));
|
|
if (StrEqual(arg, "all", false))
|
|
{
|
|
Markers_ClearAll();
|
|
PrintToChatAll("%sAll markers have been cleared by \x04%N\x01.", PREFIX, client);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
char targetName[MAX_TARGET_LENGTH];
|
|
int targets[MAXPLAYERS];
|
|
bool tnIsMl;
|
|
int count = ProcessTargetString(arg, client, targets, sizeof(targets),
|
|
COMMAND_FILTER_CONNECTED | COMMAND_FILTER_NO_BOTS, targetName, sizeof(targetName), tnIsMl);
|
|
if (count <= 0)
|
|
{
|
|
ReplyToTargetError(client, count);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
for (int i = 0; i < count; i++)
|
|
Marker_ClearByOwner(targets[i]);
|
|
ReplyToCommand(client, "%s Cleared markers of %s.", PREFIX_PLAIN, targetName);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
if (!IsLeader(client))
|
|
{
|
|
ReplyToCommand(client, "%s Only leaders can clear markers.", PREFIX_PLAIN);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
Marker_ClearByOwner(client);
|
|
return Plugin_Handled;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------------------
|
|
// Aim trace (port of Leader2's TracePlayerAngles)
|
|
//----------------------------------------------------------------------------------------------------
|
|
bool TracePlayerAngles(int client, float vecResult[3])
|
|
{
|
|
if (!IsClientInGame(client))
|
|
return false;
|
|
|
|
float vecEyeAngles[3], vecEyeOrigin[3];
|
|
GetClientEyeAngles(client, vecEyeAngles);
|
|
GetClientEyePosition(client, vecEyeOrigin);
|
|
|
|
Handle trace = TR_TraceRayFilterEx(vecEyeOrigin, vecEyeAngles, MASK_SHOT_HULL, RayType_Infinite, TraceFilter_NoPlayers);
|
|
|
|
// AllSolid: eyes clipped inside a solid (crowd pushes); endpos degenerates to the eye.
|
|
bool hit = TR_DidHit(trace) && !TR_AllSolid(trace);
|
|
int hitEnt = hit ? TR_GetEntityIndex(trace) : -1;
|
|
if (hit)
|
|
TR_GetEndPosition(vecResult, trace);
|
|
delete trace;
|
|
|
|
if (!hit)
|
|
return false;
|
|
|
|
// NaN or out-of-grid components reach the client as 0 (WriteBitCoord encodes them
|
|
// as zero), which draws the beam to the world origin. Catch them before the TE.
|
|
if (vecResult[0] != vecResult[0] || vecResult[1] != vecResult[1] || vecResult[2] != vecResult[2]
|
|
|| FloatAbs(vecResult[0]) > 16384.0 || FloatAbs(vecResult[1]) > 16384.0 || FloatAbs(vecResult[2]) > 16384.0)
|
|
{
|
|
LogSuspectTrace(client, hitEnt, vecResult, vecEyeOrigin, vecEyeAngles, "insane endpos");
|
|
return false;
|
|
}
|
|
|
|
// ZE maps park "removed" but still solid props at the world origin; their hulls
|
|
// catch the ray there. Hits on the world itself near origin stay legitimate.
|
|
if (hitEnt > 0 && GetVectorLength(vecResult) < 64.0)
|
|
{
|
|
LogSuspectTrace(client, hitEnt, vecResult, vecEyeOrigin, vecEyeAngles, "entity hull at world origin");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// One line per 5s so a leader holding the tracer on a culprit doesn't flood the log.
|
|
static float g_fNextSuspectTraceLog;
|
|
|
|
static void LogSuspectTrace(int client, int ent, const float end[3], const float eye[3], const float ang[3], const char[] why)
|
|
{
|
|
float now = GetGameTime();
|
|
if (now < g_fNextSuspectTraceLog)
|
|
return;
|
|
g_fNextSuspectTraceLog = now + 5.0;
|
|
|
|
char classname[64] = "worldspawn";
|
|
char model[PLATFORM_MAX_PATH];
|
|
if (ent > 0 && IsValidEdict(ent))
|
|
{
|
|
GetEdictClassname(ent, classname, sizeof(classname));
|
|
GetEntPropString(ent, Prop_Data, "m_ModelName", model, sizeof(model));
|
|
}
|
|
|
|
LogMessage("Aim trace rejected (%s) for %L: end (%.1f %.1f %.1f), hit ent %d (%s, model '%s'), eye (%.1f %.1f %.1f), ang (%.1f %.1f %.1f)",
|
|
why, client, end[0], end[1], end[2], ent, classname, model,
|
|
eye[0], eye[1], eye[2], ang[0], ang[1], ang[2]);
|
|
}
|
|
|
|
public bool TraceFilter_NoPlayers(int entity, int contentsMask)
|
|
{
|
|
return entity > MaxClients;
|
|
}
|