NoGoZones/README.md
Astra 006949d01c 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.
2026-09-25 14:18:26 +01:00

217 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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-<tag>.tar.gz -C /srv/cs2/game/csgo
```
To publish one, commit, bump `ModuleVersion`, then run `FORGEJO_TOKEN=... ./release.sh v<ModuleVersion>`.
It rebuilds the plugin from the committed source in the SDK container, tags HEAD, and uploads
`NoGoZones-<tag>.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/<map>.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 <name> begin marking a zone (one session per admin, by SteamID)
css_zone_color <r> <g> <b> 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 <name> delete a zone and stop blocking it
css_zone_active <name> <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