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 _lastOrigin = []; public void Reset() => _lastOrigin.Clear(); public void Forget(uint pawnIndex) => _lastOrigin.Remove(pawnIndex); public void Tick(IReadOnlyList 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; } } }