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
502
NoGoZones.cs
Normal file
502
NoGoZones.cs
Normal file
|
|
@ -0,0 +1,502 @@
|
|||
using System.Drawing;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Attributes.Registration;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using CounterStrikeSharp.API.Modules.Timers;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
|
||||
|
||||
namespace NoGoZones;
|
||||
|
||||
public class NoGoZones : BasePlugin, IPluginConfig<NoGoZonesConfig>
|
||||
{
|
||||
public override string ModuleName => "NoGoZones";
|
||||
public override string ModuleVersion => "0.14.0";
|
||||
public override string ModuleAuthor => "astra";
|
||||
public override string ModuleDescription => "Admin-marked box zones that players can't enter";
|
||||
|
||||
private const string Permission = "@css/root";
|
||||
private const float TraceDistance = 8192f;
|
||||
private const int NearCount = 3;
|
||||
|
||||
private class Session
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public Rgb Color { get; set; }
|
||||
public List<Point3> Points { get; } = [];
|
||||
public BeamGroup Beams { get; } = new();
|
||||
public ZoneShape? Box { get; set; }
|
||||
// Set by css_zone_height; replaces the configured height rules.
|
||||
public float? TopZ { get; set; }
|
||||
}
|
||||
|
||||
public NoGoZonesConfig Config { get; set; } = new();
|
||||
|
||||
// Keyed by SteamID64 so several admins can mark different zones at once.
|
||||
private readonly Dictionary<ulong, Session> _sessions = [];
|
||||
private List<Zone> _zones = [];
|
||||
private readonly Blocker _blocker = new();
|
||||
private readonly BeamGroup _zoneBeams = new();
|
||||
|
||||
public void OnConfigParsed(NoGoZonesConfig config) => Config = config;
|
||||
|
||||
public override void Load(bool hotReload)
|
||||
{
|
||||
RegisterListener<Listeners.OnTick>(() => _blocker.Tick(_zones, Config.IgnoreNoclip));
|
||||
RegisterListener<Listeners.OnMapStart>(OnMapStart);
|
||||
RegisterListener<Listeners.OnMapEnd>(OnMapEnd);
|
||||
RegisterEventHandler<EventPlayerDisconnect>(OnPlayerDisconnect);
|
||||
AddTimer(0.1f, () => _zoneBeams.Animate(Server.CurrentTime, Config), TimerFlags.REPEAT);
|
||||
// The round restart removes spawned entities, zone outlines included, so redraw each round.
|
||||
RegisterEventHandler<EventRoundStart>((_, _) =>
|
||||
{
|
||||
DrawZones();
|
||||
return HookResult.Continue;
|
||||
});
|
||||
|
||||
if (hotReload)
|
||||
{
|
||||
LoadZones(Server.MapName);
|
||||
DrawZones();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
foreach (var session in _sessions.Values)
|
||||
session.Beams.Clear();
|
||||
_sessions.Clear();
|
||||
_zoneBeams.Clear();
|
||||
_blocker.Reset();
|
||||
}
|
||||
|
||||
private void OnMapStart(string map)
|
||||
{
|
||||
// Entities from the old map are gone; sessions and their beams go with them.
|
||||
foreach (var session in _sessions.Values)
|
||||
session.Beams.Forget();
|
||||
_sessions.Clear();
|
||||
_zoneBeams.Forget();
|
||||
_blocker.Reset();
|
||||
LoadZones(map);
|
||||
}
|
||||
|
||||
private void OnMapEnd()
|
||||
{
|
||||
foreach (var session in _sessions.Values)
|
||||
session.Beams.Forget();
|
||||
_sessions.Clear();
|
||||
_zones = [];
|
||||
_zoneBeams.Forget();
|
||||
_blocker.Reset();
|
||||
}
|
||||
|
||||
private HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo info)
|
||||
{
|
||||
var player = @event.Userid;
|
||||
if (player is { IsValid: true } && _sessions.Remove(player.SteamID, out var session))
|
||||
session.Beams.Clear();
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
private void DrawZones()
|
||||
{
|
||||
_zoneBeams.Clear();
|
||||
if (!Config.ShowZones)
|
||||
return;
|
||||
foreach (var zone in _zones.Where(z => z.Active))
|
||||
_zoneBeams.Outline(zone.Shape, zone.LowZ, ToColor(zone.Color), Config);
|
||||
}
|
||||
|
||||
private bool LoadZones(string map)
|
||||
{
|
||||
try
|
||||
{
|
||||
_zones = ZoneStore.Load(map);
|
||||
_blocker.Reset();
|
||||
Logger.LogInformation("Loaded {Count} zone(s) for {Map}", _zones.Count, map);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Keep whatever was active rather than silently dropping every zone on a bad edit.
|
||||
Logger.LogError(ex, "Failed to load zones for {Map} from {Path}", map, ZoneStore.PathFor(map));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[ConsoleCommand("css_zone_start", "Start marking a no-go zone")]
|
||||
[CommandHelper(minArgs: 1, usage: "<name>", whoCanExecute: CommandUsage.CLIENT_ONLY)]
|
||||
[RequiresPermissions(Permission)]
|
||||
public void OnZoneStart(CCSPlayerController? player, CommandInfo command)
|
||||
{
|
||||
if (player is not { IsValid: true })
|
||||
return;
|
||||
|
||||
var name = command.GetArg(1).Trim();
|
||||
if (_zones.Any(z => z.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
Reply(player, $"A zone named {ChatColors.Yellow}{name}{ChatColors.Default} already exists on this map. Remove it first.");
|
||||
return;
|
||||
}
|
||||
if (_sessions.Any(kv => kv.Key != player.SteamID && kv.Value.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
Reply(player, $"Another admin is already marking {ChatColors.Yellow}{name}{ChatColors.Default}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_sessions.Remove(player.SteamID, out var old))
|
||||
old.Beams.Clear();
|
||||
|
||||
_sessions[player.SteamID] = new Session { Name = name, Color = Config.DefaultColor };
|
||||
Reply(player, $"Marking {ChatColors.Yellow}{name}{ChatColors.Default}. Aim and run {ChatColors.Green}css_zone_mark{ChatColors.Default} 4 times.");
|
||||
}
|
||||
|
||||
[ConsoleCommand("css_zone_color", "Set the preview color of your zone")]
|
||||
[CommandHelper(minArgs: 3, usage: "<r> <g> <b>", whoCanExecute: CommandUsage.CLIENT_ONLY)]
|
||||
[RequiresPermissions(Permission)]
|
||||
public void OnZoneColor(CCSPlayerController? player, CommandInfo command)
|
||||
{
|
||||
if (player is not { IsValid: true } || GetSession(player) is not { } session)
|
||||
return;
|
||||
|
||||
if (!byte.TryParse(command.GetArg(1), out var r) || !byte.TryParse(command.GetArg(2), out var g) ||
|
||||
!byte.TryParse(command.GetArg(3), out var b))
|
||||
{
|
||||
Reply(player, "Color values must be 0-255.");
|
||||
return;
|
||||
}
|
||||
|
||||
session.Color = new Rgb(r, g, b);
|
||||
Redraw(session);
|
||||
Reply(player, $"Color set to {r} {g} {b}.");
|
||||
}
|
||||
|
||||
[ConsoleCommand("css_zone_mark", "Mark a zone corner at your crosshair")]
|
||||
[CommandHelper(minArgs: 0, usage: "", whoCanExecute: CommandUsage.CLIENT_ONLY)]
|
||||
[RequiresPermissions(Permission)]
|
||||
public void OnZoneMark(CCSPlayerController? player, CommandInfo command)
|
||||
{
|
||||
if (player is not { IsValid: true } || GetSession(player) is not { } session)
|
||||
return;
|
||||
if (session.Points.Count >= 4)
|
||||
{
|
||||
Reply(player, $"All 4 points are marked. Use {ChatColors.Green}css_zone_confirm{ChatColors.Default} or {ChatColors.Green}css_zone_cancel{ChatColors.Default}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (TraceCrosshair(player) is not { } hit)
|
||||
return;
|
||||
|
||||
session.Points.Add(hit);
|
||||
session.Beams.Marker(hit.ToVector(), ToColor(session.Color), Config.BeamWidth);
|
||||
Reply(player, $"Point {session.Points.Count}/4 at {hit.X:F0} {hit.Y:F0} {hit.Z:F0}.");
|
||||
|
||||
if (session.Points.Count == 4)
|
||||
ShowPreview(player, session);
|
||||
}
|
||||
|
||||
[ConsoleCommand("css_zone_height", "Set your zone's top: units above the lowest point, or your crosshair")]
|
||||
[CommandHelper(minArgs: 0, usage: "[units]", whoCanExecute: CommandUsage.CLIENT_ONLY)]
|
||||
[RequiresPermissions(Permission)]
|
||||
public void OnZoneHeight(CCSPlayerController? player, CommandInfo command)
|
||||
{
|
||||
if (player is not { IsValid: true } || GetSession(player) is not { } session)
|
||||
return;
|
||||
if (session.Points.Count < 4)
|
||||
{
|
||||
Reply(player, $"Mark all 4 points first ({session.Points.Count}/4).");
|
||||
return;
|
||||
}
|
||||
|
||||
var lowZ = session.Points.Min(p => p.Z);
|
||||
float top;
|
||||
if (command.ArgCount > 1)
|
||||
{
|
||||
if (!float.TryParse(command.GetArg(1), System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var units) || units <= 0)
|
||||
{
|
||||
Reply(player, "Height must be a positive number of units.");
|
||||
return;
|
||||
}
|
||||
top = lowZ + units;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No argument: the top goes where the crosshair is, e.g. the underside of a tunnel roof.
|
||||
if (TraceCrosshair(player) is not { } hit)
|
||||
return;
|
||||
if (hit.Z <= lowZ + 1f)
|
||||
{
|
||||
Reply(player, "Aim above the lowest marked point to set the top.");
|
||||
return;
|
||||
}
|
||||
top = hit.Z;
|
||||
}
|
||||
|
||||
session.TopZ = top;
|
||||
session.Beams.Clear();
|
||||
foreach (var p in session.Points)
|
||||
session.Beams.Marker(p.ToVector(), ToColor(session.Color), Config.BeamWidth);
|
||||
ShowPreview(player, session);
|
||||
}
|
||||
|
||||
[ConsoleCommand("css_zone_confirm", "Save the previewed zone")]
|
||||
[CommandHelper(minArgs: 0, usage: "", whoCanExecute: CommandUsage.CLIENT_ONLY)]
|
||||
[RequiresPermissions(Permission)]
|
||||
public void OnZoneConfirm(CCSPlayerController? player, CommandInfo command)
|
||||
{
|
||||
if (player is not { IsValid: true } || GetSession(player) is not { } session)
|
||||
return;
|
||||
if (session.Box is not { } box)
|
||||
{
|
||||
Reply(player, $"Mark all 4 points first ({session.Points.Count}/4).");
|
||||
return;
|
||||
}
|
||||
if (_zones.Any(z => z.Name.Equals(session.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
Reply(player, $"A zone named {ChatColors.Yellow}{session.Name}{ChatColors.Default} was saved meanwhile. Cancel and pick another name.");
|
||||
return;
|
||||
}
|
||||
|
||||
var zone = new Zone
|
||||
{
|
||||
Name = session.Name,
|
||||
Points = [..session.Points],
|
||||
Mins = box.Mins,
|
||||
Maxs = box.Maxs,
|
||||
Yaw = box.Yaw,
|
||||
IsWall = box.IsWall,
|
||||
Color = session.Color,
|
||||
};
|
||||
var updated = new List<Zone>(_zones) { zone };
|
||||
try
|
||||
{
|
||||
ZoneStore.Save(Server.MapName, updated);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to save zone {Name}", zone.Name);
|
||||
Reply(player, $"{ChatColors.Red}Saving failed{ChatColors.Default}, see the server console. The preview is kept.");
|
||||
return;
|
||||
}
|
||||
|
||||
_zones = updated;
|
||||
session.Beams.Clear();
|
||||
DrawZones();
|
||||
_sessions.Remove(player.SteamID);
|
||||
Reply(player, $"Zone {ChatColors.Yellow}{zone.Name}{ChatColors.Default} saved and active.");
|
||||
}
|
||||
|
||||
[ConsoleCommand("css_zone_cancel", "Discard your in-progress zone")]
|
||||
[CommandHelper(minArgs: 0, usage: "", whoCanExecute: CommandUsage.CLIENT_ONLY)]
|
||||
[RequiresPermissions(Permission)]
|
||||
public void OnZoneCancel(CCSPlayerController? player, CommandInfo command)
|
||||
{
|
||||
if (player is not { IsValid: true } || GetSession(player) is not { } session)
|
||||
return;
|
||||
session.Beams.Clear();
|
||||
_sessions.Remove(player.SteamID);
|
||||
Reply(player, $"Discarded {ChatColors.Yellow}{session.Name}{ChatColors.Default}.");
|
||||
}
|
||||
|
||||
[ConsoleCommand("css_zone_near", "List the zones closest to you")]
|
||||
[CommandHelper(minArgs: 0, usage: "", whoCanExecute: CommandUsage.CLIENT_ONLY)]
|
||||
[RequiresPermissions(Permission)]
|
||||
public void OnZoneNear(CCSPlayerController? player, CommandInfo command)
|
||||
{
|
||||
if (player is not { IsValid: true })
|
||||
return;
|
||||
if (player.PlayerPawn.Value is not { IsValid: true } pawn || pawn.AbsOrigin is not { } origin)
|
||||
{
|
||||
Reply(player, "You need to be alive to find nearby zones.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Inactive zones too: they're invisible, which is exactly when you'd need their name.
|
||||
var here = Point3.From(origin);
|
||||
var nearest = _zones
|
||||
.Select(z => (Zone: z, Distance: z.Shape.DistanceTo(here)))
|
||||
.OrderBy(x => x.Distance)
|
||||
.Take(NearCount)
|
||||
.ToList();
|
||||
if (nearest.Count == 0)
|
||||
{
|
||||
Reply(player, $"No zones on {Server.MapName}.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (zone, distance) in nearest)
|
||||
Reply(player, $"{ChatColors.Yellow}{zone.Name}{ChatColors.Default}: {distance:F0} units, " +
|
||||
$"{(zone.Active ? "active" : $"{ChatColors.Grey}inactive{ChatColors.Default}")}, {(zone.IsWall ? "wall" : "floor")}");
|
||||
}
|
||||
|
||||
[ConsoleCommand("css_zone_active", "Turn a saved zone on or off")]
|
||||
[CommandHelper(minArgs: 2, usage: "<name> <1|0>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
[RequiresPermissions(Permission)]
|
||||
public void OnZoneActive(CCSPlayerController? player, CommandInfo command)
|
||||
{
|
||||
var name = command.GetArg(1).Trim();
|
||||
var zone = _zones.FirstOrDefault(z => z.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
if (zone == null)
|
||||
{
|
||||
command.ReplyToCommand($"[NoGoZones] No zone named {name} on this map.");
|
||||
return;
|
||||
}
|
||||
|
||||
bool active;
|
||||
switch (command.GetArg(2).Trim().ToLowerInvariant())
|
||||
{
|
||||
case "1" or "on" or "true": active = true; break;
|
||||
case "0" or "off" or "false": active = false; break;
|
||||
default:
|
||||
command.ReplyToCommand("[NoGoZones] Use 1 (active) or 0 (inactive).");
|
||||
return;
|
||||
}
|
||||
|
||||
var previous = zone.Active;
|
||||
zone.Active = active;
|
||||
try
|
||||
{
|
||||
ZoneStore.Save(Server.MapName, _zones);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
zone.Active = previous;
|
||||
Logger.LogError(ex, "Failed to save zones after toggling {Name}", zone.Name);
|
||||
command.ReplyToCommand("[NoGoZones] Saving failed, see the server console. Zone unchanged.");
|
||||
return;
|
||||
}
|
||||
|
||||
DrawZones();
|
||||
command.ReplyToCommand($"[NoGoZones] {zone.Name} is now {(active ? "active" : "inactive")}.");
|
||||
}
|
||||
|
||||
[ConsoleCommand("css_zone_remove", "Remove a saved zone")]
|
||||
[CommandHelper(minArgs: 1, usage: "<name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
[RequiresPermissions(Permission)]
|
||||
public void OnZoneRemove(CCSPlayerController? player, CommandInfo command)
|
||||
{
|
||||
var name = command.GetArg(1).Trim();
|
||||
var zone = _zones.FirstOrDefault(z => z.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
if (zone == null)
|
||||
{
|
||||
command.ReplyToCommand($"[NoGoZones] No zone named {name} on this map.");
|
||||
return;
|
||||
}
|
||||
|
||||
var updated = _zones.Where(z => z != zone).ToList();
|
||||
try
|
||||
{
|
||||
ZoneStore.Save(Server.MapName, updated);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to save zones after removing {Name}", zone.Name);
|
||||
command.ReplyToCommand("[NoGoZones] Saving failed, see the server console. Zone left active.");
|
||||
return;
|
||||
}
|
||||
|
||||
_zones = updated;
|
||||
DrawZones();
|
||||
command.ReplyToCommand($"[NoGoZones] Removed {zone.Name}.");
|
||||
}
|
||||
|
||||
[ConsoleCommand("css_zone_list", "List zones on this map")]
|
||||
[CommandHelper(minArgs: 0, usage: "", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
[RequiresPermissions(Permission)]
|
||||
public void OnZoneList(CCSPlayerController? player, CommandInfo command)
|
||||
{
|
||||
if (_zones.Count == 0)
|
||||
{
|
||||
command.ReplyToCommand($"[NoGoZones] No zones on {Server.MapName}.");
|
||||
return;
|
||||
}
|
||||
|
||||
command.ReplyToCommand($"[NoGoZones] {_zones.Count} zone(s) on {Server.MapName}:");
|
||||
foreach (var z in _zones)
|
||||
command.ReplyToCommand($" {z.Name}: {(z.Active ? "active" : "inactive")}, {(z.IsWall ? "wall" : "floor")}, {Size(z.Shape)}");
|
||||
}
|
||||
|
||||
[ConsoleCommand("css_zone_reload", "Reload zones for this map from disk")]
|
||||
[CommandHelper(minArgs: 0, usage: "", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
[RequiresPermissions(Permission)]
|
||||
public void OnZoneReload(CCSPlayerController? player, CommandInfo command)
|
||||
{
|
||||
var loaded = LoadZones(Server.MapName);
|
||||
DrawZones();
|
||||
command.ReplyToCommand(loaded
|
||||
? $"[NoGoZones] Loaded {_zones.Count} zone(s) for {Server.MapName}."
|
||||
: "[NoGoZones] Reload failed (bad JSON?), see the server console. Previous zones kept.");
|
||||
}
|
||||
|
||||
private Session? GetSession(CCSPlayerController player)
|
||||
{
|
||||
if (_sessions.TryGetValue(player.SteamID, out var session))
|
||||
return session;
|
||||
Reply(player, $"No zone in progress. Start one with {ChatColors.Green}css_zone_start <name>{ChatColors.Default}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ShowPreview(CCSPlayerController player, Session session)
|
||||
{
|
||||
var shape = ZoneShape.Compute(session.Points, Config, session.TopZ);
|
||||
session.Box = shape;
|
||||
session.Beams.Box(shape, ToColor(session.Color), Config.BeamWidth);
|
||||
Reply(player, $"Preview ({(shape.IsWall ? "wall" : "floor")}): {Size(shape)}. " +
|
||||
$"{ChatColors.Green}css_zone_height{ChatColors.Default} to change the top, " +
|
||||
$"{ChatColors.Green}css_zone_confirm{ChatColors.Default} to save, {ChatColors.Green}css_zone_cancel{ChatColors.Default} to discard.");
|
||||
}
|
||||
|
||||
// Point under the player's crosshair, world geometry only (aiming past a player doesn't hit them).
|
||||
private static Point3? TraceCrosshair(CCSPlayerController player)
|
||||
{
|
||||
var pawn = player.PlayerPawn.Value;
|
||||
if (pawn is not { IsValid: true } || pawn.AbsOrigin is not { } origin)
|
||||
{
|
||||
Reply(player, "You need to be alive to aim.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var eye = new Vector(origin.X, origin.Y, origin.Z + pawn.ViewOffset.Z);
|
||||
var end = eye + Forward(pawn.EyeAngles) * TraceDistance;
|
||||
var result = Trace.TraceEndShape(eye, end, pawn, new TraceOptions { InteractsWith = Masks.ShotBrushOnly });
|
||||
if (!result.DidHit())
|
||||
{
|
||||
Reply(player, "Nothing under your crosshair within range.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// HitPoint is only filled in when the engine reports an exact hit; EndPos (where the ray
|
||||
// stopped) is always set and is the same spot for a line trace.
|
||||
return Point3.From(result.HasExactHitPoint ? result.HitPoint : result.EndPos);
|
||||
}
|
||||
|
||||
private void Redraw(Session session)
|
||||
{
|
||||
session.Beams.Clear();
|
||||
var color = ToColor(session.Color);
|
||||
foreach (var p in session.Points)
|
||||
session.Beams.Marker(p.ToVector(), color, Config.BeamWidth);
|
||||
if (session.Box is { } box)
|
||||
session.Beams.Box(box, color, Config.BeamWidth);
|
||||
}
|
||||
|
||||
private static string Size(ZoneShape s) =>
|
||||
$"{s.Maxs.X - s.Mins.X:F0} x {s.Maxs.Y - s.Mins.Y:F0} x {s.Maxs.Z - s.Mins.Z:F0}";
|
||||
|
||||
private static Color ToColor(Rgb c) => Color.FromArgb(255, c.R, c.G, c.B);
|
||||
|
||||
private static Vector Forward(QAngle angles)
|
||||
{
|
||||
var pitch = angles.X * MathF.PI / 180f;
|
||||
var yaw = angles.Y * MathF.PI / 180f;
|
||||
return new Vector(MathF.Cos(pitch) * MathF.Cos(yaw), MathF.Cos(pitch) * MathF.Sin(yaw), -MathF.Sin(pitch));
|
||||
}
|
||||
|
||||
private static void Reply(CCSPlayerController player, string message) =>
|
||||
player.PrintToChat($" {ChatColors.Red}[NoGoZones]{ChatColors.Default} {message}");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue