From dad4a5a483c980d8ccbc71a6e2347083a8df6a99 Mon Sep 17 00:00:00 2001 From: r-melvin <40235254+r-melvin@users.noreply.github.com> Date: Thu, 1 Oct 2026 23:26:42 +0100 Subject: [PATCH] fix: keep a custom NPC's NetworkObject when it is created before the network starts InstantiateTemplateInstance removed the instance's NetworkObject whenever this peer was not the server, including before the network had started at all. A mod that creates its NPCs at scene load (The Big Pimpin's escorts, on the second load of a session) got NPCs without one. Their network behaviours then bound to the parent's NetworkObject (@Managers/@NPCs, already spawned): FishNet's spawn failed twice ("Failed to spawn pending NPC ... NullReferenceException") and left them inactive, and a mod that checks npc.NetworkObject.IsSpawned (Siesta) took them for spawned NPCs and hid them before their spawn. Only a client-only peer removes it now; before the network starts it is kept (or added). Co-Authored-By: Claude Opus 5.5 --- S1API.Tests/NPCs/NPCNetworkObjectTests.cs | 28 +++++++++++++++++++++++ S1API/Entities/NPC.cs | 20 ++++++++++------ 2 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 S1API.Tests/NPCs/NPCNetworkObjectTests.cs diff --git a/S1API.Tests/NPCs/NPCNetworkObjectTests.cs b/S1API.Tests/NPCs/NPCNetworkObjectTests.cs new file mode 100644 index 00000000..2139f687 --- /dev/null +++ b/S1API.Tests/NPCs/NPCNetworkObjectTests.cs @@ -0,0 +1,28 @@ +using S1API.Entities; + +namespace S1API.Tests.NPCs; + +public sealed class NPCNetworkObjectTests +{ + [Fact] + public void AnNpcCreatedBeforeTheNetworkStartsKeepsItsNetworkObject() + { + // The Big Pimpin creates its escorts at scene load on the second load of a session, before the server is + // up. Removing their NetworkObject then left them bound to @Managers/@NPCs and their spawn failed. + Assert.False(NPC.ShouldRemoveNetworkObject(hasNetworkManager: true, isClient: false, isServer: false)); + Assert.False(NPC.ShouldRemoveNetworkObject(hasNetworkManager: false, isClient: false, isServer: false)); + } + + [Fact] + public void TheServerAndHostKeepIt() + { + Assert.False(NPC.ShouldRemoveNetworkObject(hasNetworkManager: true, isClient: false, isServer: true)); + Assert.False(NPC.ShouldRemoveNetworkObject(hasNetworkManager: true, isClient: true, isServer: true)); + } + + [Fact] + public void AClientOnlyPeerRemovesItBecauseTheServerSpawnsTheNpcToIt() + { + Assert.True(NPC.ShouldRemoveNetworkObject(hasNetworkManager: true, isClient: true, isServer: false)); + } +} diff --git a/S1API/Entities/NPC.cs b/S1API/Entities/NPC.cs index bb6befdd..83a79d67 100644 --- a/S1API/Entities/NPC.cs +++ b/S1API/Entities/NPC.cs @@ -211,6 +211,14 @@ internal static readonly System.Collections.Generic.HashSet #region Template Prefab Helpers + /// + /// Should a new NPC instance lose its NetworkObject? Only on a client-only peer, which the server spawns + /// the NPC to. Before the network starts (a mod creating its NPCs at scene load) it must keep it, or its network + /// behaviours bind to the parent's NetworkObject and the later spawn fails. + /// + internal static bool ShouldRemoveNetworkObject(bool hasNetworkManager, bool isClient, bool isServer) => + hasNetworkManager && isClient && !isServer; + private static GameObject InstantiateTemplateInstance(System.Type npcType, NPC owner) { GameObject prefab = GetOrCreatePerNpcPrefab(npcType, owner); @@ -245,18 +253,16 @@ private static GameObject InstantiateTemplateInstance(System.Type npcType, NPC o try { var nm = InstanceFinder.NetworkManager; - bool isServer = nm != null && nm.IsServer; var existingNo = instance.GetComponent(); - if (isServer) - { - if (existingNo == null) - existingNo = instance.AddComponent(); - } - else + if (ShouldRemoveNetworkObject(nm != null, nm != null && nm.IsClient, nm != null && nm.IsServer)) { if (existingNo != null) UnityEngine.Object.Destroy(existingNo); } + else if (existingNo == null) + { + instance.AddComponent(); + } } catch { } if (S1NPCs.NPCManager.InstanceExists)