Jump to content

stationary NPCs are re-projected onto the navmesh at spawn time and end up under dynamic monument floors

Pending 3.4.1

tsuyoshi_ito
tsuyoshi_ito

Posted

Hello,

First of all, thank you for including our sleep-transition fix for stationary NPCs in 3.4.0. We confirmed that `UpdateStationary()`
and the guard in `UpdateSleep()` are still there in 3.4.1, and that path no longer warps our NPCs under the floor.

Unfortunately, after the September 2026 Rust update **the same symptom came back through a different code path**. The cause this
time is not the sleep system but the moment the NPC is created. Your previous fix is intact and still doing its job.

### The symptom

Static NPCs from Dynamic Monuments (by Adem) stand under the monument floor, on the terrain. Unlike the previous bug, this happens
**regardless of whether a player is nearby**: the NPC is already under the floor from the instant it spawns, without waiting for any
sleep transition.

### The cause (a change on the Rust side)

Rust's newer navigation agent, `Rust.Ai.Gen2.RustNavMeshAgent`, **synchronizes its own position to the nearest navmesh the moment
the component is enabled** (`OnEnable`). Here is the relevant code from the decompiled server `Assembly-CSharp.dll`:
 

public RustNavMeshAgent()
{
    ...
    _updatePosition = true;   // constructor default is true
    ...
}

private void OnEnable()
{
    ...
    if (AI.useUnityNavmesh)                      // defaults to true
    {
        _nextPositionNS = WorldToNavSpace(transform.position);
        if (SamplePositionPoly(_nextPositionNS, out var hitNS, 10f, out _))   // searches for navmesh within 10 m
            _nextPositionNS = hitNS.position;
    }
    else
    {
        ...
        TryBindNavmesh();                        // also samples with a 10 m radius
    }
    previousPositionNS = _nextPositionNS;
    TrySyncWorldPosWithNavPos();                 // rewrites transform.position while _updatePosition is true
}

When the sampled navmesh point is more than 1 m above or below the original position, SamplePositionPoly casts a physics ray (3.5 m) to find the ground under the entity and prefers navmesh within 1 m of that ground. Runtime-spawned monument floors carry no navmesh, so that correction never applies and the terrain navmesh below the floor (within 10 m) is used as-is.

This position sync is what NPCPlayer.ServerInit() turns off:

 

NavAgent.updateRotation = false;
NavAgent.updatePosition = false;

### Why this affects NpcSpawn

`CreateCustomNpc()` (around line 1126 in 3.4.1) activates the NPC **before** spawning it:

ScientistNPC scientistNpc = GameManager.server.CreateEntity(config.Prefab, position, Quaternion.identity, false); // created inactive
...
customScientist.enableSaving = false;
customScientist.gameObject.AwakeFromInstantiate();   // SetActive(true) -> RustNavMeshAgent.OnEnable() runs and moves the NPC under the floor
customScientist.Spawn();                              // -> ServerInit() sets updatePosition = false, but it is already too late

 

The position is rewritten during AwakeFromInstantiate(), and disabling the sync afterwards in ServerInit() does not restore it. DisableNavAgent() in AddStates() runs even later (Unity's Start), so it cannot help either.

This only hurts stationary NPCs. For mobile NPCs the re-projection at spawn time is desirable, since they need to walk on the navmesh anyway. Stationary NPCs, however, are meant to keep the exact position given to SpawnNpc with their NavAgent disabled, so for them the re-projection is both unnecessary and harmful.

Steps to reproduce

Create a preset with Speed = 0 (or States containing only IdleState + CombatStationaryState). The value of CanSleep does not matter.

Spawn any prefab with a walkable floor at runtime and place an NPC on that floor through the SpawnNpc API, with terrain less than 10 m below it.

The NPC immediately stands on the terrain under the floor. This happens even with a player standing right next to it — that is the difference from the previous sleep-related bug.

If the floor is more than 10 m above the terrain, the problem does not occur, because SamplePositionPoly searches within 10 m.

The fix we applied (verified on our server)

In CreateCustomNpc(), we set updatePosition to false for stationary NPCs before calling AwakeFromInstantiate():

customScientist.enableSaving = false;

customScientist.UpdateStationary();
if (customScientist.IsStationary)
{
    Rust.Ai.Gen2.RustNavMeshAgent navAgent = customScientist.GetComponent<Rust.Ai.Gen2.RustNavMeshAgent>();
    if (navAgent != null) navAgent.updatePosition = false;
}

customScientist.gameObject.AwakeFromInstantiate();
customScientist.Spawn();

UpdateStationary() is the existing public method you added in 3.4.0. It only reads Config, so it is safe to call at this point, and since AddStates() calls the same method there is no second copy of the stationary condition to keep in sync.

updatePosition is a value that NPCPlayer.ServerInit() sets to false anyway, so nothing changes for stationary NPCs afterwards — we only set it earlier.

Mobile NPCs are untouched and keep the navmesh re-projection at spawn time.

If you prefer a different approach, calling SetActive(true) after Spawn(), or disabling the NavAgent for stationary NPCs inside CreateCustomNpc() instead of waiting for AddStates(), would work as well. The key point is that the position sync has to be turned off for stationary NPCs before the RustNavMeshAgent is enabled.

After deploying this change on our server, stationary NPCs no longer end up under the floors of runtime-spawned monuments.

Patch and how to apply it

The attached NpcSpawn-stationary-spawn.patch is a unified diff against NpcSpawn.cs 3.4.1. Put the patch file in the same directory as NpcSpawn.cs and apply it with either of:

git apply NpcSpawn-stationary-spawn.patch (inside a git repository)

patch -p1 < NpcSpawn-stationary-spawn.patch

The change is small (a few lines before AwakeFromInstantiate()), so applying it by hand from the code block above works just as well.

In case the attachment does not come through, here is the full patch:

 

diff --git a/NpcSpawn.cs b/NpcSpawn.cs
--- a/NpcSpawn.cs
+++ b/NpcSpawn.cs
@@ -1140,6 +1140,19 @@ namespace Oxide.Plugins
             customScientist.Config = config;
             customScientist.Brain = customScientistBrain;
             customScientist.enableSaving = false;
+
+            // Stationary NPCs: Rust.Ai.Gen2.RustNavMeshAgent.OnEnable() re-projects the entity onto the nearest navmesh
+            // (10 m radius) and moves the transform while updatePosition is true (its constructor default).
+            // NPCPlayer.ServerInit() sets it to false, but that runs after activation, so a stationary NPC placed on a
+            // runtime-spawned structure (no navmesh on its floors) is warped to the terrain below before ServerInit().
+            // Disable the sync before activation so the NPC keeps the requested position (mobile NPCs keep vanilla behavior).
+            customScientist.UpdateStationary();
+            if (customScientist.IsStationary)
+            {
+                Rust.Ai.Gen2.RustNavMeshAgent navAgent = customScientist.GetComponent<Rust.Ai.Gen2.RustNavMeshAgent>();
+                if (navAgent != null) navAgent.updatePosition = false;
+            }
+
             customScientist.gameObject.AwakeFromInstantiate();
             customScientist.Spawn();

 

NpcSpawn-stationary-spawn.patch.txt

  • Love 1
aimacak

Posted

On 9/5/2026 at 5:18 AM, tsuyoshi_ito said:

Hello,

First of all, thank you for including our sleep-transition fix for stationary NPCs in 3.4.0. We confirmed that `UpdateStationary()`
and the guard in `UpdateSleep()` are still there in 3.4.1, and that path no longer warps our NPCs under the floor.

Unfortunately, after the September 2026 Rust update **the same symptom came back through a different code path**. The cause this
time is not the sleep system but the moment the NPC is created. Your previous fix is intact and still doing its job.

### The symptom

Static NPCs from Dynamic Monuments (by Adem) stand under the monument floor, on the terrain. Unlike the previous bug, this happens
**regardless of whether a player is nearby**: the NPC is already under the floor from the instant it spawns, without waiting for any
sleep transition.

### The cause (a change on the Rust side)

Rust's newer navigation agent, `Rust.Ai.Gen2.RustNavMeshAgent`, **synchronizes its own position to the nearest navmesh the moment
the component is enabled** (`OnEnable`). Here is the relevant code from the decompiled server `Assembly-CSharp.dll`:
 

public RustNavMeshAgent()
{
    ...
    _updatePosition = true;   // constructor default is true
    ...
}

private void OnEnable()
{
    ...
    if (AI.useUnityNavmesh)                      // defaults to true
    {
        _nextPositionNS = WorldToNavSpace(transform.position);
        if (SamplePositionPoly(_nextPositionNS, out var hitNS, 10f, out _))   // searches for navmesh within 10 m
            _nextPositionNS = hitNS.position;
    }
    else
    {
        ...
        TryBindNavmesh();                        // also samples with a 10 m radius
    }
    previousPositionNS = _nextPositionNS;
    TrySyncWorldPosWithNavPos();                 // rewrites transform.position while _updatePosition is true
}

When the sampled navmesh point is more than 1 m above or below the original position, SamplePositionPoly casts a physics ray (3.5 m) to find the ground under the entity and prefers navmesh within 1 m of that ground. Runtime-spawned monument floors carry no navmesh, so that correction never applies and the terrain navmesh below the floor (within 10 m) is used as-is.

This position sync is what NPCPlayer.ServerInit() turns off:

 

NavAgent.updateRotation = false;
NavAgent.updatePosition = false;

### Why this affects NpcSpawn

`CreateCustomNpc()` (around line 1126 in 3.4.1) activates the NPC **before** spawning it:

ScientistNPC scientistNpc = GameManager.server.CreateEntity(config.Prefab, position, Quaternion.identity, false); // created inactive
...
customScientist.enableSaving = false;
customScientist.gameObject.AwakeFromInstantiate();   // SetActive(true) -> RustNavMeshAgent.OnEnable() runs and moves the NPC under the floor
customScientist.Spawn();                              // -> ServerInit() sets updatePosition = false, but it is already too late

 

The position is rewritten during AwakeFromInstantiate(), and disabling the sync afterwards in ServerInit() does not restore it. DisableNavAgent() in AddStates() runs even later (Unity's Start), so it cannot help either.

This only hurts stationary NPCs. For mobile NPCs the re-projection at spawn time is desirable, since they need to walk on the navmesh anyway. Stationary NPCs, however, are meant to keep the exact position given to SpawnNpc with their NavAgent disabled, so for them the re-projection is both unnecessary and harmful.

Steps to reproduce

Create a preset with Speed = 0 (or States containing only IdleState + CombatStationaryState). The value of CanSleep does not matter.

Spawn any prefab with a walkable floor at runtime and place an NPC on that floor through the SpawnNpc API, with terrain less than 10 m below it.

The NPC immediately stands on the terrain under the floor. This happens even with a player standing right next to it — that is the difference from the previous sleep-related bug.

If the floor is more than 10 m above the terrain, the problem does not occur, because SamplePositionPoly searches within 10 m.

The fix we applied (verified on our server)

In CreateCustomNpc(), we set updatePosition to false for stationary NPCs before calling AwakeFromInstantiate():

customScientist.enableSaving = false;

customScientist.UpdateStationary();
if (customScientist.IsStationary)
{
    Rust.Ai.Gen2.RustNavMeshAgent navAgent = customScientist.GetComponent<Rust.Ai.Gen2.RustNavMeshAgent>();
    if (navAgent != null) navAgent.updatePosition = false;
}

customScientist.gameObject.AwakeFromInstantiate();
customScientist.Spawn();

UpdateStationary() is the existing public method you added in 3.4.0. It only reads Config, so it is safe to call at this point, and since AddStates() calls the same method there is no second copy of the stationary condition to keep in sync.

updatePosition is a value that NPCPlayer.ServerInit() sets to false anyway, so nothing changes for stationary NPCs afterwards — we only set it earlier.

Mobile NPCs are untouched and keep the navmesh re-projection at spawn time.

If you prefer a different approach, calling SetActive(true) after Spawn(), or disabling the NavAgent for stationary NPCs inside CreateCustomNpc() instead of waiting for AddStates(), would work as well. The key point is that the position sync has to be turned off for stationary NPCs before the RustNavMeshAgent is enabled.

After deploying this change on our server, stationary NPCs no longer end up under the floors of runtime-spawned monuments.

Patch and how to apply it

The attached NpcSpawn-stationary-spawn.patch is a unified diff against NpcSpawn.cs 3.4.1. Put the patch file in the same directory as NpcSpawn.cs and apply it with either of:

git apply NpcSpawn-stationary-spawn.patch (inside a git repository)

patch -p1 < NpcSpawn-stationary-spawn.patch

The change is small (a few lines before AwakeFromInstantiate()), so applying it by hand from the code block above works just as well.

In case the attachment does not come through, here is the full patch:

 

diff --git a/NpcSpawn.cs b/NpcSpawn.cs
--- a/NpcSpawn.cs
+++ b/NpcSpawn.cs
@@ -1140,6 +1140,19 @@ namespace Oxide.Plugins
             customScientist.Config = config;
             customScientist.Brain = customScientistBrain;
             customScientist.enableSaving = false;
+
+            // Stationary NPCs: Rust.Ai.Gen2.RustNavMeshAgent.OnEnable() re-projects the entity onto the nearest navmesh
+            // (10 m radius) and moves the transform while updatePosition is true (its constructor default).
+            // NPCPlayer.ServerInit() sets it to false, but that runs after activation, so a stationary NPC placed on a
+            // runtime-spawned structure (no navmesh on its floors) is warped to the terrain below before ServerInit().
+            // Disable the sync before activation so the NPC keeps the requested position (mobile NPCs keep vanilla behavior).
+            customScientist.UpdateStationary();
+            if (customScientist.IsStationary)
+            {
+                Rust.Ai.Gen2.RustNavMeshAgent navAgent = customScientist.GetComponent<Rust.Ai.Gen2.RustNavMeshAgent>();
+                if (navAgent != null) navAgent.updatePosition = false;
+            }
+
             customScientist.gameObject.AwakeFromInstantiate();
             customScientist.Spawn();

 

NpcSpawn-stationary-spawn.patch.txt 1.31 kB · 0 downloads

Hello, I apologize for the delayed response 😞
This is the second time we’ve been impressed by your approach to creating appeals, thank you ❤️
I’ve asked KpucTaJI to take a look 😉

About Us

Codefling is the largest marketplace for plugins, maps, tools, and more, making it easy for customers to discover new content and for creators to monetize their work.

Downloads
3m
Total downloads
Customers
12k
Customers served
Files Sold
170.7k
Total sales
Payments
3.7m
Processed total
×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.