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
239
Beams.cs
Normal file
239
Beams.cs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
using System.Drawing;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
|
||||
|
||||
namespace NoGoZones;
|
||||
|
||||
// Preview and zone outline drawing. CSS 1.0.375 has no debug-overlay natives, so every line is a real env_beam
|
||||
// entity (same pattern jRandomSkills uses live). Beams are networked to every client, not just
|
||||
// the marking admin.
|
||||
public class BeamGroup
|
||||
{
|
||||
// Handles, not CBeam wrappers: a wrapper's pointer goes stale once the entity is gone,
|
||||
// a handle just stops resolving.
|
||||
private readonly List<CHandle<CBeam>> _beams = [];
|
||||
// Wall fill lines that Animate moves and recolours.
|
||||
private readonly List<FillRow> _rows = [];
|
||||
|
||||
// One fill line on a wall face, as two beams either side of the face's centre so a row passing
|
||||
// the sign can open a gap around it without changing its beam count.
|
||||
private sealed record FillRow(
|
||||
CHandle<CBeam>? Left, CHandle<CBeam>? Right, Func<float, float, Vector> At,
|
||||
float U0, float U1, float Cu, float Cv, float Hole, float V0, float Span, float Offset, int Index, int Count,
|
||||
Color Color);
|
||||
|
||||
public void Line(Vector start, Vector end, Color color, float width) => Spawn(start, end, color, width);
|
||||
|
||||
private CHandle<CBeam>? Spawn(Vector start, Vector end, Color color, float width)
|
||||
{
|
||||
var beam = Utilities.CreateEntityByName<CBeam>("env_beam");
|
||||
if (beam == null || !beam.IsValid)
|
||||
return null;
|
||||
|
||||
beam.Render = color;
|
||||
beam.Width = width;
|
||||
beam.Teleport(start, new QAngle(0, 0, 0), new Vector(0, 0, 0));
|
||||
beam.EndPos.X = end.X;
|
||||
beam.EndPos.Y = end.Y;
|
||||
beam.EndPos.Z = end.Z;
|
||||
beam.DispatchSpawn();
|
||||
|
||||
var handle = new CHandle<CBeam>(beam.EntityHandle.Raw);
|
||||
_beams.Add(handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
// A small 3-axis cross plus a tall vertical line so the point is findable from a distance.
|
||||
public void Marker(Vector p, Color color, float width)
|
||||
{
|
||||
const float half = 8f;
|
||||
Line(new Vector(p.X - half, p.Y, p.Z), new Vector(p.X + half, p.Y, p.Z), color, width);
|
||||
Line(new Vector(p.X, p.Y - half, p.Z), new Vector(p.X, p.Y + half, p.Z), color, width);
|
||||
Line(new Vector(p.X, p.Y, p.Z - half), new Vector(p.X, p.Y, p.Z + 48f), color, width);
|
||||
}
|
||||
|
||||
// The 12 edges of a (possibly turned) box.
|
||||
public void Box(ZoneShape shape, Color color, float width)
|
||||
{
|
||||
var (mins, maxs) = (shape.Mins, shape.Maxs);
|
||||
Vector C(bool x, bool y, bool z) =>
|
||||
shape.ToWorld(new Point3(x ? maxs.X : mins.X, y ? maxs.Y : mins.Y, z ? maxs.Z : mins.Z)).ToVector();
|
||||
|
||||
foreach (var z in new[] { false, true })
|
||||
{
|
||||
Line(C(false, false, z), C(true, false, z), color, width);
|
||||
Line(C(true, false, z), C(true, true, z), color, width);
|
||||
Line(C(true, true, z), C(false, true, z), color, width);
|
||||
Line(C(false, true, z), C(false, false, z), color, width);
|
||||
}
|
||||
foreach (var (x, y) in new[] { (false, false), (true, false), (true, true), (false, true) })
|
||||
Line(C(x, y, false), C(x, y, true), color, width);
|
||||
}
|
||||
|
||||
// Floor zones: a rectangle crossed corner to corner, flat just above lowZ. Wall zones: a
|
||||
// rectangle standing on the wall plane from lowZ to the top, filled with horizontal lines
|
||||
// (animated by Animate) and carrying a no-entry sign in the middle.
|
||||
public void Outline(ZoneShape shape, float lowZ, Color color, NoGoZonesConfig config)
|
||||
{
|
||||
var (mins, maxs) = (shape.Mins, shape.Maxs);
|
||||
var width = config.BeamWidth;
|
||||
if (!shape.IsWall)
|
||||
{
|
||||
var z = lowZ + 2f; // keep the beams from sinking into the floor
|
||||
var c = new Point3[]
|
||||
{ new(mins.X, mins.Y, z), new(maxs.X, mins.Y, z), new(maxs.X, maxs.Y, z), new(mins.X, maxs.Y, z) };
|
||||
var w = c.Select(p => shape.ToWorld(p).ToVector()).ToArray();
|
||||
for (var i = 0; i < 4; i++)
|
||||
Line(w[i], w[(i + 1) % 4], color, width);
|
||||
Line(w[0], w[2], color, width);
|
||||
Line(w[1], w[3], color, width);
|
||||
return;
|
||||
}
|
||||
|
||||
// Work in the face's 2D coordinates: u along the wall (local X), v up (Z).
|
||||
var y = (mins.Y + maxs.Y) / 2f;
|
||||
Vector At(float u, float v) => shape.ToWorld(new Point3(u, y, v)).ToVector();
|
||||
float u0 = mins.X, u1 = maxs.X, v0 = lowZ, v1 = maxs.Z;
|
||||
float cu = (u0 + u1) / 2f, cv = (v0 + v1) / 2f;
|
||||
|
||||
var radius = config.ShowSign ? MathF.Min(config.SignSize, 0.8f * MathF.Min(u1 - u0, v1 - v0)) / 2f : 0f;
|
||||
var hole = radius > 0 ? radius + 2f : 0f; // keep the fill and X a little clear of the sign
|
||||
|
||||
// Border.
|
||||
Line(At(u0, v0), At(u1, v0), color, width);
|
||||
Line(At(u1, v0), At(u1, v1), color, width);
|
||||
Line(At(u1, v1), At(u0, v1), color, width);
|
||||
Line(At(u0, v1), At(u0, v0), color, width);
|
||||
|
||||
// Fill: horizontal lines across the face in a dimmer shade, split around the sign. Evenly
|
||||
// spaced over the wall's height so scrolling wraps seamlessly.
|
||||
if (config.FillSpacing > 0)
|
||||
{
|
||||
var fb = Math.Clamp(config.FillBrightness, 0f, 1f);
|
||||
var fill = Color.FromArgb(color.A, (int)(color.R * fb), (int)(color.G * fb), (int)(color.B * fb));
|
||||
var count = Math.Max(1, (int)MathF.Floor((v1 - v0) / config.FillSpacing));
|
||||
var span = (v1 - v0) / count;
|
||||
for (var k = 0; k < count; k++)
|
||||
{
|
||||
var placeholder = At(u0, v0);
|
||||
var row = new FillRow(
|
||||
Spawn(placeholder, placeholder, fill, config.FillWidth),
|
||||
Spawn(placeholder, placeholder, fill, config.FillWidth),
|
||||
At, u0, u1, cu, cv, hole, v0, (v1 - v0), k * span + span / 2f, k, count, fill);
|
||||
Place(row, 0f);
|
||||
_rows.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
if (radius > 0)
|
||||
Sign(cu, cv, radius, At);
|
||||
}
|
||||
|
||||
// A no-entry sign: a red disc of stacked horizontal beams with a white bar across the middle.
|
||||
private void Sign(float cu, float cv, float radius, Func<float, float, Vector> at)
|
||||
{
|
||||
var red = Color.FromArgb(255, 220, 0, 0);
|
||||
var white = Color.FromArgb(255, 255, 255, 255);
|
||||
// Rows overlap a little so the disc reads as solid.
|
||||
var rowWidth = MathF.Max(2f, radius / 6f);
|
||||
var step = rowWidth * 0.75f;
|
||||
float barHalfWidth = radius * 0.75f, barHalfHeight = radius * 0.2f;
|
||||
|
||||
for (var dv = -radius + step / 2f; dv < radius; dv += step)
|
||||
{
|
||||
var half = MathF.Sqrt(radius * radius - dv * dv);
|
||||
var v = cv + dv;
|
||||
if (MathF.Abs(dv) <= barHalfHeight)
|
||||
{
|
||||
Line(at(cu - half, v), at(cu - barHalfWidth, v), red, rowWidth);
|
||||
Line(at(cu - barHalfWidth, v), at(cu + barHalfWidth, v), white, rowWidth);
|
||||
Line(at(cu + barHalfWidth, v), at(cu + half, v), red, rowWidth);
|
||||
}
|
||||
else
|
||||
{
|
||||
Line(at(cu - half, v), at(cu + half, v), red, rowWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var handle in _beams)
|
||||
{
|
||||
var beam = handle.Value;
|
||||
if (beam is { IsValid: true })
|
||||
beam.Remove();
|
||||
}
|
||||
_beams.Clear();
|
||||
_rows.Clear();
|
||||
}
|
||||
|
||||
// After a map change the entities are already gone; just drop the handles.
|
||||
public void Forget()
|
||||
{
|
||||
_beams.Clear();
|
||||
_rows.Clear();
|
||||
}
|
||||
|
||||
// Scrolls the wall fill lines (FillScrollSpeed units/s, wrapping within the wall) and/or runs a
|
||||
// brightness wave up them (FillPulse). Called from a repeating timer.
|
||||
public void Animate(float time, NoGoZonesConfig config)
|
||||
{
|
||||
var scroll = config.FillScrollSpeed != 0f;
|
||||
if (_rows.Count == 0 || (!scroll && !config.FillPulse))
|
||||
return;
|
||||
|
||||
foreach (var row in _rows)
|
||||
{
|
||||
if (scroll)
|
||||
Place(row, time * config.FillScrollSpeed);
|
||||
if (config.FillPulse)
|
||||
{
|
||||
var period = MathF.Max(config.PulsePeriod, 0.1f);
|
||||
var wave = 0.5f + 0.5f * MathF.Sin(2f * MathF.PI * (time / period - (float)row.Index / row.Count));
|
||||
var min = Math.Clamp(config.PulseMinBrightness, 0f, 1f);
|
||||
var b = min + (1f - min) * wave;
|
||||
var color = Color.FromArgb(row.Color.A, (int)(row.Color.R * b), (int)(row.Color.G * b), (int)(row.Color.B * b));
|
||||
Recolor(row.Left, color);
|
||||
Recolor(row.Right, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Puts a fill row at its scrolled height and re-cuts it around the sign.
|
||||
private static void Place(FillRow row, float scrolled)
|
||||
{
|
||||
var v = row.V0 + Mod(row.Offset + scrolled, row.Span);
|
||||
var dv = v - row.Cv;
|
||||
var du = row.Hole > 0 && MathF.Abs(dv) < row.Hole ? MathF.Sqrt(row.Hole * row.Hole - dv * dv) : 0f;
|
||||
// Left half runs u0 -> centre (minus the hole), right half centre (plus the hole) -> u1; a
|
||||
// half the hole swallows collapses to zero length rather than disappearing.
|
||||
var leftEnd = MathF.Max(row.U0, row.Cu - du);
|
||||
var rightStart = MathF.Min(row.U1, row.Cu + du);
|
||||
Move(row.Left, row.At(row.U0, v), row.At(leftEnd, v));
|
||||
Move(row.Right, row.At(rightStart, v), row.At(row.U1, v));
|
||||
}
|
||||
|
||||
private static float Mod(float a, float m) => ((a % m) + m) % m;
|
||||
|
||||
private static void Move(CHandle<CBeam>? handle, Vector start, Vector end)
|
||||
{
|
||||
if (handle?.Value is not { IsValid: true } beam)
|
||||
return;
|
||||
beam.Teleport(start, null, null);
|
||||
beam.EndPos.X = end.X;
|
||||
beam.EndPos.Y = end.Y;
|
||||
beam.EndPos.Z = end.Z;
|
||||
Utilities.SetStateChanged(beam, "CBeam", "m_vecEndPos");
|
||||
}
|
||||
|
||||
private static void Recolor(CHandle<CBeam>? handle, Color color)
|
||||
{
|
||||
if (handle?.Value is not { IsValid: true } beam)
|
||||
return;
|
||||
beam.Render = color;
|
||||
Utilities.SetStateChanged(beam, "CBaseModelEntity", "m_clrRender");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue