# NoGoZones Admins mark box-shaped zones in-game with their crosshair. Players can't enter a confirmed zone. Confirmed zones stay marked on the ground: a rectangle over the zone's footprint with an X through it, in the zone's color. Set `ShowZones` to `false` to hide them. Blocking works either way. Built for CounterStrikeSharp **1.0.375** (`net10.0`) on Metamod:Source 2.0.0.1469. No other dependencies. ## Install Build with `cd plugins/NoGoZones && ../../build.sh`. Then copy the contents of `compiled/NoGoZones/` to `game/csgo/addons/counterstrikesharp/plugins/NoGoZones/` on the server. Or install a release, which unpacks into `game/csgo/`: ``` tar -xzf NoGoZones-.tar.gz -C /srv/cs2/game/csgo ``` To publish one, commit, bump `ModuleVersion`, then run `FORGEJO_TOKEN=... ./release.sh v`. It rebuilds the plugin from the committed source in the SDK container, tags HEAD, and uploads `NoGoZones-.tar.gz` to the git.zio.sh release. The tarball contains no configs, so an upgrade never touches the server's settings or zones. Files the plugin writes, under `addons/counterstrikesharp/configs/plugins/NoGoZones/`: - `NoGoZones.json`: settings. It's created with defaults on first load. - `zones/.json`: that map's zones, **one file per map**. It's easier to edit by hand than one combined file, and a broken edit only affects one map. Characters that aren't valid in a file name, including `/` from workshop map names, are replaced with `_`. Zones for the current map are loaded on map start and enforced right away. ## Permission Every command needs **`@css/root`**. In this server's rank groups, that means `#rank/owner`. `css_zone_remove`, `css_zone_list` and `css_zone_reload` also work from the server console or RCON. ## Workflow ``` css_zone_start begin marking a zone (one session per admin, by SteamID) css_zone_color optional, preview color (default red, 255 0 0) css_zone_mark x4: marks the point under your crosshair and draws a marker there after the 4th: draws the box outline as a preview (nothing saved yet) css_zone_height [units] optional, after the 4th mark: set the box top and redraw the preview. With a number: that many units above the lowest point. With no argument: at your crosshair (aim at a tunnel roof's underside). css_zone_confirm save the box to this map's file and start blocking it css_zone_cancel discard the session and its preview instead ``` Managing saved zones: ``` css_zone_list zones on the current map css_zone_near the 3 zones nearest to you, with distance and on/off state (finds a name to edit) css_zone_remove delete a zone and stop blocking it css_zone_active <1|0> turn a zone on or off without deleting it (also on/off, true/false) css_zone_reload re-read this map's file (after editing it by hand) ``` If `css_zone_reload` hits invalid JSON, it logs the error and keeps the zones that were already active. Several admins can mark different zones at once. A session ends on confirm, on cancel, when its admin disconnects, or on map change. ## How the box is built The plugin fits a plane through the 4 points. That decides the kind of zone: **Floor zone.** The plane is closer to horizontal: you marked the zone's footprint on the ground. - **X/Y:** the min and max of the points, axis-aligned. Mark the points in any order. - **Z (height):** - The bottom is `BottomMargin` (8) below the lowest point. - The top is whichever is higher: `MinHeight` (128) above the lowest point, or `TopMargin` (16) above the highest point. That's taller than a crouch-jump. - **Outline:** a rectangle with an X, flat, just above the lowest point. **Wall zone.** The plane is closer to vertical: you marked a face, e.g. two points at the foot of a tunnel mouth and two higher up its sides. - **Shape:** a slab `WallThickness` (16) deep, turned to lie in that face. It doesn't need to line up with the map's axes. - **Width:** the spread of the points along the face. - **Height:** from `BottomMargin` below the lowest point up to the highest point. - **Outline:** a rectangle standing on the face itself, with no X. - **Fill:** the face is filled with dimmer horizontal lines, about every `FillSpacing` units, so it reads as a wall. - The lines scroll up at `FillScrollSpeed` units/s and wrap around. A negative value scrolls down, and 0 keeps them still. - With `FillPulse`, a brightness wave runs up them once every `PulsePeriod` seconds. - **Sign:** a no-entry sign (a red disc with a white bar) sits in the middle, up to `SignSize` units across. The fill lines open a gap around it as they pass. **Changing the top.** `css_zone_height` replaces the top for either kind. Use it to stop a floor zone under a tunnel roof, or to raise a wall zone above where you could aim. `NoGoZones.json` settings: `MinHeight`, `TopMargin`, `BottomMargin`, `WallThickness`, `DefaultColor` (`{"R":..,"G":..,"B":..}`), `BeamWidth`, `FillSpacing` (12; 0 turns the fill off), `FillWidth` (4), `FillBrightness` (0.8), `FillScrollSpeed` (12), `FillPulse` (true), `PulsePeriod` (1.5), `PulseMinBrightness` (0.6), `ShowSign`, `SignSize` (48), `ShowZones` (default `true`: draw the rectangle with an X over each saved zone), `IgnoreNoclip` (default `true`: noclipping players pass through). Each zone in the file stores: - `Points`: the 4 raw marks. - The computed box: `Mins`/`Maxs`, `Yaw` and `IsWall`. - `Color`. - `Active`: `false` means the zone is kept but doesn't block or draw. It's set by `css_zone_active`, and older files without it count as active. `Mins`/`Maxs` are in the zone's own frame, rotated by `Yaw` degrees about Z. For floor zones `Yaw` is 0, so they're plain world coordinates. The blocker uses only the box, so a hand edit to it takes effect after `css_zone_reload`. Editing `Points` doesn't recompute the box. ## Design decisions and API limitations ### Crosshair trace This uses `Trace.TraceEndShape` from `CounterStrikeSharp.API.Modules.Utils` (native in 1.0.375), with no Ray-Trace library. - The ray runs from the eye (`AbsOrigin + ViewOffset.Z`) 8192 units along the view direction. - It uses the mask `Masks.ShotBrushOnly` and ignores the admin's own pawn. So it hits world geometry, not players. - The marked point is `HitPoint` if `HasExactHitPoint` is set, otherwise `EndPos`. CSS's `TraceResult` only guarantees the former when the engine reports an exact hit. ### Preview drawing: `env_beam` entities The 1.0.375 source has **no debug-overlay or draw-line natives** at all. The only way to draw a line in the world is a real `env_beam` entity (`CBeam`): set `Render` (color), `Width`, the start via `Teleport` and `EndPos`, then `DispatchSpawn`. jRandomSkills already draws its trace beams this way on this server. - Each point marker is 3 beams: a small cross, plus a vertical line so it's visible from a distance. - The box preview is 12 beams, one per edge. - They're removed on confirm or cancel. - A confirmed floor zone is drawn as 6 beams: a rectangle plus its two diagonals, flat over the footprint just above the floor. - A wall zone stands on the marked face as a border, plus the fill lines and the sign. That's about 30–60 beams, depending on its height. - Each fill line is two beams, either side of the sign, so a line can open a gap as it scrolls past the sign without spawning or removing beams. - A 0.1 s timer moves the lines (`Teleport` for the start, `m_vecEndPos` for the end) and recolours them (`m_clrRender`). Every change is flagged with `SetStateChanged`, so clients receive it. - That's about 10 network updates a second per fill beam: a few thousand entity updates a second across a map full of walls. Set `FillScrollSpeed` to 0 and `FillPulse` to false for static walls with no updates at all. - The sign is beams too: a stack of overlapping horizontal rows forming the disc, with the middle rows split red/white/red for the bar. A real image would need a custom texture on every client, and clients only get that through a Workshop addon. - The round restart removes spawned entities, so the outlines are redrawn at every round start. They're also redrawn after confirm, remove and reload. **Limitation:** beams are ordinary networked entities, so **every player sees the preview and the zone outlines**, not only admins. Hiding them from other players would need a `CheckTransmit` filter. That isn't implemented. ### Blocking: per-tick movement block **Why not a single custom-size solid:** - `CCollisionProperty` exposes `Mins`, `Maxs`, `SolidType` and `SolidFlags` as writable schema fields, but CSS has **no native to rebuild an entity's physics shape** afterwards. - CS2 player movement collides against the VPhysics shape built from the model at spawn, so writing the bounds doesn't make a wall. - Brush entities get their shape from brush models compiled into the map, and those can't be created at runtime. **Tried and removed (0.7.0–0.10.0): scaled `dev_cube` walls.** These are the findings from live tests: - A per-axis scale isn't possible. The Hammer-style `scales` keyvalue is ignored, and the scene node only has a single `m_flScale`. - A uniform scale does scale the collision, and bullets pass through. So a zone could be filled with overlapping uniformly scaled cubes. The problem is that a cube's side is capped by the zone's thinnest dimension. That made thin walls either need hundreds of cubes, or be thickened until they stuck out past the marked face. It was dropped in favour of beams plus the per-tick block. The per-tick block works like this. On every tick (`Listeners.OnTick`), for each living player: 1. Work in the zone's own frame, so turned wall zones use the same math. Grow the zone box by the player's own collision hull (`Collision.Mins`/`Maxs`, so crouching counts). For a turned zone, it grows by the hull's width along each of the zone's axes. Then check the player's origin against the grown box, which keeps the whole body out, not just the feet. 2. Sweep from last tick's origin to this tick's. If that step entered the box, move the player back just outside the face they crossed, and zero their velocity into that face. Movement along the face is kept, so players slide along the walls. Because the whole step is checked, a fast player (a speed skill, for example) can't skip through a thin zone between two ticks. 3. If a player is already inside (a zone confirmed on top of them, or a spawn inside one), push them out through the nearest face. Tradeoffs compared with real collision: - **It's corrected after the fact, not prevented.** The engine moves the player first, and the plugin puts them back in the same tick. Normally this looks like a wall, but under latency the client can predict a few units into the zone and then snap back. Expect a slight rubber-band feel at the edge, especially at high speed or with high ping. - **Only players are blocked.** Bullets, grenades, the C4, chickens and physics props go straight through. Nothing is there for them to hit. - **The top of a zone isn't a real floor.** A player on top is held up each tick but isn't "on ground" to the engine. They can stand there, but can't jump off it normally, and may show the falling animation. The 128-unit default height makes the top unreachable by jumping. - **Cost:** it runs every tick, with a cost of players × zones box checks. That's negligible for realistic zone counts. ## Untested This compiles against CSS 1.0.375. It has **not been run on the live server** yet. In-game, check: - beam visibility and color - how solid the wall feels, including at the edges and with speed skills - whether players standing on top of a zone behave acceptably