| .gitignore | ||
| Beams.cs | ||
| Blocker.cs | ||
| NoGoZones.cs | ||
| NoGoZones.csproj | ||
| README.md | ||
| release.sh | ||
| Zone.cs | ||
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, orTopMargin(16) above the highest point. That's taller than a crouch-jump.
- The bottom is
- 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
BottomMarginbelow 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
FillSpacingunits, so it reads as a wall.- The lines scroll up at
FillScrollSpeedunits/s and wrap around. A negative value scrolls down, and 0 keeps them still. - With
FillPulse, a brightness wave runs up them once everyPulsePeriodseconds.
- The lines scroll up at
- Sign: a no-entry sign (a red disc with a white bar) sits in the middle, up to
SignSizeunits 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,YawandIsWall. Color.Active:falsemeans the zone is kept but doesn't block or draw. It's set bycss_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.ShotBrushOnlyand ignores the admin's own pawn. So it hits world geometry, not players. - The marked point is
HitPointifHasExactHitPointis set, otherwiseEndPos. CSS'sTraceResultonly 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 (
Teleportfor the start,m_vecEndPosfor the end) and recolours them (m_clrRender). Every change is flagged withSetStateChanged, 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
FillScrollSpeedto 0 andFillPulseto 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:
CCollisionPropertyexposesMins,Maxs,SolidTypeandSolidFlagsas 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
scaleskeyvalue is ignored, and the scene node only has a singlem_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:
- 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. - 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.
- 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