From 08d0edb99a8382e7ec32375e0ac121baef19779b Mon Sep 17 00:00:00 2001 From: r-melvin <40235254+r-melvin@users.noreply.github.com> Date: Fri, 2 Oct 2026 00:09:29 +0100 Subject: [PATCH 1/2] fix: attach NPC data before ConfigurePrefab for dealers and suppliers too ConfigurePrefab only had a framework data object for plain NPCs. A supplier or dealer built from a donor with none threw from WithVoice ("Could not apply S1API voice 'tyler' because the custom NPC has no framework data", Drug Expansion's Disco Davey) and never registered. Any prefab whose NPC component has no data now gets one before ConfigurePrefab, matching the component's own type; a role's own data still replaces it afterwards and identity and voice are re-applied to it. Co-Authored-By: Claude Opus 5.5 --- S1API.Tests/NPCs/NPCPrefabDataTests.cs | 21 ++++++++++++++++++++ S1API/Entities/NPC.cs | 27 +++++++++++++++++++------- 2 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 S1API.Tests/NPCs/NPCPrefabDataTests.cs diff --git a/S1API.Tests/NPCs/NPCPrefabDataTests.cs b/S1API.Tests/NPCs/NPCPrefabDataTests.cs new file mode 100644 index 00000000..41cd0ed7 --- /dev/null +++ b/S1API.Tests/NPCs/NPCPrefabDataTests.cs @@ -0,0 +1,21 @@ +using S1API.Entities; +using S1API.Internal.Entities; + +namespace S1API.Tests.NPCs; + +public sealed class NPCPrefabDataTests +{ + [Fact] + public void ADonorThatIsAlreadyADealerOrSupplierGetsDataOfItsOwnKind() + { + // A dealer donor keeps its Dealer component, so plain data attached before ConfigurePrefab would stay. + Assert.Equal(NpcRootRole.Dealer, NPC.DataRoleForComponent(isDealer: true, isSupplier: false)); + Assert.Equal(NpcRootRole.Supplier, NPC.DataRoleForComponent(isDealer: false, isSupplier: true)); + } + + [Fact] + public void APlainDonorGetsPlainDataWhichTheRolesOwnDataReplacesLater() + { + Assert.Equal(NpcRootRole.Plain, NPC.DataRoleForComponent(isDealer: false, isSupplier: false)); + } +} diff --git a/S1API/Entities/NPC.cs b/S1API/Entities/NPC.cs index bb6befdd..9a13c855 100644 --- a/S1API/Entities/NPC.cs +++ b/S1API/Entities/NPC.cs @@ -358,6 +358,13 @@ private static bool IsGeneratedS1ApiPrefab(GameObject prefabRoot) ?? FindSpawnablePrefabWithComponent(spawnablePrefabs, count); } + /// + /// The kind of data an NPC component without any gets before ConfigurePrefab: its own. A donor that already + /// is a dealer keeps that component, so plain data on it would never be replaced. + /// + internal static NpcRootRole DataRoleForComponent(bool isDealer, bool isSupplier) => + isDealer ? NpcRootRole.Dealer : isSupplier ? NpcRootRole.Supplier : NpcRootRole.Plain; + private static S1NPCs.NPC? FindPlainNpcComponent(GameObject prefabRoot) { if (prefabRoot == null) @@ -1018,13 +1025,19 @@ private static GameObject GetOrCreatePerNpcPrefab(System.Type npcType, NPC? owne NormalizeBaseEmployeePrefab(prefabNO.gameObject, sourcePrefabName, rootRole); prefabNO.gameObject.name = prefabName; - // Some native plain NPC prefabs have no framework data object. Attach one - // before ConfigurePrefab applies identity and other saved defaults. - if (rootRole == NpcRootRole.Plain) - { - S1NPCs.NPC? plainNpc = FindPlainNpcComponent(prefabNO.gameObject); - if (plainNpc != null && NPCDataAccess.GetDataObject(plainNpc) == null) - NPCDataAccess.AssignNewData(plainNpc, rootRole, plainNpc); + // Some native prefabs have no framework data object: the plain NPC prefab, and the donor a dealer or + // supplier is built from. Attach one before ConfigurePrefab, whose builder calls write to it + // (WithVoice throws without one). It matches the component's own type, so a donor that already is a + // dealer or supplier keeps the right kind of data even if no new component replaces it. + S1NPCs.NPC? donorNpc = rootRole == NpcRootRole.Plain + ? FindPlainNpcComponent(prefabNO.gameObject) + : GetPreferredNpcComponent(prefabNO.gameObject); + if (donorNpc != null && NPCDataAccess.GetDataObject(donorNpc) == null) + { + NpcRootRole dataRole = DataRoleForComponent( + CrossType.Is(donorNpc, out S1Economy.Dealer _), + CrossType.Is(donorNpc, out S1Economy.Supplier _)); + NPCDataAccess.AssignNewData(donorNpc, dataRole, donorNpc); } // Ensure template prefab does not execute runtime logic or remain in NPC registry From 787e3776677b1980518bc7f5c42091af8ed37da2 Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 1 Oct 2026 21:20:25 -0700 Subject: [PATCH 2/2] chore: trim redundant NPC data comments --- S1API.Tests/NPCs/NPCPrefabDataTests.cs | 1 - S1API/Entities/NPC.cs | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/S1API.Tests/NPCs/NPCPrefabDataTests.cs b/S1API.Tests/NPCs/NPCPrefabDataTests.cs index 41cd0ed7..8e4d1b83 100644 --- a/S1API.Tests/NPCs/NPCPrefabDataTests.cs +++ b/S1API.Tests/NPCs/NPCPrefabDataTests.cs @@ -8,7 +8,6 @@ public sealed class NPCPrefabDataTests [Fact] public void ADonorThatIsAlreadyADealerOrSupplierGetsDataOfItsOwnKind() { - // A dealer donor keeps its Dealer component, so plain data attached before ConfigurePrefab would stay. Assert.Equal(NpcRootRole.Dealer, NPC.DataRoleForComponent(isDealer: true, isSupplier: false)); Assert.Equal(NpcRootRole.Supplier, NPC.DataRoleForComponent(isDealer: false, isSupplier: true)); } diff --git a/S1API/Entities/NPC.cs b/S1API/Entities/NPC.cs index 9a13c855..02fb1783 100644 --- a/S1API/Entities/NPC.cs +++ b/S1API/Entities/NPC.cs @@ -1025,10 +1025,7 @@ private static GameObject GetOrCreatePerNpcPrefab(System.Type npcType, NPC? owne NormalizeBaseEmployeePrefab(prefabNO.gameObject, sourcePrefabName, rootRole); prefabNO.gameObject.name = prefabName; - // Some native prefabs have no framework data object: the plain NPC prefab, and the donor a dealer or - // supplier is built from. Attach one before ConfigurePrefab, whose builder calls write to it - // (WithVoice throws without one). It matches the component's own type, so a donor that already is a - // dealer or supplier keeps the right kind of data even if no new component replaces it. + // ConfigurePrefab needs data matching the donor component, before role replacement. S1NPCs.NPC? donorNpc = rootRole == NpcRootRole.Plain ? FindPlainNpcComponent(prefabNO.gameObject) : GetPreferredNpcComponent(prefabNO.gameObject);