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.
239 lines
10 KiB
C#
239 lines
10 KiB
C#
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");
|
|
}
|
|
}
|