CounterStrikeSharp 1.0.375 plugin. Admins mark 4 points with their crosshair (native Trace.TraceEndShape), preview the box with env_beam lines, and confirm it into a per-map zone file. Points in a near-vertical plane make a wall zone, turned to match the face. - Players are kept out by a per-tick movement block (swept against the zone box grown by the player's hull), since CSS can't give a spawned entity custom-size collision. - Wall zones are drawn as a border with horizontal fill lines (optionally scrolling and pulsing) and a beam-built no-entry sign; floor zones as a rectangle with an X. - Commands: css_zone_start/color/mark/height/confirm/cancel, css_zone_list/near/remove/ active/reload, all gated on @css/root. - release.sh builds from the committed source and publishes NoGoZones-<tag>.tar.gz.
188 lines
7.1 KiB
C#
188 lines
7.1 KiB
C#
using CounterStrikeSharp.API;
|
|
using CounterStrikeSharp.API.Core;
|
|
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
|
|
|
|
namespace NoGoZones;
|
|
|
|
// Logical barrier: CSS can't give a spawned entity a custom-size collision shape (see README), so
|
|
// zones are enforced by moving players back out every tick.
|
|
//
|
|
// Works on the player's origin against the zone box grown by the player's hull (Minkowski sum),
|
|
// so the whole hull is kept out, not just the feet. Each tick it compares last tick's origin to
|
|
// this tick's: if the step crossed into a zone, the player is put back on the face they came
|
|
// through, keeping their movement along the face (so walls slide, and the top can be stood on).
|
|
// Sweeping the step also stops fast players from skipping over a thin zone between two ticks.
|
|
public class Blocker
|
|
{
|
|
private const float Epsilon = 0.05f;
|
|
|
|
// Used if a pawn's collision property isn't readable.
|
|
private static readonly Point3 DefaultHullMins = new(-16f, -16f, 0f);
|
|
private static readonly Point3 DefaultHullMaxs = new(16f, 16f, 72f);
|
|
|
|
private readonly Dictionary<uint, Point3> _lastOrigin = [];
|
|
|
|
public void Reset() => _lastOrigin.Clear();
|
|
|
|
public void Forget(uint pawnIndex) => _lastOrigin.Remove(pawnIndex);
|
|
|
|
public void Tick(IReadOnlyList<Zone> zones, bool ignoreNoclip)
|
|
{
|
|
if (zones.Count == 0)
|
|
{
|
|
_lastOrigin.Clear();
|
|
return;
|
|
}
|
|
|
|
foreach (var player in Utilities.GetPlayers())
|
|
{
|
|
if (player is not { IsValid: true, PawnIsAlive: true })
|
|
continue;
|
|
var pawn = player.PlayerPawn.Value;
|
|
if (pawn is not { IsValid: true } || pawn.AbsOrigin is not { } absOrigin)
|
|
continue;
|
|
|
|
var origin = Point3.From(absOrigin);
|
|
var index = pawn.Index;
|
|
|
|
if (ignoreNoclip && pawn.MoveType == MoveType_t.MOVETYPE_NOCLIP)
|
|
{
|
|
_lastOrigin[index] = origin;
|
|
continue;
|
|
}
|
|
|
|
var (hullMins, hullMaxs) = Hull(pawn);
|
|
var previous = _lastOrigin.TryGetValue(index, out var last) ? last : origin;
|
|
var velocity = pawn.AbsVelocity;
|
|
var v = new Point3(velocity.X, velocity.Y, velocity.Z);
|
|
var moved = false;
|
|
|
|
// Horizontal hull half-widths; the hull is centred on the origin in X/Y.
|
|
float hx = MathF.Max(MathF.Abs(hullMins.X), MathF.Abs(hullMaxs.X));
|
|
float hy = MathF.Max(MathF.Abs(hullMins.Y), MathF.Abs(hullMaxs.Y));
|
|
|
|
foreach (var zone in zones)
|
|
{
|
|
if (!zone.Active)
|
|
continue;
|
|
|
|
// Work in the zone's own frame. A turned zone sees the (world-aligned) hull as wider,
|
|
// so grow it by the hull's extent along each local axis.
|
|
var shape = zone.Shape;
|
|
float c = MathF.Abs(shape.Cos), s = MathF.Abs(shape.Sin);
|
|
float ex = hx * c + hy * s, ey = hx * s + hy * c;
|
|
|
|
// Region the origin must stay out of.
|
|
var lo = new Point3(shape.Mins.X - ex, shape.Mins.Y - ey, shape.Mins.Z - hullMaxs.Z);
|
|
var hi = new Point3(shape.Maxs.X + ex, shape.Maxs.Y + ey, shape.Maxs.Z - hullMins.Z);
|
|
|
|
var localOrigin = shape.ToLocal(origin);
|
|
var localV = shape.ToLocal(v);
|
|
if (Resolve(shape.ToLocal(previous), ref localOrigin, ref localV, lo, hi))
|
|
{
|
|
origin = shape.ToWorld(localOrigin);
|
|
v = shape.ToWorld(localV);
|
|
moved = true;
|
|
}
|
|
}
|
|
|
|
if (moved)
|
|
pawn.Teleport(origin.ToVector(), null, v.ToVector());
|
|
|
|
_lastOrigin[index] = origin;
|
|
}
|
|
}
|
|
|
|
private static (Point3, Point3) Hull(CCSPlayerPawn pawn)
|
|
{
|
|
var collision = pawn.Collision;
|
|
if (collision == null)
|
|
return (DefaultHullMins, DefaultHullMaxs);
|
|
var mins = Point3.From(collision.Mins);
|
|
var maxs = Point3.From(collision.Maxs);
|
|
// A zero-size hull means the property wasn't populated; don't let that shrink the zone.
|
|
return maxs.X - mins.X < 1f ? (DefaultHullMins, DefaultHullMaxs) : (mins, maxs);
|
|
}
|
|
|
|
private static bool Inside(Point3 p, Point3 lo, Point3 hi) =>
|
|
p.X > lo.X && p.X < hi.X && p.Y > lo.Y && p.Y < hi.Y && p.Z > lo.Z && p.Z < hi.Z;
|
|
|
|
// Returns true if origin/velocity were changed.
|
|
private static bool Resolve(Point3 from, ref Point3 to, ref Point3 v, Point3 lo, Point3 hi)
|
|
{
|
|
if (Inside(from, lo, hi))
|
|
{
|
|
// Already inside last tick (zone created on top of them, spawn, teleport): push out
|
|
// along the shallowest face.
|
|
if (!Inside(to, lo, hi))
|
|
return false;
|
|
PushOutShallowest(ref to, ref v, lo, hi);
|
|
return true;
|
|
}
|
|
|
|
// Slab test on the segment from -> to: find where it enters the box, and through which face.
|
|
float tEnter = 0f, tExit = 1f;
|
|
int axis = -1;
|
|
bool fromLow = false;
|
|
for (var a = 0; a < 3; a++)
|
|
{
|
|
float p0 = Get(from, a), d = Get(to, a) - p0, l = Get(lo, a), h = Get(hi, a);
|
|
if (MathF.Abs(d) < 1e-6f)
|
|
{
|
|
if (p0 <= l || p0 >= h)
|
|
return false; // parallel to this slab and outside it
|
|
continue;
|
|
}
|
|
float t1 = (l - p0) / d, t2 = (h - p0) / d;
|
|
bool enterLow = t1 < t2;
|
|
if (!enterLow)
|
|
(t1, t2) = (t2, t1);
|
|
if (t1 > tEnter)
|
|
{
|
|
tEnter = t1;
|
|
axis = a;
|
|
fromLow = enterLow;
|
|
}
|
|
tExit = MathF.Min(tExit, t2);
|
|
if (tEnter > tExit)
|
|
return false;
|
|
}
|
|
if (axis < 0)
|
|
return false;
|
|
|
|
// Clamp just outside the entry face and kill velocity into it; the other two axes keep
|
|
// this tick's movement, so the player slides along the face.
|
|
Set(ref to, axis, fromLow ? Get(lo, axis) - Epsilon : Get(hi, axis) + Epsilon);
|
|
var va = Get(v, axis);
|
|
if (fromLow ? va > 0 : va < 0)
|
|
Set(ref v, axis, 0f);
|
|
return true;
|
|
}
|
|
|
|
private static void PushOutShallowest(ref Point3 p, ref Point3 v, Point3 lo, Point3 hi)
|
|
{
|
|
int best = 0;
|
|
bool toLow = true;
|
|
float bestDepth = float.MaxValue;
|
|
for (var a = 0; a < 3; a++)
|
|
{
|
|
float down = Get(p, a) - Get(lo, a), up = Get(hi, a) - Get(p, a);
|
|
if (down < bestDepth) { bestDepth = down; best = a; toLow = true; }
|
|
if (up < bestDepth) { bestDepth = up; best = a; toLow = false; }
|
|
}
|
|
Set(ref p, best, toLow ? Get(lo, best) - Epsilon : Get(hi, best) + Epsilon);
|
|
Set(ref v, best, 0f);
|
|
}
|
|
|
|
private static float Get(Point3 p, int axis) => axis switch { 0 => p.X, 1 => p.Y, _ => p.Z };
|
|
|
|
private static void Set(ref Point3 p, int axis, float value)
|
|
{
|
|
switch (axis)
|
|
{
|
|
case 0: p.X = value; break;
|
|
case 1: p.Y = value; break;
|
|
default: p.Z = value; break;
|
|
}
|
|
}
|
|
}
|