π§ Navigation meshesΒΆ
Every CS2 map ships a navigation mesh β the surface the gameβs bots walk on, tiled into convex polygonal areas that are wired together by connections (which area you can step into, and from which edge). Itβs a compact model of βwhere you can stand and how areas join up,β useful for tagging positions, measuring routes, and reasoning about map control.
Awpy parses these .nav files β published by
awpy-data alongside the collision
meshes and radars β and answers the two questions youβd actually ask of one:
which area is this point in, and whatβs the shortest path between two
areas.
NavMeshΒΆ
from awpy import NavMesh
nav = NavMesh("de_dust2")
nav.find_area((-1500.0, 900.0, 60.0)) # -> an area id, e.g. 812
nav.find_path(812, 42) # -> [812, ..., 42] (area ids along the way)
The constructor takes a map name or a .nav file:
NavMesh("de_dust2") # newest release cached under ~/.awpy
# (downloads the latest if none is cached)
NavMesh("de_dust2", version=2000873) # pin an awpy-data release
NavMesh("path/to/de_dust2.nav") # load a specific .nav file
A string counts as a file path when it ends in .nav or contains a path
separator; a pathlib.Path always does. Anything else is treated as a map name
and resolved through awpy.data.nav_path.
Points are (x, y, z) tuples (or lists) in Hammer units, Z-up β the same
frame as demo world positions, so the *_x / *_y / *_z columns from
demo.kills, demo.grenades, demo.snapshots(...), etc. pass straight in.
Areas are identified by a numeric area_id; find_area returns that id.
AreasΒΆ
nav.areas is a DataFrame with one row per area β the analysis-friendly view:
nav.areas
# shape: (2_248, 9)
# βββββββββββ¬βββββββββββββ¬ββββββββββββββββββββββββββ¬ββββββββββββ¬ββββ¬βββββββ¬ββββββββββββββββ
# β area_id β hull_index β dynamic_attribute_flags β n_corners β β¦ β size β n_connections β
# βββββββββββͺβββββββββββββͺββββββββββββββββββββββββββͺββββββββββββͺββββͺβββββββͺββββββββββββββββ‘
# β 1 β 0 β 0 β 4 β β¦ β 520β¦ β 1 β
# β β¦ β β¦ β β¦ β β¦ β β¦ β β¦ β β¦ β
Column |
Description |
|---|---|
|
Unique id of the area. |
|
Collision hull the area was generated for. |
|
Engine attribute bitset (bomb-target, defuse, β¦). |
|
Number of polygon corners. |
|
Geometric center of the polygon. |
|
2D area of the polygon (Hammer unitsΒ²). |
|
Distinct neighbouring areas. |
For the full geometry of a single area β its corners and neighbour ids β use
nav.area(area_id):
nav.area(1)
# {'area_id': 1, 'hull_index': 0, 'dynamic_attribute_flags': 0,
# 'corners': [(150.5, 10.5, 12.0), (185.0, 21.0, 10.0), ...],
# 'connections': [3], 'ladders_above': [], 'ladders_below': [],
# 'centroid': (162.5, 25.1, 11.0), 'size': 520.9}
QueriesΒΆ
area = nav.find_area((x, y, z)) # area containing a world point (or None)
nav.neighbors(area) # -> [ids reachable directly]
nav.find_path(start, end) # -> [ids] from start to end, inclusive
find_area handles overlapping floors: when several areas cover the same
(x, y) (a bridge over a tunnel, stacked levels), it returns the one whose
height is closest to the pointβs z.
find_path accepts either an area id or a point for each endpoint (a
point is resolved with find_area first), and returns the list of area ids from
start to end inclusive β or an empty list if either endpoint maps to no area
or no route connects them. Connections are directional, matching the mesh.
Edges can be weighted three ways:
nav.find_path(a, b, weight="distance") # default: 3D distance between centroids
nav.find_path(a, b, weight="hops") # fewest areas
nav.find_path(a, b, weight="size") # sum of area sizes; routes away from large areas
Member |
Type |
Description |
|---|---|---|
|
Load a mapβs nav (or a |
|
|
|
One row per area (see above). |
|
|
Full detail for one area, including |
|
|
Area containing an |
|
|
Distinct areas reachable directly. |
|
|
Shortest area path; endpoints are ids or points. |
|
|
Nav-mesh format version (CS2 ships 36). |
|
|
Whether bot-navigation data was generated. |
|
|
Number of areas. |
|
|
The file the nav was loaded from. |
Example: tag every kill with its areaΒΆ
import polars as pl
from awpy import Demo, NavMesh
demo = Demo("match.dem")
nav = NavMesh(demo.header["map_name"])
kills = demo.kills.with_columns(
pl.Series(
"victim_area",
[
nav.find_area((r["victim_x"], r["victim_y"], r["victim_z"]))
for r in demo.kills.iter_rows(named=True)
],
)
)
For where the assets come from and how to manage the cache (awpy.data, the
awpy get CLI), see ποΈ Visibility & map data.