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.
This commit is contained in:
commit
006949d01c
8 changed files with 1470 additions and 0 deletions
188
Blocker.cs
Normal file
188
Blocker.cs
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue