Summary
Engine: 5.8.3 (CL 58210709), installed build. Platform: Linux x86-64 (Vulkan), editor. Area: NavigationSystem, UnrealEd (EditorBuildUtils).
In an editor world, DoInitialSetup adds ENavigationBuildLock::AsyncLoadLock at map load when bWaitForAsyncLoadingBeforeBuildingNavigationAutomatically is true (the default). The lock lifts only after FAssetCompilingManager reports 0 remaining assets and then 2 s and 16 core-ticker ticks pass.
While the lock is held, an explicit Build > Build Paths (or the BuildPaths console command, or FNavigationSystem::Build from a script) is refused. The editor still reports the build as done:
UNavigationSystemV1::Buildlogs one LogNavigation Warning and returns (it isvoid).FEditorBuildUtils::EditorBuildthen runs Map Check, logs “Build time 0:00” and returnstrue(“the build completed successfully”).- When the lock later lifts, it is removed with
ELockRemovalRebuildAction::NoRebuildand the dirty areas are reset. The refused build is never run. Navigation changes made while the lock was held are not rebuilt either: the bounds path skipsAddDirtyAreaswhile locked.
The setting’s own comment says it only affects automatic builds: “In editor worlds, wait for async loading to finish before allowing to build navigation automatically”. The explicit build is refused anyway.
What Type of Bug are you experiencing?
AI
Steps to Reproduce
With the editor Python script in Additional Notes (E3_repro.py; run one step per call, so the editor ticks between them):
setup: a new level from/Engine/Maps/Templates/Template_Default, with aNavMeshBoundsVolumeover its floor, saved.builda few seconds later (it builds), then save the level.repro, in one call: load the level and issueBuildPaths. A user who loads a level and presses Build Paths within about 2 s gets the same result.buildagain, a few seconds later.
Expected Result
Step 3 builds, or Build Paths reports that it could not (a return value, a Map Check or Message Log error, a notification) and/or builds once the lock lifts.
Observed Result
- Step 3 logs a refusal, then Map Check and the editor’s finish line for a completed build, all in the same frame. From the second template run:
[18.20.40:969][392]Cmd: BuildPaths
[18.20.40:977][392]LogNavigationDataBuild: Display: UNavigationSystemV1::Build started...
[18.20.40:977][392]LogNavigation: Warning: Build Navigation NOT building because navigation build is locked (flags: 0x20).
[18.20.40:977][392]Cmd: MAP CHECK DONTDISPLAYDIALOG
[18.20.40:977][392]MapCheck: Map check complete: 0 Error(s), 0 Warning(s), took 0.178ms to complete.
[18.20.40:977][392]LogEditorBuildUtils: Build time 0:00
The first run showed the same. A build issued 0.4 s earlier, before the load, had built normally (UNavigationSystemV1::Build total execution time: 0.01s).
- Step 4 builds normally (
total execution time: 0.01s). - Nothing in the log marks the step-3 request as pending, and the source removes the lock with
NoRebuild(see Additional Notes).
Affects Versions
5.8
Platform(s)
Linux
Upload an image
Additional Notes
Where in 5.8.3
NavigationSystem.cpp1060-1068 (DoInitialSetup): addsAsyncLoadLock. 5950-6003 (UpdateDelayedUnlockeRegistration/RegisterDelayedUnlock): the lock lifts only after 0 remaining assets, 2 s and 16 ticks; 5996-5997 reset the dirty areas and remove the lock withNoRebuild.NavigationSystem.cpp4455-4462 (UNavigationSystemV1::Build): refuses on any lock butNoUpdateInEditor, with a Warning only. The same message (“because navigation build is locked”) is also printed when!bHasWork, with no lock held.NavigationSystem.cpp4367-4371: navigation-bounds updates add no dirty areas while locked.EditorBuildUtils.cpp442-460 (BuildAIPaths), 954-993 (TriggerNavigationBuilder→FNavigationSystem::Build), 599-609 (EditorBuildreturnsbBuildCompleted, which does not reflect the navigation result).NavigationSystem.h350-353: the setting’s comment.
Impact
Any scripted pipeline that loads a level (or rebuilds materials, which keeps assets compiling) and then builds navigation has its navmesh silently not built or left stale. The only sign is one Warning among thousands of lines. In an editor script the lock can never lift inside the same Python call, because the unlock needs ticks.
This cost us one full world-generation run (2026-11-01 19:13). A material rebuild ~70 s after load kept assets compiling, Build Paths was refused, and the levels were saved with every road off the navmesh. Only a later validation step noticed. Since then the refusal has recurred in three runs (2026-11-02 00:14, 07:50, 07:53; the 07:50 one came 1.4 s after its level loaded), caught each time by the check below.
Log excerpt (2026-11-02, our map; ours is World Partition, but the path is the non-partitioned FNavigationSystem::Build):
[07.50.11:439][ 74]LogWorldPartition: Display: WorldPartition initialize started... <- level load
[07.50.12:822][ 80]Cmd: BuildPaths
[07.50.12:859][ 80]LogNavigationDataBuild: Display: UNavigationSystemV1::Build started...
[07.50.12:859][ 80]LogNavigation: Warning: Build Navigation NOT building because navigation build is locked (flags: 0x20).
[07.50.12:859][ 80]Cmd: MAP CHECK DONTDISPLAYDIALOG
[07.50.12:863][ 80]LogEditorBuildUtils: Build time 0:00
[07.51.13:042][249]Cmd: BuildPaths <- retry 60 s later: built
Our workaround
Build Paths is checked by its effect, not trusted by its return. Right after the call, our stage projects known walkable points (a road’s two ends) onto the navmesh and fails if either is off. The pipeline then retries that step as a separate editor job (up to 8 times, 60 s apart), so the editor ticks between attempts and the lock can lift. Waiting inside the same script does not work: it blocks the ticks that release the lock.
Untried alternative: set bWaitForAsyncLoadingBeforeBuildingNavigationAutomatically=False under [/Script/NavigationSystem.NavigationSystemV1], which also gives up the wait for automatic builds.
Suggested fixes (any one helps)
- Treat an explicit build like
NoUpdateInEditorand let it ignoreAsyncLoadLock. It could first callFAssetCompilingManager::Get().FinishAllCompilation(), sinceBuild()already blocks until the navmesh finishes. - Or queue it: remember a refused explicit build and run it when the lock lifts, instead of
NoRebuildplus a dirty-area reset. - At least report it: return a result from
Build(), haveEditorBuildreturn false or add a Map Check error, and make the!bHasWorkcase print its own message.
Repro script (E3_repro.py)
"""E3 repro (UE 5.8.3): an explicit Build Paths is refused while ENavigationBuildLock::AsyncLoadLock is held, and the
editor still reports a completed build.
Run in the editor's Python console (or Tools > Execute Python Script) with one argument, one call per step:
setup a level from the Basic template with a NavMeshBoundsVolume over its floor, saved to MAP.
build Build Paths. Run it a few seconds after setup (it builds), then save the level.
repro load MAP and issue Build Paths in the same call. The lock taken at load cannot lift inside this call (it waits
for 2 s and 16 ticks with no asset compiling), so the log shows "Navigation NOT building because navigation
build is locked (flags: 0x20)", then Map Check and "Build time 0:00", as for a completed build.
build again, a few seconds later: it builds.
"""
# The editor's embedded `unreal` module has no stubs outside the editor, so every member reads as unknown here.
# pyright: reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false
# pyright: reportUnknownParameterType=false
import sys
import unreal # pyright: ignore[reportMissingImports] # the editor's embedded module, absent outside it
MAP = "/Game/E3Repro/E3_NavLock"
TEMPLATE = "/Engine/Maps/Templates/Template_Default"
def world() -> unreal.World:
return unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem).get_editor_world()
def setup() -> None:
unreal.EditorLoadingAndSavingUtils.new_map_from_template(TEMPLATE, False)
actors = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
floor = [a for a in actors.get_all_level_actors() if a.get_actor_label() == "Floor"]
if not floor:
raise RuntimeError("no actor labelled Floor: start from the Basic template")
centre, half = floor[0].get_actor_bounds(False)
bounds = actors.spawn_actor_from_class(unreal.NavMeshBoundsVolume, centre)
# The default brush is 2 m: cover the floor and 2 m above it.
bounds.set_actor_scale3d(
unreal.Vector(half.x / 100.0 + 1.0, half.y / 100.0 + 1.0, half.z / 100.0 + 2.0)
)
unreal.EditorLoadingAndSavingUtils.save_map(world(), MAP)
print(
f"E3 setup: saved {MAP}. Run 'build' a few seconds later, then save the level."
)
def build() -> None:
unreal.SystemLibrary.execute_console_command(world(), "BuildPaths")
print("E3 build: Build Paths issued; the log says whether it built")
def repro() -> None:
unreal.EditorLoadingAndSavingUtils.load_map(MAP)
unreal.SystemLibrary.execute_console_command(world(), "BuildPaths")
print(
"E3 repro: loaded and Build Paths issued in one call; the log shows the refusal"
)
STEPS = {"setup": setup, "build": build, "repro": repro}
if __name__ == "__main__":
STEPS[sys.argv[1] if len(sys.argv) > 1 else "repro"]()
