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> _beams = []; // Wall fill lines that Animate moves and recolours. private readonly List _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? Left, CHandle? Right, Func 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? Spawn(Vector start, Vector end, Color color, float width) { var beam = Utilities.CreateEntityByName("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(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 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? 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? handle, Color color) { if (handle?.Value is not { IsValid: true } beam) return; beam.Render = color; Utilities.SetStateChanged(beam, "CBaseModelEntity", "m_clrRender"); } }