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.
217 lines
12 KiB
Markdown
217 lines
12 KiB
Markdown
# 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
|