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)