Skip to content
The navmesh at runtime

The navmesh at runtime

What a resource can and cannot do to the GTA V navmesh: the 17 navmesh natives, blocking objects, disabling areas, streaming regions, and why peds refuse to walk on a custom map.

Last updated

Peds walk on a navmesh. It is not the same thing as the path node network that traffic drives on, it is not collision, and it is not something a script can build. This page is about what your resource can actually do to it while the server is running, and about the question everybody hits eventually: why peds refuse to walk on a custom map.

The whole native surface is 17 natives, and every write is subtractive

An exhaustive search of the Cfx.re native list for NAVMESH or NAV_MESH returns exactly 17 natives. Here they are, grouped by what they do.

Masking what is already there

Native What it does
ADD_NAVMESH_BLOCKING_OBJECT Adds a box peds and vehicles route around. Returns a handle.
UPDATE_NAVMESH_BLOCKING_OBJECT Moves or resizes one you added, by that handle.
REMOVE_NAVMESH_BLOCKING_OBJECT Removes it.
DOES_NAVMESH_BLOCKING_OBJECT_EXIST Asks whether a handle is still live.
DISABLE_NAVMESH_IN_AREA Turns the navmesh off over a box outright.

Streaming, not content

Native What it does
ADD_NAVMESH_REQUIRED_REGION Asks the streamer to load the navmesh around a point.
REMOVE_NAVMESH_REQUIRED_REGIONS Drops every request you made.
ARE_ALL_NAVMESH_REGIONS_LOADED Are the requested regions in yet.
_IS_NAVMESH_REQUIRED_REGION_OWNED_BY_ANY_THREAD Unnamed native, takes no arguments.

Asking questions

Native What it does
IS_NAVMESH_LOADED_IN_AREA Is navmesh streamed in over this box.
GET_NUM_NAVMESHES_EXISTING_IN_AREA How many navmesh files cover this box, loaded or not.
GET_NAVMESH_ROUTE_RESULT The state of the route a ped is following.
GET_NAVMESH_ROUTE_DISTANCE_REMAINING How far is left, and whether the path is ready.

Issuing tasks

Native What it does
TASK_FOLLOW_NAV_MESH_TO_COORD Walk a ped to a point, on the navmesh.
TASK_FOLLOW_NAV_MESH_TO_COORD_ADVANCED The same with more arguments.
TASK_VEHICLE_GOTO_NAVMESH Drive a vehicle to a point using the navmesh.
SET_VEHICLE_ACTIVE_FOR_PED_NAVIGATION Whether peds treat a vehicle as something to path around.

Read that list again and notice what is not in it. Nothing creates a polygon. Nothing moves one. Nothing retags one. Not one of these natives takes a vertex, a triangle or a flag word. Every write among them takes a box and subtracts.

Note

Two natives often listed as navmesh natives are not. _ADD_TACTICAL_ANALYSIS_POINT and _CLEAR_TACTICAL_ANALYSIS_POINTS are in the MISC namespace and belong to CTacticalAnalysis, which is combat cover scoring. Neither has NAVMESH in its name and neither touches the navmesh.

Blocking objects: the tool you usually want

If peds are walking through your new building, or across a bridge you removed, a blocking object is the answer. It is a box, it takes effect immediately and it needs no file.

client.lua
local blocker = 0

AddEventHandler('onClientResourceStart', function(resource)
    if resource ~= GetCurrentResourceName() then return end
    -- x, y, z is the centre. width, length, height are the full extents.
    blocker = AddNavmeshBlockingObject(
        -820.0, -1075.0, 11.0,   -- centre
        24.0, 18.0, 6.0,         -- width, length, height
        0.0,                     -- heading, in degrees
        false,                   -- bPermanent
        7                        -- eBlockingObjectFlags
    )
end)

AddEventHandler('onResourceStop', function(resource)
    if resource ~= GetCurrentResourceName() then return end
    if blocker ~= 0 then
        RemoveNavmeshBlockingObject(blocker)
        blocker = 0
    end
end)

The last argument is a flag set, documented on the native itself:

Value Name Blocks
0 BLOCKING_OBJECT_DEFAULT The default.
1 BLOCKING_OBJECT_WANDERPATH Wander paths.
2 BLOCKING_OBJECT_SHORTESTPATH Regular shortest paths.
4 BLOCKING_OBJECT_FLEEPATH Flee paths.
7 BLOCKING_OBJECT_ALLPATHS All paths.

Warning

Only 32 blocking objects can exist at once, and nothing manages them for you. Keep every handle, remove them in onResourceStop, and do not add one per frame or per spawned prop. Once you are past 32 the next call quietly gives you nothing.

bPermanent is the eighth argument. False lets the game clean the object up on a streaming pass; true keeps it until you remove it.

Disabling a whole area

For anything larger than a handful of boxes, switch the area off instead of stacking blockers.

client.lua
local x1, y1, z1 = -900.0, -900.0, 0.0
local x2, y2, z2 = -750.0, -750.0, 80.0

DisableNavmeshInArea(x1, y1, z1, x2, y2, z2, true)
-- Ped paths are a separate switch. Flip both, or peds keep spawning on
-- the pavement you just made unwalkable.
SetPedPathsInArea(x1, y1, z1, x2, y2, z2, false, 0)

Put both back when you are done, and from onResourceStop as well, otherwise the area stays off for everyone who was already connected:

client.lua
DisableNavmeshInArea(x1, y1, z1, x2, y2, z2, false)
SetPedPathsBackToOriginal(x1, y1, z1, x2, y2, z2, 0)

The native documentation for DISABLE_NAVMESH_IN_AREA says it in one line: use this for a large area, use ADD_NAVMESH_BLOCKING_OBJECT for a small one.

Is there a navmesh here at all?

This is the check worth running before you debug anything else. The two natives answer different questions and both matter.

client.lua
RegisterCommand('navcheck', function()
    local p = GetEntityCoords(PlayerPedId())
    local x1, y1, z1 = p.x - 40.0, p.y - 40.0, p.z - 20.0
    local x2, y2, z2 = p.x + 40.0, p.y + 40.0, p.z + 20.0

    -- Is it streamed in right now?
    local loaded = IsNavmeshLoadedInArea(x1, y1, z1, x2, y2, z2)
    -- How many navmesh files cover this box at all, loaded or not?
    local existing = GetNumNavmeshesExistingInArea(x1, y1, z1, x2, y2, z2)

    print(('navmesh loaded: %s, files covering this box: %d'):format(tostring(loaded), existing))
end, false)

GET_NUM_NAVMESHES_EXISTING_IN_AREA returning 0 is the real answer for a custom map built out over the sea: the game ships no navmesh there, so there is nothing to stream and nothing to load. IS_NAVMESH_LOADED_IN_AREA returning false with a non zero count just means it has not streamed in yet, which you can fix:

client.lua
AddNavmeshRequiredRegion(x, y, 150.0)

while not AreAllNavmeshRegionsLoaded() do
    Wait(0)
end

-- ... do the thing that needed it ...

RemoveNavmeshRequiredRegions()

That streams a file the game already has. It does not make one.

Why peds will not walk on your custom map

Because there is no navmesh under it, and no native can make one.

TASK_FOLLOW_NAV_MESH_TO_COORD needs navmesh to path on. Its own documentation is explicit about what happens when it cannot find a route: “Sometimes a path may not be able to be found. This could happen because there simply isn’t any way to get there… In this case the ped will simply stand still.” There is no error, no event and no return value on the task itself. The ped stands there.

To find out rather than guess, read the route back:

client.lua
local ped = PlayerPedId()
TaskFollowNavMeshToCoord(ped, x, y, z, 1.0, 20000, 1.0, 0, 0.0)

Wait(500)
local state, remaining, ready = GetNavmeshRouteDistanceRemaining(ped)
print(('route state %s, ready %s, %s m left'):format(state, tostring(ready), remaining))
print('route result ' .. GetNavmeshRouteResult(ped))

There is one flag worth knowing in the task’s own flag set, ENAV_GO_FAR_AS_POSSIBLE_IF_TARGET_NAVMESH_NOT_LOADED = 4. Its documentation says it makes the ped get as close as possible on whatever navmesh is loaded, and then adds the line that matters: the navmesh must still be loaded at the path start. It does not rescue a ped standing on a platform with no navmesh under it.

So the honest options for a custom map are:

  • Build it over ground the game already has navmesh for, which is most of the map.
  • Ship a .ynv for it, with everything below in mind.
  • Move peds with something that is not the pathfinder: TASK_GO_STRAIGHT_TO_COORD, a sequence of waypoints you author yourself, or attaching them to something.

Streaming a .ynv, and the trap in it

A .ynv in a resource’s stream/ folder needs no data_file line, no files{} entry and no script. The client takes the extension, looks up the streaming module for it, takes the file name without the extension as a slot name, and swaps the slot’s handle to point at your file. That is the same generic path every streamed asset goes through.

A stock name is the clean case. Ship navmesh[123][102].ynv and it replaces the game’s file of that name. The original handle is pushed onto a stack first, so when the resource stops the game’s own file comes back, and restarting the resource works.

A new name is not the clean case. Two things happen:

  1. A new slot gets created in the navmesh store, and navmesh slots are never removed. FiveM’s own source skips them on cleanup with a comment saying that re-registration crashes. So the slot name leaks for the rest of the game session. Restarting the resource works, precisely because nothing tried to remove the slot.
  2. Nothing is going to ask for it. The game requests navmesh by region, through the navmesh[x][y] naming and the grid behind it. A slot called mymap_navmesh is registered and never requested.

Caution

Treat “stream a .ynv under a new name” as unproven rather than as a feature. The registration is real and is in the source. That the game ever loads it is not something the source shows, and the naming scheme argues against it.

One more thing about timing: the cleanup that would release a slot only runs at session shutdown, not at resource stop. So a server that restarts a navmesh resource repeatedly during a session is accumulating state either way.

The file itself

If you do need to look at a .ynv, the YNV Navmesh Viewer opens one over the real map and takes it apart: the polygons coloured by flag, the full flag set of any polygon with the real bit names, the sector tree, the portals and cover points, and a count of which neighbouring cells the file’s edges point into.

That last number is why there is a viewer and not an editor. An edge in a .ynv names the polygon on the other side by an area id and a polygon index, and where that area is a different cell, the index is a position in the neighbouring file’s own polygon list. Across the stock build that is 2.75% of edge parts, roughly 350 per file. Add or remove one polygon and everything after it renumbers, which silently repoints the edges of up to four files you do not have open.

For the road network that traffic drives on, which is a completely separate set of files, see the YND Traffic Path Editor. Path nodes can be edited one file at a time, and that tool writes them.