NoGoZones/Blocker.cs
Astra 006949d01c NoGoZones 0.14.0: admin-marked no-go zones for CS2
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.
2026-09-25 14:18:26 +01:00

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;
}
}
}