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
189
Zone.cs
Normal file
189
Zone.cs
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
|
||||
|
||||
namespace NoGoZones;
|
||||
|
||||
// Plain float triple for JSON; CSS's Vector wraps native memory and doesn't serialize.
|
||||
public record struct Point3(float X, float Y, float Z)
|
||||
{
|
||||
public static Point3 From(Vector v) => new(v.X, v.Y, v.Z);
|
||||
public Vector ToVector() => new(X, Y, Z);
|
||||
}
|
||||
|
||||
public record struct Rgb(byte R, byte G, byte B);
|
||||
|
||||
public class Zone
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public List<Point3> Points { get; set; } = [];
|
||||
// Box in the zone's own frame (world rotated by -Yaw about Z). Yaw is 0 for floor zones and for
|
||||
// every zone saved before wall zones existed, so there Mins/Maxs are plain world coordinates.
|
||||
public Point3 Mins { get; set; }
|
||||
public Point3 Maxs { get; set; }
|
||||
public float Yaw { get; set; }
|
||||
public bool IsWall { get; set; }
|
||||
public Rgb Color { get; set; }
|
||||
// Inactive zones stay saved but neither block nor draw. Missing in older files, so defaults on.
|
||||
public bool Active { get; set; } = true;
|
||||
|
||||
[JsonIgnore]
|
||||
public ZoneShape Shape => new(Mins, Maxs, Yaw, IsWall);
|
||||
|
||||
// Lowest marked point, where the outline is drawn. Falls back to the box bottom if Points was
|
||||
// emptied by hand.
|
||||
[JsonIgnore]
|
||||
public float LowZ => Points.Count > 0 ? Points.Min(p => p.Z) : Mins.Z;
|
||||
}
|
||||
|
||||
// A box that can be turned about Z: Mins/Maxs live in a local frame rotated by Yaw degrees.
|
||||
public readonly record struct ZoneShape(Point3 Mins, Point3 Maxs, float Yaw, bool IsWall)
|
||||
{
|
||||
private readonly float _cos = MathF.Cos(Yaw * MathF.PI / 180f);
|
||||
private readonly float _sin = MathF.Sin(Yaw * MathF.PI / 180f);
|
||||
|
||||
public float Cos => _cos;
|
||||
public float Sin => _sin;
|
||||
|
||||
public Point3 ToLocal(Point3 w) => new(w.X * _cos + w.Y * _sin, -w.X * _sin + w.Y * _cos, w.Z);
|
||||
// Distance from a world point to the nearest part of the box; 0 if inside.
|
||||
public float DistanceTo(Point3 world)
|
||||
{
|
||||
var p = ToLocal(world);
|
||||
float dx = MathF.Max(0, MathF.Max(Mins.X - p.X, p.X - Maxs.X));
|
||||
float dy = MathF.Max(0, MathF.Max(Mins.Y - p.Y, p.Y - Maxs.Y));
|
||||
float dz = MathF.Max(0, MathF.Max(Mins.Z - p.Z, p.Z - Maxs.Z));
|
||||
return MathF.Sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
public Point3 ToWorld(Point3 l) => new(l.X * _cos - l.Y * _sin, l.X * _sin + l.Y * _cos, l.Z);
|
||||
|
||||
// Two kinds of zone, told apart by the plane through the 4 points:
|
||||
// - Floor (plane closer to horizontal): an axis-aligned box over the points' footprint. Z runs
|
||||
// from BottomMargin below the lowest point up to whichever is higher: MinHeight above the
|
||||
// lowest point, or TopMargin above the highest.
|
||||
// - Wall (plane closer to vertical, e.g. marked on a tunnel mouth): a WallThickness-deep slab
|
||||
// turned to lie in that plane, spanning the points along the face and from BottomMargin below
|
||||
// the lowest point up to the highest.
|
||||
// An explicit topZ (css_zone_height) replaces the top in both cases.
|
||||
public static ZoneShape Compute(IReadOnlyList<Point3> points, NoGoZonesConfig config, float? topZ = null)
|
||||
{
|
||||
float lowZ = points.Min(p => p.Z), highZ = points.Max(p => p.Z);
|
||||
|
||||
if (WallNormal(points) is { } n)
|
||||
{
|
||||
// Local X runs along the face, local Y straight through it.
|
||||
var yaw = MathF.Atan2(n.X, -n.Y) * 180f / MathF.PI;
|
||||
var frame = new ZoneShape(default, default, yaw, true);
|
||||
var local = points.Select(frame.ToLocal).ToList();
|
||||
float midY = local.Average(p => p.Y), half = config.WallThickness / 2f;
|
||||
return frame with
|
||||
{
|
||||
Mins = new Point3(local.Min(p => p.X), midY - half, lowZ - config.BottomMargin),
|
||||
Maxs = new Point3(local.Max(p => p.X), midY + half, topZ ?? highZ),
|
||||
};
|
||||
}
|
||||
|
||||
return new ZoneShape(
|
||||
new Point3(points.Min(p => p.X), points.Min(p => p.Y), lowZ - config.BottomMargin),
|
||||
new Point3(points.Max(p => p.X), points.Max(p => p.Y),
|
||||
topZ ?? Math.Max(lowZ + config.MinHeight, highZ + config.TopMargin)),
|
||||
0f, false);
|
||||
}
|
||||
|
||||
// Horizontal unit normal of the points' plane if it's a wall, otherwise null. Uses the largest
|
||||
// cross product among the three pairs from the first point, so marking order doesn't matter.
|
||||
private static Point3? WallNormal(IReadOnlyList<Point3> points)
|
||||
{
|
||||
var p0 = points[0];
|
||||
Point3 Sub(Point3 a) => new(a.X - p0.X, a.Y - p0.Y, a.Z - p0.Z);
|
||||
static Point3 Cross(Point3 a, Point3 b) => new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X);
|
||||
static float Len(Point3 v) => MathF.Sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z);
|
||||
|
||||
var best = default(Point3);
|
||||
for (var i = 1; i < points.Count; i++)
|
||||
for (var j = i + 1; j < points.Count; j++)
|
||||
{
|
||||
var c = Cross(Sub(points[i]), Sub(points[j]));
|
||||
if (Len(c) > Len(best))
|
||||
best = c;
|
||||
}
|
||||
|
||||
var len = Len(best);
|
||||
if (len < 1f || MathF.Abs(best.Z / len) >= 0.5f)
|
||||
return null; // all in a line, or nearer horizontal than vertical: a floor zone
|
||||
var h = MathF.Sqrt(best.X * best.X + best.Y * best.Y);
|
||||
return new Point3(best.X / h, best.Y / h, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
// One JSON file per map: configs/plugins/NoGoZones/zones/<map>.json, holding a list of zones.
|
||||
public static class ZoneStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
IncludeFields = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
};
|
||||
|
||||
public static string ZonesDirectory => Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp",
|
||||
"configs", "plugins", "NoGoZones", "zones");
|
||||
|
||||
public static string PathFor(string map)
|
||||
{
|
||||
// Workshop maps can carry path separators in their name.
|
||||
var safe = string.Concat(map.Select(c => Path.GetInvalidFileNameChars().Contains(c) || c == '/' ? '_' : c));
|
||||
return Path.Combine(ZonesDirectory, $"{safe}.json");
|
||||
}
|
||||
|
||||
public static List<Zone> Load(string map)
|
||||
{
|
||||
var path = PathFor(map);
|
||||
if (!File.Exists(path))
|
||||
return [];
|
||||
return JsonSerializer.Deserialize<List<Zone>>(File.ReadAllText(path), JsonOptions) ?? [];
|
||||
}
|
||||
|
||||
public static void Save(string map, List<Zone> zones)
|
||||
{
|
||||
Directory.CreateDirectory(ZonesDirectory);
|
||||
var path = PathFor(map);
|
||||
// Write-then-rename so a crash mid-write never leaves a truncated zone file behind.
|
||||
File.WriteAllText(path + ".tmp", JsonSerializer.Serialize(zones, JsonOptions));
|
||||
File.Move(path + ".tmp", path, overwrite: true);
|
||||
}
|
||||
}
|
||||
|
||||
public class NoGoZonesConfig : BasePluginConfig
|
||||
{
|
||||
[JsonPropertyName("MinHeight")] public float MinHeight { get; set; } = 128f;
|
||||
[JsonPropertyName("TopMargin")] public float TopMargin { get; set; } = 16f;
|
||||
// Depth of a wall zone as marked (outline, per-tick block), measured through the face.
|
||||
[JsonPropertyName("WallThickness")] public float WallThickness { get; set; } = 16f;
|
||||
[JsonPropertyName("BottomMargin")] public float BottomMargin { get; set; } = 8f;
|
||||
[JsonPropertyName("DefaultColor")] public Rgb DefaultColor { get; set; } = new(255, 0, 0);
|
||||
[JsonPropertyName("BeamWidth")] public float BeamWidth { get; set; } = 1.5f;
|
||||
// Wall zones: horizontal fill lines every FillSpacing units (0 = off), FillWidth wide.
|
||||
[JsonPropertyName("FillSpacing")] public float FillSpacing { get; set; } = 12f;
|
||||
[JsonPropertyName("FillWidth")] public float FillWidth { get; set; } = 4f;
|
||||
// Fill lines' brightness relative to the zone colour (0-1). Beams draw additively, so dim lines
|
||||
// all but vanish against bright walls, especially from a distance.
|
||||
[JsonPropertyName("FillBrightness")] public float FillBrightness { get; set; } = 0.8f;
|
||||
// Fill lines scroll up at this many units/s (negative = down, 0 = still), and/or carry a
|
||||
// brightness wave upwards, one cycle every PulsePeriod seconds.
|
||||
[JsonPropertyName("FillScrollSpeed")] public float FillScrollSpeed { get; set; } = 12f;
|
||||
[JsonPropertyName("FillPulse")] public bool FillPulse { get; set; } = true;
|
||||
[JsonPropertyName("PulsePeriod")] public float PulsePeriod { get; set; } = 1.5f;
|
||||
// Dimmest point of the pulse, as a fraction of FillBrightness (0-1).
|
||||
[JsonPropertyName("PulseMinBrightness")] public float PulseMinBrightness { get; set; } = 0.6f;
|
||||
// Wall zones: a no-entry sign in the middle, up to SignSize units across.
|
||||
[JsonPropertyName("ShowSign")] public bool ShowSign { get; set; } = true;
|
||||
[JsonPropertyName("SignSize")] public float SignSize { get; set; } = 48f;
|
||||
// Draw confirmed zones (outline; wall zones also get the fill lines and sign).
|
||||
[JsonPropertyName("ShowZones")] public bool ShowZones { get; set; } = true;
|
||||
// Let noclipping players (admins flying around) pass through zones.
|
||||
[JsonPropertyName("IgnoreNoclip")] public bool IgnoreNoclip { get; set; } = true;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue