From 4099782a9db7d57dbf6098fa6c1c5c48dc975714 Mon Sep 17 00:00:00 2001 From: Seth W Date: Sun, 26 Apr 2026 14:41:38 -0400 Subject: [PATCH 01/22] Expose native wrappers and add utility APIs - Add NativeDefinition, NativeClothingDefinition, NativeClothingInstance public properties - Add CreateInstance() to ClothingItemDefinition for instance creation - Add IsItemRegistered(), EnsureItemRegistered() to ItemManager (idempotent registry check) - Add NativePlayer property to Player - Add CurrentBasicAvatarSettings wrapper property to Player - Add InsertClothing() and SendAppearance() proxy methods to Player --- S1API/Entities/Player.cs | 41 +++++++++++++++++++++++++++ S1API/Items/ClothingItemDefinition.cs | 14 +++++++++ S1API/Items/ClothingItemInstance.cs | 5 ++++ S1API/Items/ItemDefinition.cs | 5 ++++ S1API/Items/ItemManager.cs | 41 +++++++++++++++++++++++++++ 5 files changed, 106 insertions(+) diff --git a/S1API/Entities/Player.cs b/S1API/Entities/Player.cs index 1c3b9da7..6dcc3c3f 100644 --- a/S1API/Entities/Player.cs +++ b/S1API/Entities/Player.cs @@ -11,8 +11,10 @@ using System.Linq; using System.Reflection; using HarmonyLib; +using S1API.Avatar; using S1API.Entities.Interfaces; using S1API.Internal.Abstraction; +using S1API.Items; using S1API.Law; using UnityEngine; using S1API.Vehicles; @@ -63,6 +65,11 @@ public class Player : IEntity, IHealth /// internal S1PlayerScripts.Player S1Player; + /// + /// The underlying game player. + /// + public object NativePlayer => S1Player; + /// /// INTERNAL: Constructor to create a new player from an S1 instance. /// @@ -273,6 +280,40 @@ public PropertyWrapper? LastVisitedProperty { /// public object CurrentAvatarSettings => S1Player.CurrentAvatarSettings; + /// + /// The player's current avatar settings as an S1API wrapper. + /// + public BasicAvatarSettings? CurrentBasicAvatarSettings => + S1Player.CurrentAvatarSettings == null + ? null + : new BasicAvatarSettings(S1Player.CurrentAvatarSettings); + + /// + /// Inserts clothing into the matching player clothing slot. + /// + public void InsertClothing(ClothingItemInstance clothing) + { + if (clothing == null) + { + throw new ArgumentNullException(nameof(clothing)); + } + + S1Player.Clothing.InsertClothing(clothing.S1ClothingInstance); + } + + /// + /// Sends updated appearance settings through the game's native appearance flow. + /// + public void SendAppearance(BasicAvatarSettings settings) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + S1Player.SendAppearance(settings.S1BasicAvatarSettings); + } + /// /// Revives the player. /// diff --git a/S1API/Items/ClothingItemDefinition.cs b/S1API/Items/ClothingItemDefinition.cs index eb816233..da951658 100644 --- a/S1API/Items/ClothingItemDefinition.cs +++ b/S1API/Items/ClothingItemDefinition.cs @@ -34,6 +34,20 @@ internal ClothingItemDefinition(S1Clothing.ClothingDefinition definition) /// internal S1Clothing.ClothingDefinition S1ClothingDefinition { get; } + /// + /// The underlying game clothing definition. + /// + public object NativeClothingDefinition => S1ClothingDefinition; + + /// + /// Creates a clothing instance from this definition. + /// + public ClothingItemInstance CreateInstance(int quantity = 1, ClothingColor color = ClothingColor.White) => + new ClothingItemInstance(new S1Clothing.ClothingInstance( + S1ClothingDefinition, + quantity, + (S1Clothing.EClothingColor)color)); + /// /// The clothing slot this item occupies. /// diff --git a/S1API/Items/ClothingItemInstance.cs b/S1API/Items/ClothingItemInstance.cs index f545156f..3e04e5b4 100644 --- a/S1API/Items/ClothingItemInstance.cs +++ b/S1API/Items/ClothingItemInstance.cs @@ -17,6 +17,11 @@ public class ClothingItemInstance : ItemInstance /// internal readonly S1Clothing.ClothingInstance S1ClothingInstance; + /// + /// The underlying game clothing instance. + /// + public object NativeClothingInstance => S1ClothingInstance; + /// /// INTERNAL: Creates a ClothingItemInstance wrapper. /// diff --git a/S1API/Items/ItemDefinition.cs b/S1API/Items/ItemDefinition.cs index e5454afd..c93b8ad6 100644 --- a/S1API/Items/ItemDefinition.cs +++ b/S1API/Items/ItemDefinition.cs @@ -26,6 +26,11 @@ public class ItemDefinition : IGUIDReference /// internal S1ItemFramework.ItemDefinition S1ItemDefinition { get; } + /// + /// The underlying game item definition. + /// + public object NativeDefinition => S1ItemDefinition; + /// /// INTERNAL: Wraps an existing native item definition. /// diff --git a/S1API/Items/ItemManager.cs b/S1API/Items/ItemManager.cs index f6b4be29..240f190e 100644 --- a/S1API/Items/ItemManager.cs +++ b/S1API/Items/ItemManager.cs @@ -88,6 +88,47 @@ public static void RegisterItem(ItemDefinition definition) S1Registry.Instance.AddToRegistry(definition.S1ItemDefinition); } + /// + /// Checks whether an item ID is present in the active game registry. + /// + /// The ID of the item to look up. + /// True if the item is registered; otherwise, false. + public static bool IsItemRegistered(string itemID) + { + if (string.IsNullOrWhiteSpace(itemID) || S1Registry.Instance == null) + { + return false; + } + + return S1.Registry.ItemExists(itemID); + } + + /// + /// Registers an item only when it is missing from the active game registry. + /// + /// The item definition to register. + /// True if the item is registered after the call; otherwise, false. + public static bool EnsureItemRegistered(ItemDefinition definition) + { + if (definition == null) + { + throw new ArgumentNullException(nameof(definition), "Cannot register null item definition"); + } + + if (S1Registry.Instance == null) + { + return false; + } + + if (IsItemRegistered(definition.ID)) + { + return true; + } + + S1Registry.Instance.AddToRegistry(definition.S1ItemDefinition); + return IsItemRegistered(definition.ID); + } + /// /// Prevents a runtime-registered item from being removed during the next scene transition. /// This is useful for items that must survive a menu-to-game load sequence. From 91fa4e9cfcb1ed35ae0ed5bed7fe423283886fa0 Mon Sep 17 00:00:00 2001 From: Seth W Date: Sun, 26 Apr 2026 17:02:22 -0400 Subject: [PATCH 02/22] Update documentation + comments on Clothing APIs --- S1API/Items/ClothingItemCreator.cs | 2 +- S1API/Items/ClothingItemDefinition.cs | 4 ++-- S1API/docs/clothing-items.md | 25 ++++++++++++++++++------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/S1API/Items/ClothingItemCreator.cs b/S1API/Items/ClothingItemCreator.cs index 546d79ea..320a3993 100644 --- a/S1API/Items/ClothingItemCreator.cs +++ b/S1API/Items/ClothingItemCreator.cs @@ -14,7 +14,7 @@ namespace S1API.Items { /// /// Provides convenient static methods for creating custom clothing items. - /// Use for flexible configuration or for variants. + /// Use for flexible configuration or for variants. /// public static class ClothingItemCreator { diff --git a/S1API/Items/ClothingItemDefinition.cs b/S1API/Items/ClothingItemDefinition.cs index da951658..b7628bae 100644 --- a/S1API/Items/ClothingItemDefinition.cs +++ b/S1API/Items/ClothingItemDefinition.cs @@ -15,8 +15,8 @@ namespace S1API.Items /// Extends with clothing-specific properties. /// /// - /// Use to create new clothing items, - /// or to create variants of existing items. + /// Use to create new clothing items, + /// or to create variants of existing items. /// public sealed class ClothingItemDefinition : StorableItemDefinition { diff --git a/S1API/docs/clothing-items.md b/S1API/docs/clothing-items.md index e5c3ba04..c2498610 100644 --- a/S1API/docs/clothing-items.md +++ b/S1API/docs/clothing-items.md @@ -4,7 +4,7 @@ Clothing content bridges the avatar rendering system, runtime resource registry, ## Quick Checklist -1. Wait for the `Main` scene before touching registries (`OnSceneWasLoaded`). +1. Hook setup after the `Main` scene starts, then register clothing during `GameLifecycle.OnPreLoad` so definitions exist before save data loads. 2. (Optional) Clone an existing accessory prefab and override its materials/textures via `AccessoryFactory`. 3. Build or clone the clothing definition with `ClothingItemCreator`, pointing `WithClothingAsset` at your custom accessory path. 4. Register icons and pricing just like other items. @@ -88,7 +88,7 @@ int shopsAdded = ShopManager.AddToCompatibleShops(itemDefinition); MelonLogger.Msg($"Added to {shopsAdded} shop(s)"); ``` -Shop injection should happen after the registry confirms your item exists (e.g., immediately after `CreateClothingItem()` succeeds). +Shop injection should happen after the registry confirms your item exists (e.g., immediately after `Build()` succeeds or during `GameLifecycle.OnLoadComplete`). ## Slots and Application Types @@ -113,7 +113,7 @@ Shop injection should happen after the registry confirms your item exists (e.g., ## Testing and Troubleshooting -- **Initialization timing**: Guard the workflow with `if (sceneName == "Main" && !_initialized)` to avoid duplicate registration across scene loads. +- **Initialization timing**: Subscribe from the `Main` scene once, register definitions in `GameLifecycle.OnPreLoad`, and add shop entries in `GameLifecycle.OnLoadComplete`. - **Resource paths**: Match the string passed to `WithClothingAsset` with the `targetResourcePath` you registered via `AccessoryFactory`. - **Texture validation**: Log texture dimensions after loading so you catch mis-sized PNGs early. - **Shop coverage**: `ShopManager.AddToCompatibleShops` returns how many inventories accepted the item—log the count and ensure it is non-zero for your desired vendors. @@ -126,6 +126,7 @@ Here's a complete example combining all steps: ```csharp using MelonLoader; using S1API.Items; +using S1API.Lifecycle; using S1API.Rendering; using S1API.Internal.Utils; using S1API.Shops; @@ -141,11 +142,14 @@ public class MyMod : MelonMod { if (sceneName == "Main" && !_itemsInitialized) { - InitializeCustomClothing(); + GameLifecycle.OnPreLoad += InitializeCustomClothing; + GameLifecycle.OnLoadComplete += AddCustomClothingToShops; _itemsInitialized = true; } } + private ClothingItemDefinition customCap; + private void InitializeCustomClothing() { // Step 1: Create and register custom accessory @@ -179,7 +183,7 @@ public class MyMod : MelonMod assembly, "MyMod.Resources.CustomCap.icon.png"); - var customCap = ClothingItemCreator.CloneFrom("cap") + customCap = ClothingItemCreator.CloneFrom("cap") .WithBasicInfo( id: "custom_cap", name: "Custom Cap", @@ -194,10 +198,17 @@ public class MyMod : MelonMod { customCap.Icon = icon; } + MelonLogger.Msg($"Created custom clothing item: {customCap.Name}"); + } + + private void AddCustomClothingToShops() + { + if (customCap == null) + { + return; + } - // Step 3: Add to shops int shopsAdded = ShopManager.AddToCompatibleShops(customCap); - MelonLogger.Msg($"Created custom clothing item: {customCap.Name}"); MelonLogger.Msg($"Added to {shopsAdded} shop(s)"); } } From 05e57d699a98482e3d27c03b4fe703152b0bfa43 Mon Sep 17 00:00:00 2001 From: Seth W Date: Sun, 26 Apr 2026 17:05:01 -0400 Subject: [PATCH 03/22] Ensure custom clothing is recognized as clothing --- S1API/Items/ClothingItemDefinitionBuilder.cs | 32 ++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/S1API/Items/ClothingItemDefinitionBuilder.cs b/S1API/Items/ClothingItemDefinitionBuilder.cs index e6bb6f02..5b0e2237 100644 --- a/S1API/Items/ClothingItemDefinitionBuilder.cs +++ b/S1API/Items/ClothingItemDefinitionBuilder.cs @@ -13,6 +13,7 @@ #endif using System.Collections.Generic; +using S1API.Internal.Utils; using UnityEngine; namespace S1API.Items @@ -220,6 +221,8 @@ public ClothingItemDefinitionBuilder WithPricing(float basePurchasePrice, float /// A wrapper around the created clothing item definition. public ClothingItemDefinition Build() { + EnsureNativeClothingItemUi(); + // Register with the game's registry S1Registry.Instance.AddToRegistry(_definition); @@ -233,8 +236,37 @@ public ClothingItemDefinition Build() /// internal S1Clothing.ClothingDefinition BuildInternal() { + EnsureNativeClothingItemUi(); + return _definition; } + + private void EnsureNativeClothingItemUi() + { + if (_definition.CustomItemUI != null || S1Registry.Instance == null) + { + return; + } + + var allItems = S1Registry.Instance.GetAllItems(); + if (allItems == null) + { + return; + } + + foreach (var item in allItems) + { + if (item == null || + !CrossType.Is(item, out S1Clothing.ClothingDefinition clothingDefinition) || + clothingDefinition.CustomItemUI == null) + { + continue; + } + + _definition.CustomItemUI = clothingDefinition.CustomItemUI; + return; + } + } } } From 7d1be9370e9eb1ffec63ea6916045efe9a992afe Mon Sep 17 00:00:00 2001 From: Seth W Date: Sun, 26 Apr 2026 17:05:27 -0400 Subject: [PATCH 04/22] Simplify clothing equips --- S1API/Entities/Player.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/S1API/Entities/Player.cs b/S1API/Entities/Player.cs index 6dcc3c3f..fdb7653d 100644 --- a/S1API/Entities/Player.cs +++ b/S1API/Entities/Player.cs @@ -301,6 +301,30 @@ public void InsertClothing(ClothingItemInstance clothing) S1Player.Clothing.InsertClothing(clothing.S1ClothingInstance); } + /// + /// Creates and inserts clothing into the matching player clothing slot. + /// + /// The clothing item definition to equip. + /// The clothing color to use for the created instance. + /// The clothing instance that was inserted. + public ClothingItemInstance EquipClothing(ClothingItemDefinition definition, ClothingColor color = ClothingColor.White) + { + if (definition == null) + { + throw new ArgumentNullException(nameof(definition)); + } + + ClothingItemInstance clothing = definition.CreateInstance(color); + InsertClothing(clothing); + return clothing; + } + + /// + /// Refreshes the player's avatar from the current clothing slots. + /// + public void RefreshClothingAppearance() => + S1Player.Clothing.RefreshAppearance(); + /// /// Sends updated appearance settings through the game's native appearance flow. /// From ef85b8d3d81c9dfe4e6e9d709edb0bab74721ffa Mon Sep 17 00:00:00 2001 From: Seth W Date: Sun, 26 Apr 2026 17:05:39 -0400 Subject: [PATCH 05/22] Add OnRevive wrapper --- S1API/Entities/Player.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/S1API/Entities/Player.cs b/S1API/Entities/Player.cs index fdb7653d..4b286b86 100644 --- a/S1API/Entities/Player.cs +++ b/S1API/Entities/Player.cs @@ -383,6 +383,15 @@ public event Action OnDeath remove => EventHelper.RemoveListener(value, S1Player.Health.onDie); } + /// + /// Called when the player revives. + /// + public event Action OnRevive + { + add => EventHelper.AddListener(value, S1Player.Health.onRevive); + remove => EventHelper.RemoveListener(value, S1Player.Health.onRevive); + } + private static readonly HashSet InvinciblePlayers = new HashSet(); internal static bool IsPlayerInvincible(S1PlayerScripts.Player player) => InvinciblePlayers.Contains(player); From 927a5aa18b65b92c0556e1b333c4db154855c2b9 Mon Sep 17 00:00:00 2001 From: Seth W Date: Sun, 26 Apr 2026 17:06:09 -0400 Subject: [PATCH 06/22] Maintain existing clothing stack quantity behavior --- S1API/Items/ClothingItemDefinition.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/S1API/Items/ClothingItemDefinition.cs b/S1API/Items/ClothingItemDefinition.cs index b7628bae..857f06fc 100644 --- a/S1API/Items/ClothingItemDefinition.cs +++ b/S1API/Items/ClothingItemDefinition.cs @@ -40,12 +40,14 @@ internal ClothingItemDefinition(S1Clothing.ClothingDefinition definition) public object NativeClothingDefinition => S1ClothingDefinition; /// - /// Creates a clothing instance from this definition. + /// Creates a clothing instance from this definition with the specified color. /// - public ClothingItemInstance CreateInstance(int quantity = 1, ClothingColor color = ClothingColor.White) => + /// The clothing color to apply to the created instance. + /// A clothing instance using the specified color. + public ClothingItemInstance CreateInstance(ClothingColor color) => new ClothingItemInstance(new S1Clothing.ClothingInstance( S1ClothingDefinition, - quantity, + 1, (S1Clothing.EClothingColor)color)); /// From bc2250309adecd0ddd84588f63b4a38d9a5d122c Mon Sep 17 00:00:00 2001 From: Seth W Date: Sun, 26 Apr 2026 18:08:16 -0400 Subject: [PATCH 07/22] docs: clarify clothing registration idempotency --- S1API/docs/clothing-items.md | 67 +++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/S1API/docs/clothing-items.md b/S1API/docs/clothing-items.md index c2498610..24291a17 100644 --- a/S1API/docs/clothing-items.md +++ b/S1API/docs/clothing-items.md @@ -4,7 +4,7 @@ Clothing content bridges the avatar rendering system, runtime resource registry, ## Quick Checklist -1. Hook setup after the `Main` scene starts, then register clothing during `GameLifecycle.OnPreLoad` so definitions exist before save data loads. +1. Subscribe once after the `Main` scene starts, then register clothing during `GameLifecycle.OnPreLoad` so definitions exist before save data loads. Because `OnPreLoad` can run on each save load, registration code should be idempotent. 2. (Optional) Clone an existing accessory prefab and override its materials/textures via `AccessoryFactory`. 3. Build or clone the clothing definition with `ClothingItemCreator`, pointing `WithClothingAsset` at your custom accessory path. 4. Register icons and pricing just like other items. @@ -88,7 +88,7 @@ int shopsAdded = ShopManager.AddToCompatibleShops(itemDefinition); MelonLogger.Msg($"Added to {shopsAdded} shop(s)"); ``` -Shop injection should happen after the registry confirms your item exists (e.g., immediately after `Build()` succeeds or during `GameLifecycle.OnLoadComplete`). +Shop injection should happen after the registry confirms your item exists (e.g., immediately after `Build()` succeeds or during `GameLifecycle.OnLoadComplete`). `ShopManager.AddToCompatibleShops` can be called on later loads because shop additions skip existing listings. ## Slots and Application Types @@ -113,7 +113,7 @@ Shop injection should happen after the registry confirms your item exists (e.g., ## Testing and Troubleshooting -- **Initialization timing**: Subscribe from the `Main` scene once, register definitions in `GameLifecycle.OnPreLoad`, and add shop entries in `GameLifecycle.OnLoadComplete`. +- **Initialization timing**: Subscribe from the `Main` scene once, register definitions in `GameLifecycle.OnPreLoad`, and add shop entries in `GameLifecycle.OnLoadComplete`. Guard registration with `ItemManager.IsItemRegistered` or a definition lookup so repeated loads do not duplicate custom items. - **Resource paths**: Match the string passed to `WithClothingAsset` with the `targetResourcePath` you registered via `AccessoryFactory`. - **Texture validation**: Log texture dimensions after loading so you catch mis-sized PNGs early. - **Shop coverage**: `ShopManager.AddToCompatibleShops` returns how many inventories accepted the item—log the count and ensure it is non-zero for your desired vendors. @@ -136,7 +136,11 @@ using UnityEngine; public class MyMod : MelonMod { + private const string CustomItemId = "custom_cap"; + private const string CustomAccessoryPath = "MyMod/Accessories/CustomCap"; + private bool _itemsInitialized = false; + private ClothingItemDefinition customCap; public override void OnSceneWasLoaded(int buildIndex, string sceneName) { @@ -148,34 +152,41 @@ public class MyMod : MelonMod } } - private ClothingItemDefinition customCap; - private void InitializeCustomClothing() { + if (ItemManager.IsItemRegistered(CustomItemId)) + { + customCap = ItemManager.GetItemDefinition(CustomItemId) as ClothingItemDefinition; + return; + } + // Step 1: Create and register custom accessory var assembly = Assembly.GetExecutingAssembly(); - var customTexture = TextureUtils.LoadTextureFromResource( - assembly, - "MyMod.Resources.CustomCap.custom_cap_texture.png"); - - var textureReplacements = new Dictionary - { - { "_MainTex", customTexture }, - { "_BaseMap", customTexture }, - { "_Albedo", customTexture } - }; - - bool accessoryRegistered = AccessoryFactory.CreateAndRegisterAccessory( - sourceResourcePath: "avatar/accessories/head/cap/Cap", - targetResourcePath: "MyMod/Accessories/CustomCap", - newName: "CustomCap", - textureReplacements: textureReplacements, - colorTint: null); - - if (!accessoryRegistered) + if (!RuntimeResourceRegistry.IsRegistered(CustomAccessoryPath)) { - MelonLogger.Error("Failed to register custom accessory"); - return; + var customTexture = TextureUtils.LoadTextureFromResource( + assembly, + "MyMod.Resources.CustomCap.custom_cap_texture.png"); + + var textureReplacements = new Dictionary + { + { "_MainTex", customTexture }, + { "_BaseMap", customTexture }, + { "_Albedo", customTexture } + }; + + bool accessoryRegistered = AccessoryFactory.CreateAndRegisterAccessory( + sourceResourcePath: "avatar/accessories/head/cap/Cap", + targetResourcePath: CustomAccessoryPath, + newName: "CustomCap", + textureReplacements: textureReplacements, + colorTint: null); + + if (!accessoryRegistered) + { + MelonLogger.Error("Failed to register custom accessory"); + return; + } } // Step 2: Create clothing item definition @@ -185,10 +196,10 @@ public class MyMod : MelonMod customCap = ClothingItemCreator.CloneFrom("cap") .WithBasicInfo( - id: "custom_cap", + id: CustomItemId, name: "Custom Cap", description: "A custom cap with unique style.") - .WithClothingAsset("MyMod/Accessories/CustomCap") + .WithClothingAsset(CustomAccessoryPath) .WithColorable(false) .WithDefaultColor(ClothingColor.White) .WithPricing(75f, 0.5f) From 58276e20203dc37e144cdedf062ea99f478150e9 Mon Sep 17 00:00:00 2001 From: Seth W Date: Sun, 26 Apr 2026 18:08:21 -0400 Subject: [PATCH 08/22] fix: respect clothing instance quantity --- S1API/Items/ClothingItemDefinition.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/S1API/Items/ClothingItemDefinition.cs b/S1API/Items/ClothingItemDefinition.cs index 857f06fc..41f6c847 100644 --- a/S1API/Items/ClothingItemDefinition.cs +++ b/S1API/Items/ClothingItemDefinition.cs @@ -39,15 +39,32 @@ internal ClothingItemDefinition(S1Clothing.ClothingDefinition definition) /// public object NativeClothingDefinition => S1ClothingDefinition; + /// + /// Creates a clothing instance from this definition using the default color. + /// + /// The quantity to apply to the created clothing instance. + /// A clothing item instance using this definition's default color. + public override ItemInstance CreateInstance(int quantity = 1) => + CreateInstance(quantity, DefaultColor); + /// /// Creates a clothing instance from this definition with the specified color. /// /// The clothing color to apply to the created instance. /// A clothing instance using the specified color. public ClothingItemInstance CreateInstance(ClothingColor color) => + CreateInstance(1, color); + + /// + /// Creates a clothing instance from this definition with the specified quantity and color. + /// + /// The quantity to apply to the created clothing instance. + /// The clothing color to apply to the created instance. + /// A clothing instance using the specified quantity and color. + public ClothingItemInstance CreateInstance(int quantity, ClothingColor color) => new ClothingItemInstance(new S1Clothing.ClothingInstance( S1ClothingDefinition, - 1, + quantity, (S1Clothing.EClothingColor)color)); /// From 15ea7cb2651874bd1e343b8de8a7bbfb946d07a2 Mon Sep 17 00:00:00 2001 From: Seth W Date: Sun, 26 Apr 2026 18:08:24 -0400 Subject: [PATCH 09/22] docs: clarify avatar settings wrapper --- S1API/Entities/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S1API/Entities/Player.cs b/S1API/Entities/Player.cs index 4b286b86..f87bcd87 100644 --- a/S1API/Entities/Player.cs +++ b/S1API/Entities/Player.cs @@ -281,7 +281,7 @@ public PropertyWrapper? LastVisitedProperty { public object CurrentAvatarSettings => S1Player.CurrentAvatarSettings; /// - /// The player's current avatar settings as an S1API wrapper. + /// The player's current avatar settings as a new S1API wrapper around the current native settings. /// public BasicAvatarSettings? CurrentBasicAvatarSettings => S1Player.CurrentAvatarSettings == null From 97b90a183205bf82055bbcbc3a38b800a9cabe97 Mon Sep 17 00:00:00 2001 From: Seth W Date: Sun, 26 Apr 2026 18:08:26 -0400 Subject: [PATCH 10/22] refactor: use registry alias consistently --- S1API/Items/ItemManager.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/S1API/Items/ItemManager.cs b/S1API/Items/ItemManager.cs index 240f190e..b384dfc9 100644 --- a/S1API/Items/ItemManager.cs +++ b/S1API/Items/ItemManager.cs @@ -1,12 +1,10 @@ #if (IL2CPPMELON) -using S1 = Il2CppScheduleOne; using S1ItemFramework = Il2CppScheduleOne.ItemFramework; using S1Product = Il2CppScheduleOne.Product; using S1Registry = Il2CppScheduleOne.Registry; using S1Clothing = Il2CppScheduleOne.Clothing; using S1Packaging = Il2CppScheduleOne.Product.Packaging; #elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) -using S1 = ScheduleOne; using S1ItemFramework = ScheduleOne.ItemFramework; using S1Product = ScheduleOne.Product; using S1Registry = ScheduleOne.Registry; @@ -35,7 +33,7 @@ public static class ItemManager /// An instance of the item definition. public static ItemDefinition GetItemDefinition(string itemID) { - S1ItemFramework.ItemDefinition itemDefinition = S1.Registry.GetItem(itemID); + S1ItemFramework.ItemDefinition itemDefinition = S1Registry.GetItem(itemID); if (itemDefinition == null) return null; @@ -100,7 +98,7 @@ public static bool IsItemRegistered(string itemID) return false; } - return S1.Registry.ItemExists(itemID); + return S1Registry.ItemExists(itemID); } /// From 8abf293c285c1ca84cde0aa2bca4beb520321d84 Mon Sep 17 00:00:00 2001 From: Seth W Date: Sun, 26 Apr 2026 18:08:28 -0400 Subject: [PATCH 11/22] fix: document clothing UI template sharing --- S1API/Items/ClothingItemDefinitionBuilder.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/S1API/Items/ClothingItemDefinitionBuilder.cs b/S1API/Items/ClothingItemDefinitionBuilder.cs index 5b0e2237..2bf7c679 100644 --- a/S1API/Items/ClothingItemDefinitionBuilder.cs +++ b/S1API/Items/ClothingItemDefinitionBuilder.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using S1API.Internal.Utils; +using S1API.Logging; using UnityEngine; namespace S1API.Items @@ -24,6 +25,9 @@ namespace S1API.Items /// public sealed class ClothingItemDefinitionBuilder { + private static readonly Log Logger = new Log("ClothingItemDefinitionBuilder"); + private static bool _warnedMissingNativeClothingItemUi; + private readonly S1Clothing.ClothingDefinition _definition; /// @@ -263,9 +267,18 @@ private void EnsureNativeClothingItemUi() continue; } + // CustomItemUI is a native UI template. Share the existing template instead of + // cloning it here; listing state is bound per item by the game, and cloning + // Unity/Il2Cpp UI objects is riskier across runtimes. _definition.CustomItemUI = clothingDefinition.CustomItemUI; return; } + + if (!_warnedMissingNativeClothingItemUi) + { + _warnedMissingNativeClothingItemUi = true; + Logger.Warning("Could not find a native clothing CustomItemUI template. Custom clothing inventory UI may be incomplete if Build() runs before base clothing is registered."); + } } } } From 463202978bc9b30cfe093bbedaea4e08d2b638f9 Mon Sep 17 00:00:00 2001 From: Seth W Date: Sun, 26 Apr 2026 19:00:26 -0400 Subject: [PATCH 12/22] Align clothing UI missing-warning dedupe with existing keyed warning convention - Replace the one-shot native clothing UI warning with keyed per-reason dedupe. - Keep the richer registry/template failure diagnostics while avoiding repeated log spam. - Match the existing `GrowContainerAdditives` warning pattern without changing its behavior. --- S1API/Items/ClothingItemDefinitionBuilder.cs | 29 ++++++++++++++------ 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/S1API/Items/ClothingItemDefinitionBuilder.cs b/S1API/Items/ClothingItemDefinitionBuilder.cs index 2bf7c679..502f82a7 100644 --- a/S1API/Items/ClothingItemDefinitionBuilder.cs +++ b/S1API/Items/ClothingItemDefinitionBuilder.cs @@ -26,7 +26,8 @@ namespace S1API.Items public sealed class ClothingItemDefinitionBuilder { private static readonly Log Logger = new Log("ClothingItemDefinitionBuilder"); - private static bool _warnedMissingNativeClothingItemUi; + private static readonly object Gate = new object(); + private static readonly HashSet WarnedMissingNativeClothingItemUiReasons = new HashSet(System.StringComparer.OrdinalIgnoreCase); private readonly S1Clothing.ClothingDefinition _definition; @@ -247,14 +248,21 @@ internal S1Clothing.ClothingDefinition BuildInternal() private void EnsureNativeClothingItemUi() { - if (_definition.CustomItemUI != null || S1Registry.Instance == null) + if (_definition.CustomItemUI != null) { return; } + if (S1Registry.Instance == null) + { + WarnMissingNativeClothingItemUi("S1Registry.Instance is null"); + return; + } + var allItems = S1Registry.Instance.GetAllItems(); if (allItems == null) { + WarnMissingNativeClothingItemUi("S1Registry.Instance.GetAllItems() returned null"); return; } @@ -267,17 +275,22 @@ private void EnsureNativeClothingItemUi() continue; } - // CustomItemUI is a native UI template. Share the existing template instead of - // cloning it here; listing state is bound per item by the game, and cloning - // Unity/Il2Cpp UI objects is riskier across runtimes. _definition.CustomItemUI = clothingDefinition.CustomItemUI; return; } - if (!_warnedMissingNativeClothingItemUi) + WarnMissingNativeClothingItemUi("no S1Clothing.ClothingDefinition with S1Clothing.ClothingDefinition.CustomItemUI was found"); + } + + private static void WarnMissingNativeClothingItemUi(string reason) + { + if (string.IsNullOrWhiteSpace(reason)) + return; + + lock (Gate) { - _warnedMissingNativeClothingItemUi = true; - Logger.Warning("Could not find a native clothing CustomItemUI template. Custom clothing inventory UI may be incomplete if Build() runs before base clothing is registered."); + if (WarnedMissingNativeClothingItemUiReasons.Add(reason)) + Logger.Warning($"Could not borrow a native clothing CustomItemUI template ({reason}). Custom clothing inventory UI may be incomplete. This usually means Build() was called before any native clothing registered."); } } } From 4e54811e9cea73daa5cd09da1647bbc4edb35901 Mon Sep 17 00:00:00 2001 From: Seth W Date: Sun, 26 Apr 2026 19:01:29 -0400 Subject: [PATCH 13/22] Clarify CurrentBasicAvatarSettings returns a new wrapper per access --- S1API/Entities/Player.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/S1API/Entities/Player.cs b/S1API/Entities/Player.cs index f87bcd87..5f80b75f 100644 --- a/S1API/Entities/Player.cs +++ b/S1API/Entities/Player.cs @@ -281,7 +281,8 @@ public PropertyWrapper? LastVisitedProperty { public object CurrentAvatarSettings => S1Player.CurrentAvatarSettings; /// - /// The player's current avatar settings as a new S1API wrapper around the current native settings. + /// The player's current avatar settings as an S1API wrapper around S1Player.CurrentAvatarSettings. + /// returns a new instance on each access when S1Player.CurrentAvatarSettings is available. /// public BasicAvatarSettings? CurrentBasicAvatarSettings => S1Player.CurrentAvatarSettings == null From 2ca5ce7b2ac36c7f42f66794fdc2a1a703185c7e Mon Sep 17 00:00:00 2001 From: Seth W Date: Sun, 26 Apr 2026 19:52:35 -0400 Subject: [PATCH 14/22] Clarify clothing registration guards --- S1API/docs/clothing-items.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S1API/docs/clothing-items.md b/S1API/docs/clothing-items.md index 24291a17..ff9b436e 100644 --- a/S1API/docs/clothing-items.md +++ b/S1API/docs/clothing-items.md @@ -4,7 +4,7 @@ Clothing content bridges the avatar rendering system, runtime resource registry, ## Quick Checklist -1. Subscribe once after the `Main` scene starts, then register clothing during `GameLifecycle.OnPreLoad` so definitions exist before save data loads. Because `OnPreLoad` can run on each save load, registration code should be idempotent. +1. Subscribe to `GameLifecycle.OnPreLoad` once after the `Main` scene starts, then let that callback register clothing before save data loads. Because `OnPreLoad` fires on every load, guard registration with either a manual `ItemManager.IsItemRegistered` check when you need to recover an existing definition, or `ItemManager.EnsureItemRegistered` when you already have the definition instance. 2. (Optional) Clone an existing accessory prefab and override its materials/textures via `AccessoryFactory`. 3. Build or clone the clothing definition with `ClothingItemCreator`, pointing `WithClothingAsset` at your custom accessory path. 4. Register icons and pricing just like other items. From e6ef4bbffab9d2f80f96453d40eec52f745b71d2 Mon Sep 17 00:00:00 2001 From: Seth W Date: Mon, 27 Apr 2026 02:53:21 -0400 Subject: [PATCH 15/22] chore: address code rabbit warnings --- S1API/Entities/Player.cs | 8 ++++---- S1API/Items/ClothingItemDefinition.cs | 2 +- S1API/Items/ClothingItemDefinitionBuilder.cs | 12 ++++++++---- S1API/Items/ClothingItemInstance.cs | 2 +- S1API/Items/ItemDefinition.cs | 2 +- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/S1API/Entities/Player.cs b/S1API/Entities/Player.cs index 5f80b75f..6d5a7600 100644 --- a/S1API/Entities/Player.cs +++ b/S1API/Entities/Player.cs @@ -66,7 +66,7 @@ public class Player : IEntity, IHealth internal S1PlayerScripts.Player S1Player; /// - /// The underlying game player. + /// The underlying game player (ScheduleOne.PlayerScripts.Player). /// public object NativePlayer => S1Player; @@ -281,10 +281,10 @@ public PropertyWrapper? LastVisitedProperty { public object CurrentAvatarSettings => S1Player.CurrentAvatarSettings; /// - /// The player's current avatar settings as an S1API wrapper around S1Player.CurrentAvatarSettings. - /// returns a new instance on each access when S1Player.CurrentAvatarSettings is available. + /// Retrieves the player's current avatar settings as an S1API wrapper. + /// Returns a new instance on each call when S1Player.CurrentAvatarSettings is available. /// - public BasicAvatarSettings? CurrentBasicAvatarSettings => + public BasicAvatarSettings? GetCurrentBasicAvatarSettings() => S1Player.CurrentAvatarSettings == null ? null : new BasicAvatarSettings(S1Player.CurrentAvatarSettings); diff --git a/S1API/Items/ClothingItemDefinition.cs b/S1API/Items/ClothingItemDefinition.cs index 41f6c847..54984637 100644 --- a/S1API/Items/ClothingItemDefinition.cs +++ b/S1API/Items/ClothingItemDefinition.cs @@ -35,7 +35,7 @@ internal ClothingItemDefinition(S1Clothing.ClothingDefinition definition) internal S1Clothing.ClothingDefinition S1ClothingDefinition { get; } /// - /// The underlying game clothing definition. + /// The underlying game clothing definition (ScheduleOne.Clothing.ClothingDefinition). /// public object NativeClothingDefinition => S1ClothingDefinition; diff --git a/S1API/Items/ClothingItemDefinitionBuilder.cs b/S1API/Items/ClothingItemDefinitionBuilder.cs index 502f82a7..170f77d2 100644 --- a/S1API/Items/ClothingItemDefinitionBuilder.cs +++ b/S1API/Items/ClothingItemDefinitionBuilder.cs @@ -26,7 +26,6 @@ namespace S1API.Items public sealed class ClothingItemDefinitionBuilder { private static readonly Log Logger = new Log("ClothingItemDefinitionBuilder"); - private static readonly object Gate = new object(); private static readonly HashSet WarnedMissingNativeClothingItemUiReasons = new HashSet(System.StringComparer.OrdinalIgnoreCase); private readonly S1Clothing.ClothingDefinition _definition; @@ -81,6 +80,7 @@ internal ClothingItemDefinitionBuilder(S1Clothing.ClothingDefinition source) _definition.UsableInFilters = source.UsableInFilters; _definition.StoredItem = source.StoredItem; _definition.Equippable = source.Equippable; + _definition.CustomItemUI = source.CustomItemUI; // Copy clothing-specific properties _definition.Slot = source.Slot; @@ -275,6 +275,9 @@ private void EnsureNativeClothingItemUi() continue; } + // CustomItemUI is a native UI template. Share the existing template instead of + // cloning it here; listing state is bound per item by the game, and cloning + // Unity/Il2Cpp UI objects is riskier across runtimes. _definition.CustomItemUI = clothingDefinition.CustomItemUI; return; } @@ -285,12 +288,13 @@ private void EnsureNativeClothingItemUi() private static void WarnMissingNativeClothingItemUi(string reason) { if (string.IsNullOrWhiteSpace(reason)) + { return; + } - lock (Gate) + if (WarnedMissingNativeClothingItemUiReasons.Add(reason)) { - if (WarnedMissingNativeClothingItemUiReasons.Add(reason)) - Logger.Warning($"Could not borrow a native clothing CustomItemUI template ({reason}). Custom clothing inventory UI may be incomplete. This usually means Build() was called before any native clothing registered."); + Logger.Warning($"Could not borrow a native clothing CustomItemUI template ({reason}). Custom clothing inventory UI may be incomplete. This usually means Build() was called before any native clothing registered."); } } } diff --git a/S1API/Items/ClothingItemInstance.cs b/S1API/Items/ClothingItemInstance.cs index 3e04e5b4..3a7ed38d 100644 --- a/S1API/Items/ClothingItemInstance.cs +++ b/S1API/Items/ClothingItemInstance.cs @@ -18,7 +18,7 @@ public class ClothingItemInstance : ItemInstance internal readonly S1Clothing.ClothingInstance S1ClothingInstance; /// - /// The underlying game clothing instance. + /// The underlying game clothing instance (ScheduleOne.Clothing.ClothingInstance). /// public object NativeClothingInstance => S1ClothingInstance; diff --git a/S1API/Items/ItemDefinition.cs b/S1API/Items/ItemDefinition.cs index c93b8ad6..8870bf47 100644 --- a/S1API/Items/ItemDefinition.cs +++ b/S1API/Items/ItemDefinition.cs @@ -27,7 +27,7 @@ public class ItemDefinition : IGUIDReference internal S1ItemFramework.ItemDefinition S1ItemDefinition { get; } /// - /// The underlying game item definition. + /// The underlying game item definition (ScheduleOne.ItemFramework.ItemDefinition). /// public object NativeDefinition => S1ItemDefinition; From 6463fbeec800bc5990c30c84b72e96125779d4f5 Mon Sep 17 00:00:00 2001 From: Seth W Date: Mon, 27 Apr 2026 23:20:45 -0400 Subject: [PATCH 16/22] Remove public Native* properties --- S1API/Entities/Player.cs | 5 ----- S1API/Items/ClothingItemDefinition.cs | 5 ----- S1API/Items/ClothingItemInstance.cs | 5 ----- S1API/Items/ItemDefinition.cs | 5 ----- 4 files changed, 20 deletions(-) diff --git a/S1API/Entities/Player.cs b/S1API/Entities/Player.cs index 6d5a7600..5293265a 100644 --- a/S1API/Entities/Player.cs +++ b/S1API/Entities/Player.cs @@ -65,11 +65,6 @@ public class Player : IEntity, IHealth /// internal S1PlayerScripts.Player S1Player; - /// - /// The underlying game player (ScheduleOne.PlayerScripts.Player). - /// - public object NativePlayer => S1Player; - /// /// INTERNAL: Constructor to create a new player from an S1 instance. /// diff --git a/S1API/Items/ClothingItemDefinition.cs b/S1API/Items/ClothingItemDefinition.cs index 54984637..b6b25d52 100644 --- a/S1API/Items/ClothingItemDefinition.cs +++ b/S1API/Items/ClothingItemDefinition.cs @@ -34,11 +34,6 @@ internal ClothingItemDefinition(S1Clothing.ClothingDefinition definition) /// internal S1Clothing.ClothingDefinition S1ClothingDefinition { get; } - /// - /// The underlying game clothing definition (ScheduleOne.Clothing.ClothingDefinition). - /// - public object NativeClothingDefinition => S1ClothingDefinition; - /// /// Creates a clothing instance from this definition using the default color. /// diff --git a/S1API/Items/ClothingItemInstance.cs b/S1API/Items/ClothingItemInstance.cs index 3a7ed38d..f545156f 100644 --- a/S1API/Items/ClothingItemInstance.cs +++ b/S1API/Items/ClothingItemInstance.cs @@ -17,11 +17,6 @@ public class ClothingItemInstance : ItemInstance /// internal readonly S1Clothing.ClothingInstance S1ClothingInstance; - /// - /// The underlying game clothing instance (ScheduleOne.Clothing.ClothingInstance). - /// - public object NativeClothingInstance => S1ClothingInstance; - /// /// INTERNAL: Creates a ClothingItemInstance wrapper. /// diff --git a/S1API/Items/ItemDefinition.cs b/S1API/Items/ItemDefinition.cs index 8870bf47..e5454afd 100644 --- a/S1API/Items/ItemDefinition.cs +++ b/S1API/Items/ItemDefinition.cs @@ -26,11 +26,6 @@ public class ItemDefinition : IGUIDReference /// internal S1ItemFramework.ItemDefinition S1ItemDefinition { get; } - /// - /// The underlying game item definition (ScheduleOne.ItemFramework.ItemDefinition). - /// - public object NativeDefinition => S1ItemDefinition; - /// /// INTERNAL: Wraps an existing native item definition. /// From 2ab08a80caa4b80ebfaf77e905eeb7402e4f5463 Mon Sep 17 00:00:00 2001 From: Seth W Date: Mon, 27 Apr 2026 23:20:52 -0400 Subject: [PATCH 17/22] Make EquipClothing honor definition defaults --- S1API/Entities/Player.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/S1API/Entities/Player.cs b/S1API/Entities/Player.cs index 5293265a..40cddacb 100644 --- a/S1API/Entities/Player.cs +++ b/S1API/Entities/Player.cs @@ -298,19 +298,18 @@ public void InsertClothing(ClothingItemInstance clothing) } /// - /// Creates and inserts clothing into the matching player clothing slot. + /// Creates clothing with the definition's default color and inserts it into the matching player clothing slot. /// /// The clothing item definition to equip. - /// The clothing color to use for the created instance. /// The clothing instance that was inserted. - public ClothingItemInstance EquipClothing(ClothingItemDefinition definition, ClothingColor color = ClothingColor.White) + public ClothingItemInstance EquipClothing(ClothingItemDefinition definition) { if (definition == null) { throw new ArgumentNullException(nameof(definition)); } - ClothingItemInstance clothing = definition.CreateInstance(color); + ClothingItemInstance clothing = definition.CreateInstance(definition.DefaultColor); InsertClothing(clothing); return clothing; } From 97b707025bc9f5a728d9ba1299267fc52de48b44 Mon Sep 17 00:00:00 2001 From: Seth W Date: Mon, 27 Apr 2026 23:23:26 -0400 Subject: [PATCH 18/22] chore: resolve code rabbit nitpicks --- S1API/Items/ClothingItemDefinitionBuilder.cs | 2 +- S1API/docs/clothing-items.md | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/S1API/Items/ClothingItemDefinitionBuilder.cs b/S1API/Items/ClothingItemDefinitionBuilder.cs index 170f77d2..72ebeea5 100644 --- a/S1API/Items/ClothingItemDefinitionBuilder.cs +++ b/S1API/Items/ClothingItemDefinitionBuilder.cs @@ -26,7 +26,7 @@ namespace S1API.Items public sealed class ClothingItemDefinitionBuilder { private static readonly Log Logger = new Log("ClothingItemDefinitionBuilder"); - private static readonly HashSet WarnedMissingNativeClothingItemUiReasons = new HashSet(System.StringComparer.OrdinalIgnoreCase); + private static readonly HashSet WarnedMissingNativeClothingItemUiReasons = new HashSet(); private readonly S1Clothing.ClothingDefinition _definition; diff --git a/S1API/docs/clothing-items.md b/S1API/docs/clothing-items.md index ff9b436e..e8c17f78 100644 --- a/S1API/docs/clothing-items.md +++ b/S1API/docs/clothing-items.md @@ -138,6 +138,7 @@ public class MyMod : MelonMod { private const string CustomItemId = "custom_cap"; private const string CustomAccessoryPath = "MyMod/Accessories/CustomCap"; + private const string CustomTextureResourceName = "MyMod.Resources.CustomCap.custom_cap_texture.png"; private bool _itemsInitialized = false; private ClothingItemDefinition customCap; @@ -157,6 +158,12 @@ public class MyMod : MelonMod if (ItemManager.IsItemRegistered(CustomItemId)) { customCap = ItemManager.GetItemDefinition(CustomItemId) as ClothingItemDefinition; + if (customCap == null) + { + MelonLogger.Error($"Registered item '{CustomItemId}' is not a ClothingItemDefinition; custom clothing setup cannot continue."); + return; + } + return; } @@ -166,7 +173,13 @@ public class MyMod : MelonMod { var customTexture = TextureUtils.LoadTextureFromResource( assembly, - "MyMod.Resources.CustomCap.custom_cap_texture.png"); + CustomTextureResourceName); + + if (customTexture == null) + { + MelonLogger.Error($"Failed to load custom clothing texture resource: {CustomTextureResourceName}"); + return; + } var textureReplacements = new Dictionary { @@ -216,6 +229,7 @@ public class MyMod : MelonMod { if (customCap == null) { + MelonLogger.Warning($"Skipping shop registration because '{CustomItemId}' was not created as a ClothingItemDefinition."); return; } From 06fc96ea1de6aa82f4314c94578e4a16b960244b Mon Sep 17 00:00:00 2001 From: Seth W Date: Mon, 27 Apr 2026 23:25:16 -0400 Subject: [PATCH 19/22] Default clothing to non-stackable --- S1API/Items/ClothingItemDefinitionBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S1API/Items/ClothingItemDefinitionBuilder.cs b/S1API/Items/ClothingItemDefinitionBuilder.cs index 72ebeea5..79c38652 100644 --- a/S1API/Items/ClothingItemDefinitionBuilder.cs +++ b/S1API/Items/ClothingItemDefinitionBuilder.cs @@ -38,7 +38,7 @@ internal ClothingItemDefinitionBuilder() _definition = ScriptableObject.CreateInstance(); // Set defaults - _definition.StackLimit = 10; + _definition.StackLimit = 1; _definition.BasePurchasePrice = 10f; _definition.ResellMultiplier = 0.5f; _definition.Category = S1CoreItemFramework.EItemCategory.Clothing; From 9a79094dd0df4757dedd62cdd6572a8fa14a65ad Mon Sep 17 00:00:00 2001 From: Seth W Date: Mon, 27 Apr 2026 23:57:50 -0400 Subject: [PATCH 20/22] Add thread-safe synchronization to the static warning dedup set --- S1API/Items/ClothingItemDefinitionBuilder.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/S1API/Items/ClothingItemDefinitionBuilder.cs b/S1API/Items/ClothingItemDefinitionBuilder.cs index 79c38652..08863100 100644 --- a/S1API/Items/ClothingItemDefinitionBuilder.cs +++ b/S1API/Items/ClothingItemDefinitionBuilder.cs @@ -27,6 +27,7 @@ public sealed class ClothingItemDefinitionBuilder { private static readonly Log Logger = new Log("ClothingItemDefinitionBuilder"); private static readonly HashSet WarnedMissingNativeClothingItemUiReasons = new HashSet(); + private static readonly object WarnedMissingNativeClothingItemUiLock = new object(); private readonly S1Clothing.ClothingDefinition _definition; @@ -292,7 +293,13 @@ private static void WarnMissingNativeClothingItemUi(string reason) return; } - if (WarnedMissingNativeClothingItemUiReasons.Add(reason)) + bool shouldWarn; + lock (WarnedMissingNativeClothingItemUiLock) + { + shouldWarn = WarnedMissingNativeClothingItemUiReasons.Add(reason); + } + + if (shouldWarn) { Logger.Warning($"Could not borrow a native clothing CustomItemUI template ({reason}). Custom clothing inventory UI may be incomplete. This usually means Build() was called before any native clothing registered."); } From 2e84797d667f1868ae897f650cc0bceb70c3aeb5 Mon Sep 17 00:00:00 2001 From: Seth W Date: Tue, 28 Apr 2026 00:01:34 -0400 Subject: [PATCH 21/22] Handle null source.SlotsToBlock in the Mono clone path --- S1API/Items/ClothingItemDefinitionBuilder.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/S1API/Items/ClothingItemDefinitionBuilder.cs b/S1API/Items/ClothingItemDefinitionBuilder.cs index 08863100..f98c17e1 100644 --- a/S1API/Items/ClothingItemDefinitionBuilder.cs +++ b/S1API/Items/ClothingItemDefinitionBuilder.cs @@ -65,6 +65,11 @@ internal ClothingItemDefinitionBuilder() /// internal ClothingItemDefinitionBuilder(S1Clothing.ClothingDefinition source) { + if (source == null) + { + throw new System.ArgumentNullException(nameof(source)); + } + _definition = ScriptableObject.CreateInstance(); // Copy all StorableItemDefinition properties @@ -99,7 +104,9 @@ internal ClothingItemDefinitionBuilder(S1Clothing.ClothingDefinition source) } } #else - _definition.SlotsToBlock = new List(source.SlotsToBlock); + _definition.SlotsToBlock = source.SlotsToBlock == null + ? new List() + : new List(source.SlotsToBlock); #endif } From 84f47d5cc3b3c81fddaecb23ea236772570a1eb6 Mon Sep 17 00:00:00 2001 From: Seth W Date: Tue, 28 Apr 2026 00:06:16 -0400 Subject: [PATCH 22/22] Cache the resolved native CustomItemUI template on first access --- S1API/Items/ClothingItemDefinitionBuilder.cs | 21 +++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/S1API/Items/ClothingItemDefinitionBuilder.cs b/S1API/Items/ClothingItemDefinitionBuilder.cs index f98c17e1..506bad10 100644 --- a/S1API/Items/ClothingItemDefinitionBuilder.cs +++ b/S1API/Items/ClothingItemDefinitionBuilder.cs @@ -3,12 +3,14 @@ using S1ItemFramework = Il2CppScheduleOne.ItemFramework; using S1CoreItemFramework = Il2CppScheduleOne.Core.Items.Framework; using S1Registry = Il2CppScheduleOne.Registry; +using S1UiItems = Il2CppScheduleOne.UI.Items; using Il2CppCollections = Il2CppSystem.Collections.Generic; #elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) using S1Clothing = ScheduleOne.Clothing; using S1ItemFramework = ScheduleOne.ItemFramework; using S1CoreItemFramework = ScheduleOne.Core.Items.Framework; using S1Registry = ScheduleOne.Registry; +using S1UiItems = ScheduleOne.UI.Items; using Il2CppCollections = System.Collections.Generic; #endif @@ -28,6 +30,7 @@ public sealed class ClothingItemDefinitionBuilder private static readonly Log Logger = new Log("ClothingItemDefinitionBuilder"); private static readonly HashSet WarnedMissingNativeClothingItemUiReasons = new HashSet(); private static readonly object WarnedMissingNativeClothingItemUiLock = new object(); + private static S1UiItems.ItemUI? s_cachedNativeCustomItemUI; private readonly S1Clothing.ClothingDefinition _definition; @@ -261,6 +264,12 @@ private void EnsureNativeClothingItemUi() return; } + if (s_cachedNativeCustomItemUI != null) + { + _definition.CustomItemUI = s_cachedNativeCustomItemUI; + return; + } + if (S1Registry.Instance == null) { WarnMissingNativeClothingItemUi("S1Registry.Instance is null"); @@ -277,8 +286,13 @@ private void EnsureNativeClothingItemUi() foreach (var item in allItems) { if (item == null || - !CrossType.Is(item, out S1Clothing.ClothingDefinition clothingDefinition) || - clothingDefinition.CustomItemUI == null) + !CrossType.Is(item, out S1Clothing.ClothingDefinition clothingDefinition)) + { + continue; + } + + var customItemUI = clothingDefinition.CustomItemUI; + if (customItemUI == null) { continue; } @@ -286,7 +300,8 @@ private void EnsureNativeClothingItemUi() // CustomItemUI is a native UI template. Share the existing template instead of // cloning it here; listing state is bound per item by the game, and cloning // Unity/Il2Cpp UI objects is riskier across runtimes. - _definition.CustomItemUI = clothingDefinition.CustomItemUI; + s_cachedNativeCustomItemUI = customItemUI; + _definition.CustomItemUI = customItemUI; return; }