diff --git a/S1API.Tests/Entities/CustomNpcReadinessCollection.cs b/S1API.Tests/Entities/CustomNpcReadinessCollection.cs new file mode 100644 index 00000000..9d823238 --- /dev/null +++ b/S1API.Tests/Entities/CustomNpcReadinessCollection.cs @@ -0,0 +1,7 @@ +namespace S1API.Tests.Entities; + +[CollectionDefinition(Name, DisableParallelization = true)] +public sealed class CustomNpcReadinessCollection +{ + public const string Name = "Custom NPC readiness"; +} diff --git a/S1API.Tests/Entities/CustomNpcReadinessPolicyTests.cs b/S1API.Tests/Entities/CustomNpcReadinessPolicyTests.cs index 765848ff..37d0ea21 100644 --- a/S1API.Tests/Entities/CustomNpcReadinessPolicyTests.cs +++ b/S1API.Tests/Entities/CustomNpcReadinessPolicyTests.cs @@ -1,9 +1,11 @@ using S1API.Entities; using S1API.Internal.Entities; using S1API.Internal.Patches; +using S1API.Internal.Utils; namespace S1API.Tests.Entities; +[Collection(CustomNpcReadinessCollection.Name)] public sealed class CustomNpcReadinessPolicyTests { [Fact] @@ -14,6 +16,16 @@ public void ClientHydrationSignalsReadyOnlyAfterEveryCustomNpcTypeCompletes() try { + foreach (Type npcType in ReflectionUtils.GetDerivedClasses()) + { + if (npcType.Assembly != typeof(NPC).Assembly + && npcType != typeof(DealerNpc) + && npcType != typeof(CustomerNpc)) + { + NPC.FinalizedCustomNpcTypes.Add(npcType); + } + } + var dealer = TestObjectFactory.CreateUninitialized(); var customer = TestObjectFactory.CreateUninitialized(); diff --git a/S1API.Tests/Entities/NPCRoleDeclarationTests.cs b/S1API.Tests/Entities/NPCRoleDeclarationTests.cs new file mode 100644 index 00000000..05b1e700 --- /dev/null +++ b/S1API.Tests/Entities/NPCRoleDeclarationTests.cs @@ -0,0 +1,160 @@ +using System.Reflection; +using S1API.Entities; +using S1API.Internal.Entities; + +namespace S1API.Tests.Entities; + +public sealed class NPCRoleDeclarationTests +{ + [Fact] + public void IsCustomerIsVirtualReadOnlyBooleanDefaultingToFalse() + { + PropertyInfo? property = typeof(NPC).GetProperty(nameof(NPC.IsCustomer)); + MethodInfo? getter = property?.GetMethod; + var npc = (NPC)System.Runtime.CompilerServices.RuntimeHelpers + .GetUninitializedObject(typeof(RoleTestNpc)); + + Assert.NotNull(property); + Assert.Equal(typeof(bool), property!.PropertyType); + Assert.True(getter!.IsVirtual); + Assert.False(getter.IsFinal); + Assert.Null(property.SetMethod); + Assert.False(npc.IsCustomer); + } + + [Fact] + public void DeclaredPropertiesRejectsNullType() + { + ArgumentNullException exception = Assert.Throws( + () => NpcRoleDeclarationResolver.GetDeclaredProperties(null!)); + + Assert.Equal("npcType", exception.ParamName); + } + + [Fact] + public void DeclaredPropertiesRejectsNonNpcType() + { + ArgumentException exception = Assert.Throws( + () => NpcRoleDeclarationResolver.GetDeclaredProperties(typeof(string))); + + Assert.Equal("npcType", exception.ParamName); + Assert.Contains("does not derive from", exception.Message); + } + + [Theory] + [InlineData(false, false, false, false)] + [InlineData(false, true, false, false)] + [InlineData(true, false, false, false)] + [InlineData(true, true, false, false)] + [InlineData(true, false, true, false)] + [InlineData(true, true, true, false)] + [InlineData(true, false, false, true)] + [InlineData(true, true, false, true)] + public void ValidRoleCombinationsRemainComposable( + bool isPhysical, + bool isCustomer, + bool isDealer, + bool isSupplier) + { + var declaration = new NpcRoleDeclaration( + isPhysical, + isCustomer, + isDealer, + isSupplier); + + NpcRoleDeclaration validated = declaration.Validate(typeof(NPC)); + + Assert.Equal(isCustomer, validated.IsCustomer); + Assert.Equal(isDealer, validated.IsDealer); + Assert.Equal(isSupplier, validated.IsSupplier); + } + + [Fact] + public void DealerAndSupplierDeclarationFailsEarly() + { + var declaration = new NpcRoleDeclaration( + isPhysical: true, + isCustomer: false, + isDealer: true, + isSupplier: true); + + InvalidOperationException exception = Assert.Throws( + () => declaration.Validate(typeof(NPC))); + + Assert.Contains("cannot be both a dealer and a supplier", exception.Message); + } + + [Fact] + public void NonPhysicalSupplierDeclarationFailsEarly() + { + var declaration = new NpcRoleDeclaration( + isPhysical: false, + isCustomer: false, + isDealer: false, + isSupplier: true); + + InvalidOperationException exception = Assert.Throws( + () => declaration.Validate(typeof(NPC))); + + Assert.Contains("must override IsPhysical to return true", exception.Message); + } + + [Fact] + public void CompatibilityDeclarationsOnlyAddLegacyCapabilities() + { + var properties = new NpcRoleDeclaration( + isPhysical: true, + isCustomer: false, + isDealer: false, + isSupplier: false); + + NpcRoleDeclaration effective = properties + .WithCompatibilityRoles( + isCustomer: true, + isDealer: true, + isSupplier: false) + .Validate(typeof(NPC)); + + Assert.True(effective.IsPhysical); + Assert.True(effective.IsCustomer); + Assert.True(effective.IsDealer); + Assert.False(effective.IsSupplier); + Assert.Equal(NpcRootRole.Dealer, effective.RootRole); + } + + [Theory] + [InlineData(nameof(NPCPrefabBuilder.EnsureCustomer), nameof(NPC.IsCustomer))] + [InlineData(nameof(NPCPrefabBuilder.EnsureDealer), nameof(NPC.IsDealer))] + [InlineData(nameof(NPCPrefabBuilder.EnsureSupplier), nameof(NPC.IsSupplier))] + public void LegacyEnsureMethodsRemainFluentNonErrorObsoleteShims( + string methodName, + string replacementProperty) + { + MethodInfo? method = typeof(NPCPrefabBuilder).GetMethod( + methodName, + BindingFlags.Public | BindingFlags.Instance, + binder: null, + types: Type.EmptyTypes, + modifiers: null); + ObsoleteAttribute? obsolete = method?.GetCustomAttribute(); + + Assert.NotNull(method); + Assert.Equal(typeof(NPCPrefabBuilder), method!.ReturnType); + Assert.Empty(method.GetParameters()); + Assert.NotNull(obsolete); + Assert.False(obsolete!.IsError); + Assert.Contains(replacementProperty, obsolete.Message); + } + +#pragma warning disable CS0618 + private static NPCPrefabBuilder CompileLegacyFluentCalls(NPCPrefabBuilder builder) => + builder.EnsureCustomer().EnsureDealer().EnsureSupplier(); +#pragma warning restore CS0618 + + private sealed class RoleTestNpc : NPC + { + internal override void CreateInternal() + { + } + } +} diff --git a/S1API/Entities/NPC.cs b/S1API/Entities/NPC.cs index 239f4e8f..363c402b 100644 --- a/S1API/Entities/NPC.cs +++ b/S1API/Entities/NPC.cs @@ -955,7 +955,8 @@ private static GameObject GetOrCreatePerNpcPrefab(System.Type npcType, NPC? owne NetworkObject? chosen = null; int count = spawnablePrefabs.GetObjectCount(); - NpcRootRole rootRole = GetDeclaredRootRole(npcType); + NpcRoleDeclaration roleDeclaration = GetDeclaredRoles(npcType); + NpcRootRole rootRole = roleDeclaration.RootRole; chosen = ResolveNpcSpawnablePrefab(spawnablePrefabs, count, rootRole); if (chosen == null) @@ -1036,6 +1037,7 @@ private static GameObject GetOrCreatePerNpcPrefab(System.Type npcType, NPC? owne throw new InvalidOperationException("NPC prefab is missing its NetworkObject."); var prefabRoot = prefabNO.gameObject ?? throw new InvalidOperationException("NPC prefab is missing its GameObject."); var builder = new NPCPrefabBuilder(prefabRoot, npcType); + PrepareDeclaredRoleInfrastructure(builder, roleDeclaration); if (owner != null) { owner.ConfigurePrefab(builder); @@ -1045,29 +1047,37 @@ private static GameObject GetOrCreatePerNpcPrefab(System.Type npcType, NPC? owne InvokeConfigurePrefabWithoutInstance(npcType, builder); } - // ConfigurePrefab may declare a specialized root role even when the virtual property was not overridden. - rootRole = GetDeclaredRootRole(npcType); + // Legacy builder calls may declare roles that were not exposed by type-level properties. + roleDeclaration = GetDeclaredRoles(npcType); + rootRole = roleDeclaration.RootRole; + S1Economy.Dealer? dealerComponent = null; + S1Economy.Supplier? supplierComponent = null; switch (rootRole) { case NpcRootRole.Dealer: - { - var dealerComponent = EnsureDealerComponentOnPrefab(prefabNO.gameObject); - var dealerDefaults = BuildDealerDefaultsForType(npcType); - if (dealerComponent != null && dealerDefaults != null) - TryApplyDealerDefaults(dealerComponent, dealerDefaults); + dealerComponent = EnsureDealerComponentOnPrefab(prefabNO.gameObject); break; - } case NpcRootRole.Supplier: - { - var supplierComponent = EnsureSupplierComponentOnPrefab(prefabNO.gameObject); - var supplierDefaults = BuildSupplierDefaultsForType(npcType); - if (supplierComponent != null && supplierDefaults != null) - TryApplySupplierDefaults(supplierComponent, supplierDefaults); - SupplierRuntimeCoordinator.FinalizePrefabInfrastructure( - prefabNO.gameObject, - supplierDefaults?.PersistentId); + supplierComponent = EnsureSupplierComponentOnPrefab(prefabNO.gameObject); break; - } + } + + // Root replacement can invalidate references prepared against the donor NPC, so this + // pass is deliberately repeated after compatibility roles have been materialized. + PrepareDeclaredRoleInfrastructure(builder, roleDeclaration); + + var dealerDefaults = BuildDealerDefaultsForType(npcType); + if (dealerComponent != null && dealerDefaults != null) + TryApplyDealerDefaults(dealerComponent, dealerDefaults); + + var supplierDefaults = BuildSupplierDefaultsForType(npcType); + if (supplierComponent != null && supplierDefaults != null) + TryApplySupplierDefaults(supplierComponent, supplierDefaults); + if (supplierComponent != null) + { + SupplierRuntimeCoordinator.FinalizePrefabInfrastructure( + prefabNO.gameObject, + supplierDefaults?.PersistentId); } // Ensure schedule actions exist on the template so NetworkBehaviour indices are stable @@ -1082,34 +1092,6 @@ private static GameObject GetOrCreatePerNpcPrefab(System.Type npcType, NPC? owne RemoveEmployeeComponentsFromBaseEmployeeFallback(prefabNO.gameObject); } - // If we are pre-registering without an instance owner, ensure baseline Customer exists when applicable - if (owner == null) - { - try - { - // Only add Customer for types that opted-in via EnsureCustomer - if (IsCustomerType(npcType)) - { - var existingCustomer = prefabNO.gameObject.GetComponent(); - if (existingCustomer == null) - { - existingCustomer = prefabNO.gameObject.AddComponent(); - } - - // Apply defaults if the mod registered them - var defaults = GetCustomerDefaultsForType(npcType); - if (defaults != null && existingCustomer != null) - { - var data = BuildCustomerDefaultsForType(npcType); - if (data != null) - TrySetCustomerDataOnComponent(existingCustomer, data); - } - } - - } - catch { } - } - RepairBehaviourOwnership(prefabRoot, GetPreferredNpcComponent(prefabRoot)); // Register as spawnable so FishNet assigns stable behaviour indices and can network-spawn @@ -1157,46 +1139,39 @@ private static void FinalizeSupplierPrefabIfNeeded(System.Type npcType, GameObje BuildSupplierDefaultsForType(npcType)?.PersistentId); } - private static NpcRootRole GetDeclaredRootRole(System.Type npcType) + private static NpcRoleDeclaration GetDeclaredRoles(System.Type npcType) { - bool isDealer = IsDealerType(npcType); - bool isSupplier = IsSupplierType(npcType); - bool isPhysical = false; + NpcRoleDeclaration declaration = NpcRoleDeclarationResolver + .GetDeclaredProperties(npcType) + .WithCompatibilityRoles( + IsCustomerType(npcType), + IsDealerType(npcType), + IsSupplierType(npcType)) + .Validate(npcType); - try - { - NPC tempInstance = (NPC)FormatterServices.GetUninitializedObject(npcType); - isDealer |= tempInstance.IsDealer; - isSupplier |= tempInstance.IsSupplier; - isPhysical = tempInstance.IsPhysical; - } - catch - { - } + if (declaration.IsCustomer) + RegisterCustomerType(npcType); + if (declaration.IsSupplier) + RegisterSupplierType(npcType); + else if (declaration.IsDealer) + RegisterDealerType(npcType); - if (isDealer && isSupplier) - { - throw new InvalidOperationException( - $"Custom NPC type '{npcType.FullName}' cannot be both a dealer and a supplier root."); - } + return declaration; + } - if (isSupplier) - { - if (!isPhysical) - { - throw new InvalidOperationException( - $"Custom supplier type '{npcType.FullName}' must override IsPhysical to return true."); - } + private static NpcRootRole GetDeclaredRootRole(System.Type npcType) => + GetDeclaredRoles(npcType).RootRole; - RegisterSupplierType(npcType); - return NpcRootRole.Supplier; - } - if (isDealer) - { - RegisterDealerType(npcType); - return NpcRootRole.Dealer; - } - return NpcRootRole.Plain; + private static void PrepareDeclaredRoleInfrastructure( + NPCPrefabBuilder builder, + NpcRoleDeclaration declaration) + { + if (declaration.IsCustomer) + builder.EnsureCustomerInfrastructure(); + if (declaration.IsDealer) + builder.EnsureDealerInfrastructure(); + if (declaration.IsSupplier) + builder.EnsureSupplierInfrastructure(); } private static void InvokeConfigurePrefabWithoutInstance(System.Type npcType, NPCPrefabBuilder builder) @@ -2435,7 +2410,18 @@ internal void ApplyGeneratedIcon(Sprite icon) /// Non-physical NPCs (false): Invisible, primarily for messaging and phone contacts, cannot move or be directly interacted with. /// public virtual bool IsPhysical => false; - + + /// + /// Determines whether this NPC has native customer functionality. + /// Override as true for NPCs that should buy products from the player. + /// + /// + /// This declarative type-level value is inspected while S1API prepares the NPC prefab. + /// Overrides must be stable, side-effect-free, and must not depend on constructor or field initialization. + /// When true, S1API adds and configures the customer component before network registration. + /// + public virtual bool IsCustomer => false; + /// /// Determines if the NPC has dealer functionality. Override as true for NPCs that should be dealers. /// @@ -2443,6 +2429,7 @@ internal void ApplyGeneratedIcon(Sprite icon) /// Dealer NPCs (true): Can manage customers, handle contracts, accept cash payments, and track inventory for sales. /// When true, the NPC prefab will use the "Dealer" network prefab instead of "CivilianNPC". /// Non-dealer NPCs (false): Regular NPCs without dealer-specific functionality. + /// This declarative type-level value must be stable, side-effect-free, and independent of normal instance initialization. /// public virtual bool IsDealer => false; @@ -2452,6 +2439,8 @@ internal void ApplyGeneratedIcon(Sprite icon) /// /// Supplier NPCs can provide dead-drop orders, meetings, delivery unlocks, and debt tracking. /// A custom NPC cannot be both a dealer and a supplier. + /// Suppliers must also override to return true. + /// This declarative type-level value must be stable, side-effect-free, and independent of normal instance initialization. /// public virtual bool IsSupplier => false; diff --git a/S1API/Entities/NPCCustomer.cs b/S1API/Entities/NPCCustomer.cs index 44abb9ea..55f5578a 100644 --- a/S1API/Entities/NPCCustomer.cs +++ b/S1API/Entities/NPCCustomer.cs @@ -89,7 +89,7 @@ public void EnsureCustomer() { if (Component == null) { - Logger.Warning($"Customer component not present on NPC prefab for {NPC.ID}. Add it via NPC.ConfigurePrefab(builder.EnsureCustomer())."); + Logger.Warning($"Customer component not present on NPC prefab for {NPC.ID}. Override NPC.IsCustomer to return true."); return; } diff --git a/S1API/Entities/NPCDealer.cs b/S1API/Entities/NPCDealer.cs index 01b7d894..e0213935 100644 --- a/S1API/Entities/NPCDealer.cs +++ b/S1API/Entities/NPCDealer.cs @@ -104,14 +104,14 @@ internal static void ClearStaticDelegates() /// /// Note: Since Dealer inherits from NPC in the base game (not a component), this will only work /// if the wrapped NPC is already a Dealer instance. For custom NPCs created via S1API, - /// dealer functionality must be configured at prefab creation time using . - /// This method is called automatically when the NPC spawns if was used. + /// dealer functionality must be declared at prefab creation time by overriding . + /// This method is called automatically when a dealer NPC spawns. /// public void EnsureDealer() { if (Component == null) { - Logger.Warning($"Dealer component not present on NPC prefab for {NPC.ID}. Add it via NPC.ConfigurePrefab(builder.EnsureDealer())."); + Logger.Warning($"Dealer component not present on NPC prefab for {NPC.ID}. Override NPC.IsDealer to return true."); return; } diff --git a/S1API/Entities/NPCPrefabBuilder.cs b/S1API/Entities/NPCPrefabBuilder.cs index 800036f8..3a9764aa 100644 --- a/S1API/Entities/NPCPrefabBuilder.cs +++ b/S1API/Entities/NPCPrefabBuilder.cs @@ -87,13 +87,23 @@ private S1NPCs.NPCScheduleManager EnsureScheduleManager() } /// - /// Adds customer behavior component to the NPC. Required before configuring customer defaults. + /// Ensures customer infrastructure for compatibility with existing prefab-builder declarations. /// /// - /// Enables the NPC to act as a business customer that can buy products from the player. + /// Compatibility shim for existing mods. New NPC types should override . /// /// The builder instance for fluent chaining. - public NPCPrefabBuilder EnsureCustomer() + [Obsolete("Override NPC.IsCustomer to return true instead.", false)] + public NPCPrefabBuilder EnsureCustomer() => + DeclareCustomerCompatibility(); + + internal NPCPrefabBuilder DeclareCustomerCompatibility() + { + NPC.RegisterCustomerType(ownerType); + return EnsureCustomerInfrastructure(); + } + + internal NPCPrefabBuilder EnsureCustomerInfrastructure() { var customer = prefabRoot.GetComponent(); if (customer == null) @@ -101,8 +111,6 @@ public NPCPrefabBuilder EnsureCustomer() customer = prefabRoot.AddComponent(); customer.enabled = true; } - // Mark this NPC type as a Customer-bearing type so pre-registration adds Customer on template - NPC.RegisterCustomerType(ownerType); return this; } @@ -334,10 +342,10 @@ public NPCPrefabBuilder WithSchedule(params IScheduleActionSpec[] specs) } /// - /// Adds dealer behavior to the NPC. Required before configuring dealer defaults. + /// Ensures dealer infrastructure for compatibility with existing prefab-builder declarations. /// /// - /// Enables the NPC to act as a dealer that sells products to assigned customers. + /// Compatibility shim for existing mods. New NPC types should override . /// This marks the NPC type as dealer-capable; S1API will ensure the generated spawnable prefab /// has a Dealer-compatible NPC component before network registration when the selected base prefab /// does not already include one. @@ -345,11 +353,18 @@ public NPCPrefabBuilder WithSchedule(params IScheduleActionSpec[] specs) /// dealer functionality and ensure the messaging app displays the correct Dealer category badge. /// /// The builder instance for fluent chaining. - public NPCPrefabBuilder EnsureDealer() + [Obsolete("Override NPC.IsDealer to return true instead.", false)] + public NPCPrefabBuilder EnsureDealer() => + DeclareDealerCompatibility(); + + internal NPCPrefabBuilder DeclareDealerCompatibility() { - // Mark the type as dealer-capable; NPC prefab creation materializes the correct runtime component. NPC.RegisterDealerType(ownerType); - + return EnsureDealerInfrastructure(); + } + + internal NPCPrefabBuilder EnsureDealerInfrastructure() + { // Ensure required schedule components exist var mgr = EnsureScheduleManager(); @@ -477,33 +492,45 @@ private NPCPrefabBuilder WithVoiceInternal(NPCVoiceDefinition voice, float? pitc } /// - /// Configures this NPC type to use the native supplier root. + /// Ensures supplier infrastructure for compatibility with existing prefab-builder declarations. /// /// + /// Compatibility shim for existing mods. New NPC types should override . /// Supplier NPCs support dead-drop orders, supplier meetings, delivery unlocks, and debt tracking. /// S1API reserves a location-dialogue schedule action required by the native supplier lifecycle. /// A custom NPC cannot be both a dealer and a supplier. /// /// The builder instance for fluent chaining. - public NPCPrefabBuilder EnsureSupplier() + [Obsolete("Override NPC.IsSupplier to return true instead.", false)] + public NPCPrefabBuilder EnsureSupplier() => + DeclareSupplierCompatibility(); + + internal NPCPrefabBuilder DeclareSupplierCompatibility() { NPC.RegisterSupplierType(ownerType); + return EnsureSupplierInfrastructure(); + } + + internal NPCPrefabBuilder EnsureSupplierInfrastructure() + { SupplierRuntimeCoordinator.EnsurePrefabInfrastructure(prefabRoot); return this; } /// - /// Configures customer behavior defaults using the . Requires to be called first. + /// Configures customer behavior defaults using the . /// /// /// Configure spending behavior, order frequency, customer standards, product preferences, and relationship requirements. + /// Override to declare customer capability. This method retains the + /// legacy implicit declaration behavior for source and behavioral compatibility. /// This configuration is essential for proper save/load behavior and must be done in . /// /// Action to configure customer defaults using the builder. /// The builder instance for fluent chaining. public NPCPrefabBuilder WithCustomerDefaults(Action configure) { - EnsureCustomer(); + DeclareCustomerCompatibility(); var customer = prefabRoot.GetComponent(); if (customer != null) { @@ -584,17 +611,19 @@ public NPCPrefabBuilder WithSpawnPosition(Vector3 position) } /// - /// Configures dealer behavior defaults using the . Requires to be called first. + /// Configures dealer behavior defaults using the . /// /// /// Configure dealer settings such as signing fee, commission cut, dealer type, quality restrictions, and deal tracking. + /// Override to declare dealer capability. This method retains the + /// legacy implicit declaration behavior for source and behavioral compatibility. /// This configuration is essential for proper save/load behavior and must be done in . /// /// Action to configure dealer defaults using the builder. /// The builder instance for fluent chaining. public NPCPrefabBuilder WithDealerDefaults(Action configure) { - EnsureDealer(); + DeclareDealerCompatibility(); // Register dealer defaults for type-level application NPC.RegisterDealerDefaultsForType(ownerType, configure); @@ -635,6 +664,10 @@ public NPCPrefabBuilder WithRegion(Region region) /// /// Configures native supplier data for this NPC type. /// + /// + /// Override to declare supplier capability. This method retains the + /// legacy implicit declaration behavior for source and behavioral compatibility. + /// /// Action that defines order limits, delivery items, and supplier messages. /// The builder instance for fluent chaining. /// Thrown when is null. @@ -643,7 +676,7 @@ public NPCPrefabBuilder WithSupplierDefaults(Action configu if (configure == null) throw new ArgumentNullException(nameof(configure)); - EnsureSupplier(); + DeclareSupplierCompatibility(); NPC.RegisterSupplierDefaultsForType(ownerType, configure); return this; } diff --git a/S1API/Entities/NPCSchedule.cs b/S1API/Entities/NPCSchedule.cs index 7d717b46..df98109f 100644 --- a/S1API/Entities/NPCSchedule.cs +++ b/S1API/Entities/NPCSchedule.cs @@ -173,10 +173,10 @@ internal void AddActionFromSpec(IScheduleActionSpec spec) /// /// /// Schedule I 0.4.6 removed NPCSignal_WaitForDelivery. Customer deal attendance is - /// configured automatically by . This method + /// configured automatically when is true. This method /// now performs no runtime work and logs one compatibility warning per process. /// - [Obsolete("NPCSignal_WaitForDelivery was removed in game version 0.4.6. Customer deal attendance is configured automatically by EnsureCustomer().")] + [Obsolete("NPCSignal_WaitForDelivery was removed in game version 0.4.6. Override NPC.IsCustomer; customer deal attendance is configured automatically.")] public void EnsureDealSignal() { if (_loggedRemovedDealSignal) diff --git a/S1API/Entities/NPCSupplier.cs b/S1API/Entities/NPCSupplier.cs index 1edebe2e..d2fef09c 100644 --- a/S1API/Entities/NPCSupplier.cs +++ b/S1API/Entities/NPCSupplier.cs @@ -139,14 +139,14 @@ public IReadOnlyList ActiveDeliveries /// Ensures supplier-specific messaging state is initialized for the wrapped NPC. /// /// - /// The native supplier root must be declared during with - /// . This method does not replace an already spawned root component. + /// The native supplier root must be declared by overriding . + /// This method does not replace an already spawned root component. /// internal void EnsureSupplier() { if (Component == null) { - Logger.Warning($"Supplier root not present for NPC '{npc.ID}'. Configure it with NPCPrefabBuilder.EnsureSupplier()."); + Logger.Warning($"Supplier root not present for NPC '{npc.ID}'. Override NPC.IsSupplier to return true."); return; } diff --git a/S1API/Entities/Schedule/ActionSpecs/HandleDealSpec.cs b/S1API/Entities/Schedule/ActionSpecs/HandleDealSpec.cs index ad90ea07..17a83fef 100644 --- a/S1API/Entities/Schedule/ActionSpecs/HandleDealSpec.cs +++ b/S1API/Entities/Schedule/ActionSpecs/HandleDealSpec.cs @@ -7,8 +7,8 @@ namespace S1API.Entities.Schedule /// /// /// As of v0.4.2f4, deal handling is now automatic through the DealerAttendDealBehaviour system. - /// This spec is kept for backwards compatibility but is a no-op. Dealer NPCs set up with - /// EnsureDealer() will automatically handle deals when contracts are assigned. + /// This spec is kept for backwards compatibility but is a no-op. NPCs with + /// enabled automatically handle deals when contracts are assigned. /// [Obsolete("HandleDealSpec is no longer needed as of game version 0.4.2f4. Deal handling is now automatic through DealerAttendDealBehaviour.")] public sealed class HandleDealSpec : IScheduleActionSpec @@ -26,7 +26,7 @@ public sealed class HandleDealSpec : IScheduleActionSpec void IScheduleActionSpec.ApplyTo(NPCSchedule schedule) { // No-op: Deal handling is now automatic through DealerAttendDealBehaviour. - // Dealers set up with EnsureDealer() will automatically handle deals when contracts are assigned. + // Declared dealers automatically handle deals when contracts are assigned. // This method intentionally does nothing to maintain backwards compatibility. } } diff --git a/S1API/Entities/Schedule/NPCScheduleBuilder.cs b/S1API/Entities/Schedule/NPCScheduleBuilder.cs index ac368d34..096cd6a2 100644 --- a/S1API/Entities/Schedule/NPCScheduleBuilder.cs +++ b/S1API/Entities/Schedule/NPCScheduleBuilder.cs @@ -102,7 +102,7 @@ public PrefabScheduleBuilder SitAtSeatSet(string? seatSetName, int startTime, in /// Schedule I 0.4.6 removed the deal signal. The retained specification configures the /// current customer deal-attendance behaviour during prefab creation and otherwise no-ops. /// - [System.Obsolete("NPCSignal_WaitForDelivery was removed in game version 0.4.6. Use EnsureCustomer(); deal attendance is configured automatically.")] + [System.Obsolete("NPCSignal_WaitForDelivery was removed in game version 0.4.6. Override NPC.IsCustomer; deal attendance is configured automatically.")] public PrefabScheduleBuilder EnsureDealSignal() { _specs.Add(new EnsureDealSignalSpec()); @@ -374,8 +374,8 @@ public LocationBasedActionSpecBuilder LocationBased(Vector3 destination, int sta /// This builder instance for method chaining. /// /// As of v0.4.2f4, deal handling is now automatic through the DealerAttendDealBehaviour system. - /// This method is kept for backwards compatibility but is a no-op. Dealer NPCs set up with - /// EnsureDealer() will automatically handle deals when contracts are assigned. + /// This method is kept for backwards compatibility but is a no-op. Dealer NPCs + /// automatically handle deals when contracts are assigned. /// [System.Obsolete("HandleDeal is no longer needed as of game version 0.4.2f4. Deal handling is now automatic through DealerAttendDealBehaviour.")] public PrefabScheduleBuilder HandleDeal(int startTime, string? name = null) diff --git a/S1API/Internal/Entities/NpcRoleDeclaration.cs b/S1API/Internal/Entities/NpcRoleDeclaration.cs new file mode 100644 index 00000000..ee98902b --- /dev/null +++ b/S1API/Internal/Entities/NpcRoleDeclaration.cs @@ -0,0 +1,117 @@ +using S1API.Entities; +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace S1API.Internal.Entities +{ + internal readonly struct NpcRoleDeclaration + { + internal bool IsPhysical { get; } + internal bool IsCustomer { get; } + internal bool IsDealer { get; } + internal bool IsSupplier { get; } + + internal NpcRootRole RootRole => + IsSupplier + ? NpcRootRole.Supplier + : IsDealer + ? NpcRootRole.Dealer + : NpcRootRole.Plain; + + internal NpcRoleDeclaration( + bool isPhysical, + bool isCustomer, + bool isDealer, + bool isSupplier) + { + IsPhysical = isPhysical; + IsCustomer = isCustomer; + IsDealer = isDealer; + IsSupplier = isSupplier; + } + + internal NpcRoleDeclaration WithCompatibilityRoles( + bool isCustomer, + bool isDealer, + bool isSupplier) => + new NpcRoleDeclaration( + IsPhysical, + IsCustomer || isCustomer, + IsDealer || isDealer, + IsSupplier || isSupplier); + + internal NpcRoleDeclaration Validate(Type npcType) + { + string typeName = npcType.FullName ?? npcType.Name; + if (IsDealer && IsSupplier) + { + throw new InvalidOperationException( + $"Custom NPC type '{typeName}' cannot be both a dealer and a supplier root."); + } + + if (IsSupplier && !IsPhysical) + { + throw new InvalidOperationException( + $"Custom supplier type '{typeName}' must override IsPhysical to return true."); + } + + return this; + } + } + + internal static class NpcRoleDeclarationResolver + { + private static readonly Dictionary TypeDeclarations = + new Dictionary(); + private static readonly object DeclarationLock = new object(); + + internal static NpcRoleDeclaration GetDeclaredProperties(Type npcType) + { + if (npcType == null) + throw new ArgumentNullException(nameof(npcType)); + if (!typeof(NPC).IsAssignableFrom(npcType)) + { + throw new ArgumentException( + $"Type '{npcType.FullName}' does not derive from {typeof(NPC).FullName}.", + nameof(npcType)); + } + + lock (DeclarationLock) + { + if (TypeDeclarations.TryGetValue(npcType, out var declaration)) + return declaration; + + var npc = (NPC)FormatterServices.GetUninitializedObject(npcType); + declaration = new NpcRoleDeclaration( + ReadProperty(npc, npcType, nameof(NPC.IsPhysical), value => value.IsPhysical), + ReadProperty(npc, npcType, nameof(NPC.IsCustomer), value => value.IsCustomer), + ReadProperty(npc, npcType, nameof(NPC.IsDealer), value => value.IsDealer), + ReadProperty(npc, npcType, nameof(NPC.IsSupplier), value => value.IsSupplier)); + TypeDeclarations[npcType] = declaration; + return declaration; + } + } + + private static bool ReadProperty( + NPC npc, + Type npcType, + string propertyName, + Func read) + { + try + { + return read(npc); + } + catch (Exception ex) + { + string typeName = npcType.FullName ?? npcType.Name; + throw new InvalidOperationException( + $"Custom NPC type '{typeName}' could not evaluate declarative property " + + $"'{propertyName}' from an uninitialized instance. NPC role properties must " + + "be stable, side-effect-free values that do not depend on constructor or field initialization.", + ex); + } + } + } +} diff --git a/S1API/docs/basic-npc-creation.md b/S1API/docs/basic-npc-creation.md index 57694c0a..dfc12e51 100644 --- a/S1API/docs/basic-npc-creation.md +++ b/S1API/docs/basic-npc-creation.md @@ -115,8 +115,8 @@ Once the NPC exists in-world, add dialogue. Keep the logic small here and lean o ## Step 5 (optional): Make them a customer or dealer -- Customer NPCs: `builder.EnsureCustomer().WithCustomerDefaults(...)` (see `S1API/docs/customer-behavior.md`) -- Dealer NPCs: `public override bool IsDealer => true;` + `builder.EnsureDealer().WithDealerDefaults(...)` (see `S1API/docs/dealer-system.md`) +- Customer NPCs: `public override bool IsCustomer => true;` + `builder.WithCustomerDefaults(...)` (see `S1API/docs/customer-behavior.md`) +- Dealer NPCs: `public override bool IsDealer => true;` + `builder.WithDealerDefaults(...)` (see `S1API/docs/dealer-system.md`) ## Example NPCs diff --git a/S1API/docs/custom-npcs.md b/S1API/docs/custom-npcs.md index f8022c34..7ac9800f 100644 --- a/S1API/docs/custom-npcs.md +++ b/S1API/docs/custom-npcs.md @@ -55,7 +55,8 @@ Here's a minimal example to get you started: ```csharp public sealed class MyFirstNPC : NPC { - protected override bool IsPhysical => true; + public override bool IsPhysical => true; + public override bool IsCustomer => true; protected override void ConfigurePrefab(NPCPrefabBuilder builder) { @@ -64,7 +65,6 @@ public sealed class MyFirstNPC : NPC firstName: "John", lastName: "Doe") .WithSpawnPosition(new Vector3(0, 0, 0)) - .EnsureCustomer() .WithCustomerDefaults(cd => { cd.WithSpending(100f, 500f) .WithOrdersPerWeek(1, 3); diff --git a/S1API/docs/customer-behavior.md b/S1API/docs/customer-behavior.md index 147f7454..772c9e8b 100644 --- a/S1API/docs/customer-behavior.md +++ b/S1API/docs/customer-behavior.md @@ -17,12 +17,13 @@ The customer system allows NPCs to act as business customers, buying products fr Customer NPCs can buy products from the player, follow spending patterns, and participate in the game's economy. The customer system is configured in `ConfigurePrefab` and managed at runtime. ```csharp +public override bool IsCustomer => true; + protected override void ConfigurePrefab(NPCPrefabBuilder builder) { - builder.EnsureCustomer() - .WithCustomerDefaults(cd => { - // Spending behavior - cd.WithSpending(minWeekly: 150f, maxWeekly: 600f) + builder.WithCustomerDefaults(cd => { + // Spending behavior + cd.WithSpending(minWeekly: 150f, maxWeekly: 600f) .WithOrdersPerWeek(1, 4) .WithPreferredOrderDay(Day.Friday) .WithOrderTime(1100); // 11:00 AM @@ -47,7 +48,7 @@ protected override void ConfigurePrefab(NPCPrefabBuilder builder) // Property preferences cd.WithPreferredProperties(Property.Munchies, Property.Energizing); - }); + }); } ``` @@ -55,10 +56,10 @@ protected override void ConfigurePrefab(NPCPrefabBuilder builder) ### Enabling Customer Behavior -First, ensure the customer component is added: +Declare customer capability at the NPC type level. S1API adds the native component before `ConfigurePrefab` and network registration: ```csharp -builder.EnsureCustomer(); +public override bool IsCustomer => true; ``` ### Customer Defaults @@ -266,11 +267,12 @@ var contract = Customer.CurrentContract; ### Basic Customer ```csharp +public override bool IsCustomer => true; + protected override void ConfigurePrefab(NPCPrefabBuilder builder) { - builder.EnsureCustomer() - .WithCustomerDefaults(cd => { - cd.WithSpending(100f, 300f) + builder.WithCustomerDefaults(cd => { + cd.WithSpending(100f, 300f) .WithOrdersPerWeek(1, 2) .WithPreferredOrderDay(Day.Friday) .WithOrderTime(1400) @@ -280,18 +282,19 @@ protected override void ConfigurePrefab(NPCPrefabBuilder builder) .WithAffinities(new[] { (DrugType.Marijuana, 0.5f) }); - }); + }); } ``` ### High-Value Customer ```csharp +public override bool IsCustomer => true; + protected override void ConfigurePrefab(NPCPrefabBuilder builder) { - builder.EnsureCustomer() - .WithCustomerDefaults(cd => { - cd.WithSpending(500f, 1000f) + builder.WithCustomerDefaults(cd => { + cd.WithSpending(500f, 1000f) .WithOrdersPerWeek(3, 5) .WithPreferredOrderDay(Day.Saturday) .WithOrderTime(1100) @@ -306,18 +309,19 @@ protected override void ConfigurePrefab(NPCPrefabBuilder builder) (DrugType.Marijuana, 0.6f) }) .WithPreferredProperties(Property.Energizing, Property.BrightEyed); - }); + }); } ``` ### Risky Customer ```csharp +public override bool IsCustomer => true; + protected override void ConfigurePrefab(NPCPrefabBuilder builder) { - builder.EnsureCustomer() - .WithCustomerDefaults(cd => { - cd.WithSpending(200f, 600f) + builder.WithCustomerDefaults(cd => { + cd.WithSpending(200f, 600f) .WithOrdersPerWeek(2, 4) .WithPreferredOrderDay(Day.Sunday) .WithOrderTime(2000) @@ -330,7 +334,7 @@ protected override void ConfigurePrefab(NPCPrefabBuilder builder) (DrugType.Heroin, 0.7f), (DrugType.Cocaine, 0.5f) }); - }); + }); } ``` @@ -376,7 +380,7 @@ protected override void OnCreated() ### Don'ts - **Don't modify customer data at runtime** (except through proper APIs) -- **Don't forget to call `EnsureCustomer()`** before `WithCustomerDefaults()` +- **Don't make `IsCustomer` depend on constructor state**; S1API reads role properties from an uninitialized instance - **Don't use extreme values** for spending, addiction, or police chance - **Don't create customers with impossible requirements** (e.g., high standards with low relationship) @@ -385,14 +389,15 @@ protected override void OnCreated() Wrap customer configuration in try-catch blocks: ```csharp +public override bool IsCustomer => true; + protected override void ConfigurePrefab(NPCPrefabBuilder builder) { try { - builder.EnsureCustomer() - .WithCustomerDefaults(cd => { - // Customer configuration - }); + builder.WithCustomerDefaults(cd => { + // Customer configuration + }); } catch (Exception ex) { diff --git a/S1API/docs/dealer-system.md b/S1API/docs/dealer-system.md index 072a462f..66f6c6d3 100644 --- a/S1API/docs/dealer-system.md +++ b/S1API/docs/dealer-system.md @@ -14,7 +14,7 @@ Dealer NPCs are special NPCs that can: ## Creating a Dealer NPC -To create a dealer NPC, set `IsDealer = true` and configure dealer defaults using the `EnsureDealer()` builder method: +To create a dealer NPC, override `IsDealer` and configure any optional dealer defaults in `ConfigurePrefab`: ```csharp using S1API.Entities; @@ -36,7 +36,6 @@ public class MyDealerNPC : NPC av.Gender = 0.0f; av.Height = 1.0f; }) - .EnsureDealer() .WithDealerDefaults(dd => { dd.WithSigningFee(1000f) // Cost to recruit @@ -246,7 +245,6 @@ public sealed class ProfessionalDealer : NPC av.WithBodyLayer("Avatar/Layers/Bottom/Jeans", new Color(0.2f, 0.2f, 0.3f)); av.WithAccessoryLayer("Avatar/Accessories/Feet/Sneakers/Sneakers", Color.black); }) - .EnsureDealer() .WithDealerDefaults(dd => { dd.WithSigningFee(2500f) // Higher fee = more experienced diff --git a/S1API/docs/prefab-configuration.md b/S1API/docs/prefab-configuration.md index 698f5f05..fb107793 100644 --- a/S1API/docs/prefab-configuration.md +++ b/S1API/docs/prefab-configuration.md @@ -28,6 +28,8 @@ The `ConfigurePrefab` method is called during NPC prefab creation and allows you **Important**: Customer, relationship, and schedule configuration must be done in `ConfigurePrefab` to ensure proper save/load behavior and network compatibility. +Declare fundamental roles with `IsCustomer`, `IsDealer`, and `IsSupplier` on the NPC type. S1API reads these properties from an uninitialized instance before `ConfigurePrefab`, so overrides must be stable, side-effect-free constants that do not depend on constructors or initialized fields. A dealer cannot also be a supplier, and every supplier must be physical. + ## NPCPrefabBuilder Methods ### WithIdentity @@ -162,18 +164,18 @@ builder.WithAppearanceDefaults(avatar => avatar.WithRandomImpostor(71, "Kyle", " If no impostor is configured, S1API preserves the existing custom NPC behavior. -### EnsureCustomer +### IsCustomer -Adds customer behavior component to the NPC. +Declares customer capability at the NPC type level. S1API adds the native customer component before `ConfigurePrefab` and network registration. ```csharp -builder.EnsureCustomer(); +public override bool IsCustomer => true; ``` **What it does:** - Adds the `Customer` component to the NPC - Enables customer behavior -- Required for `WithCustomerDefaults` to work +- Makes the infrastructure available to `WithCustomerDefaults` **Use when:** - NPC should act as a business customer @@ -469,7 +471,7 @@ protected override void ConfigurePrefab(NPCPrefabBuilder builder) } ``` -`WithSupplierDefaults(...)` ensures the supplier root automatically. `EnsureSupplier()` is also available when the default order limits, empty listings, and default messages are sufficient. +`IsSupplier` ensures the supplier root automatically. `WithSupplierDefaults(...)` adds optional order limits, listings, and messages; omit it when the native defaults are sufficient. Supplier delivery items must be registered and storable before prefab configuration runs. Order limits must be finite; the minimum must be non-negative, the maximum must be positive, and the maximum cannot be below the minimum. Keep the identity ID stable: S1API uses it for the supplier's persistent stash, shop, and delivery vehicle. @@ -569,7 +571,7 @@ plan.Add(new DriveToCarParkSpec { 1. **Set identity** (id, firstName, lastName) 2. **Set icon** (optional) 3. **Set spawn position** -4. **Choose one specialized role** (customer component, dealer root, or supplier root, if needed) +4. **Declare roles on the NPC type** (`IsCustomer`, `IsDealer`, and `IsSupplier`); customer can compose with either root, while dealer and supplier are mutually exclusive 5. **Configure role defaults** 6. **Set relationship defaults** 7. **Define schedule** (if physical NPC) @@ -577,6 +579,9 @@ plan.Add(new DriveToCarParkSpec { ### Complete Example ```csharp +public override bool IsPhysical => true; +public override bool IsCustomer => true; + protected override void ConfigurePrefab(NPCPrefabBuilder builder) { Vector3 shopPosition = new Vector3(-28.060f, 1.065f, 62.070f); @@ -588,7 +593,6 @@ protected override void ConfigurePrefab(NPCPrefabBuilder builder) lastName: "Shopkeeper") .WithIcon(null) .WithSpawnPosition(spawnPosition) - .EnsureCustomer() .WithCustomerDefaults(cd => { cd.WithSpending(200f, 800f) .WithOrdersPerWeek(2, 5) @@ -634,7 +638,7 @@ protected override void ConfigurePrefab(NPCPrefabBuilder builder) - **Don't modify customer, relationship, or schedule data at runtime** (except through proper APIs) - **Don't spawn NPCs in inaccessible locations** - **Don't use invalid GUIDs** for buildings, vehicles, or machines -- **Don't forget to call `EnsureCustomer()`** before `WithCustomerDefaults()` +- **Don't make role properties depend on constructor state**; S1API reads them from an uninitialized instance - **Don't mark one NPC as both a dealer and a supplier** - **Don't configure a supplier as non-physical** - **Don't assign native supplier scene objects**; use the S1API wrappers and hidden runtime integration @@ -644,13 +648,14 @@ protected override void ConfigurePrefab(NPCPrefabBuilder builder) Wrap configuration code in try-catch blocks: ```csharp +public override bool IsCustomer => true; + protected override void ConfigurePrefab(NPCPrefabBuilder builder) { try { // Configuration code here builder.WithSpawnPosition(spawnPos) - .EnsureCustomer() .WithCustomerDefaults(cd => { // Customer configuration }); diff --git a/S1API/docs/supplier-system.md b/S1API/docs/supplier-system.md index 285e043a..2600b912 100644 --- a/S1API/docs/supplier-system.md +++ b/S1API/docs/supplier-system.md @@ -34,7 +34,7 @@ public sealed class WarehouseSupplier : NPC } ``` -`WithSupplierDefaults(...)` calls `EnsureSupplier()` for you. Call `EnsureSupplier()` directly only when you want the supplier role with default data. +`IsSupplier` is the role declaration. `WithSupplierDefaults(...)` only supplies optional order, listing, and message configuration; omit it when the native defaults are sufficient. The string overload of `WithDeliveryItem(...)` is declaration-order safe: S1API stores the stable ID during NPC prefab discovery and resolves it when supplier diff --git a/skills/schedule-one-custom-npcs/SKILL.md b/skills/schedule-one-custom-npcs/SKILL.md index dc53de06..19471da4 100644 --- a/skills/schedule-one-custom-npcs/SKILL.md +++ b/skills/schedule-one-custom-npcs/SKILL.md @@ -21,7 +21,7 @@ Follow this order: Keep these responsibilities separate: -- `ConfigurePrefab(...)`: identity, icon, spawn position, relationship defaults, customer defaults, dealer defaults, inventory defaults, schedule, and required `Ensure*` components. +- `ConfigurePrefab(...)`: identity, icon, spawn position, relationship defaults, customer defaults, dealer defaults, inventory defaults, schedule, and action-specific `Ensure*` calls such as `plan.EnsureDealSignal()`. Role infrastructure comes automatically from `IsCustomer`, `IsDealer`, and `IsSupplier`; do not add `EnsureCustomer()`, `EnsureDealer()`, or `EnsureSupplier()`. - `OnCreated()`: `base.OnCreated()`, `Appearance.Build()`, `Schedule.Enable()`, `Schedule.InitializeActions()` when needed, dialogue wiring, event subscriptions, text messages, and runtime state. Do not move persistent customer, dealer, relationship, or schedule defaults into runtime code. @@ -35,9 +35,9 @@ Do not move persistent customer, dealer, relationship, or schedule defaults into ### Customer vs dealer -- Customer NPCs need `EnsureCustomer()` before `WithCustomerDefaults(...)`. +- Customer NPCs declare `public override bool IsCustomer => true;`, then optionally use `WithCustomerDefaults(...)`. - Customer schedules usually need `plan.EnsureDealSignal()`. -- Dealer NPCs need `public override bool IsDealer => true;` plus `EnsureDealer()` and `WithDealerDefaults(...)`. +- Dealer NPCs declare `public override bool IsDealer => true;`, then optionally use `WithDealerDefaults(...)`. - Dealer schedules need `plan.EnsureDealSignal()` to function correctly, and may use `plan.HandleDeal(...)` when that better fits the role. ## Hard Rules @@ -149,7 +149,7 @@ When producing code or guidance, include: ## Common Pitfalls - Setting appearance defaults but forgetting `Appearance.Build()`. -- Calling `WithCustomerDefaults(...)` without `EnsureCustomer()`. +- Making `IsCustomer`, `IsDealer`, or `IsSupplier` depend on constructor or initialized field state. - Calling `WithDealerDefaults(...)` without `IsDealer => true`. - Omitting `EnsureDealSignal()` for customer or dealer schedules that need deals/contracts. - Using advanced location-based actions without the matching `Ensure*` call. diff --git a/skills/schedule-one-custom-npcs/references/example-project-patterns.md b/skills/schedule-one-custom-npcs/references/example-project-patterns.md index 491723ef..433df210 100644 --- a/skills/schedule-one-custom-npcs/references/example-project-patterns.md +++ b/skills/schedule-one-custom-npcs/references/example-project-patterns.md @@ -11,10 +11,12 @@ Use when the NPC is visible in the world, directly interactable, and participate Recommended structure: ```csharp +public override bool IsPhysical => true; +public override bool IsCustomer => true; + builder.WithIdentity(...) .WithAppearanceDefaults(...) .WithSpawnPosition(...) - .EnsureCustomer() .WithCustomerDefaults(...) .WithRelationshipDefaults(...) .WithSchedule(plan => @@ -35,7 +37,7 @@ Typical runtime work: Extra checks: -- Confirm `EnsureCustomer()` exists before `WithCustomerDefaults(...)`. +- Confirm the NPC overrides `IsCustomer` with a stable, side-effect-free value. - Confirm the schedule includes `EnsureDealSignal()` when the customer should actively deal. - Keep spending, standards, and relationship requirements internally consistent. @@ -83,8 +85,7 @@ Minimum structure: ```csharp public override bool IsDealer => true; -builder.EnsureDealer() - .WithDealerDefaults(dd => +builder.WithDealerDefaults(dd => { dd.WithSigningFee(1000f) .WithCut(0.15f) diff --git a/skills/schedule-one-custom-npcs/references/s1api-custom-npc-reference.md b/skills/schedule-one-custom-npcs/references/s1api-custom-npc-reference.md index 771b712b..fba56bf6 100644 --- a/skills/schedule-one-custom-npcs/references/s1api-custom-npc-reference.md +++ b/skills/schedule-one-custom-npcs/references/s1api-custom-npc-reference.md @@ -115,8 +115,9 @@ Choose points that are on walkable surfaces and fit the planned route. ### Customer defaults ```csharp -builder.EnsureCustomer() - .WithCustomerDefaults(cd => +public override bool IsCustomer => true; + +builder.WithCustomerDefaults(cd => { cd.WithSpending(150f, 600f) .WithOrdersPerWeek(1, 4) @@ -153,8 +154,7 @@ Runtime customer work is limited to events and basic actions such as: ```csharp public override bool IsDealer => true; -builder.EnsureDealer() - .WithDealerDefaults(dd => +builder.WithDealerDefaults(dd => { dd.WithSigningFee(1000f) .WithCut(0.15f)