Skip to content

Add Clothing Wrappers and Registry Helpers#77

Merged
ifBars merged 22 commits into
ifBars:stablefrom
sethwalker1:feat/clothing-defs
Apr 28, 2026
Merged

Add Clothing Wrappers and Registry Helpers#77
ifBars merged 22 commits into
ifBars:stablefrom
sethwalker1:feat/clothing-defs

Conversation

@sethwalker1
Copy link
Copy Markdown

@sethwalker1 sethwalker1 commented Apr 26, 2026

Adds native clothing and item wrappers plus helper APIs for custom clothing workflows, so mods can register definitions, verify registry state, equip clothing, refresh appearance, and respond to revives without dropping down into game internals. Also updates the clothing docs to recommend the GameLifecycle.OnPreLoad / OnLoadComplete flow and ensures newly built clothing definitions inherit a native clothing item UI when one is available.

Open Question: Clothing Stack Default

I considered changing ClothingItemDefinitionBuilder to default clothing stacks to 1, but left the current default of 10 intact in this PR.

The reason is compatibility: clothing currently inherits the same default stack behavior used by the other builders and item helpers, and changing that default here would be a behavioral change rather than just an API/documentation improvement. That feels risky for existing mods that may already rely on the current builder behavior.

For mod-specific cases where a clothing item should clearly be non-stackable, setting StackLimit = 1 explicitly on the built definition still works today.

Please advise on how you would prefer to proceed from here:

  • refactor clothing definitions to default to 1
  • maintain the existing default of 10 for compatibility and require mods to opt into 1 explicitly

Release Notes

  • New Player API methods for clothing and avatar management: GetCurrentBasicAvatarSettings(), InsertClothing(), EquipClothing(), RefreshClothingAppearance(), SendAppearance(), and OnRevive event for handling player revival events
  • ClothingItemDefinition instance creation overloads supporting custom colors and quantities, with native UI template inheritance from registry
  • Registry helper methods in ItemManager: IsItemRegistered() for safe registry lookups and EnsureItemRegistered() for conditional registration
  • ClothingItemDefinitionBuilder default change from StackLimit of 10 to 1, making clothing non-stackable by default
  • Automatic native UI template assignment for clothing definitions that find a suitable template in the registry to provide consistent UI styling
  • Updated documentation recommends two-phase lifecycle pattern using GameLifecycle.OnPreLoad for definition registration and GameLifecycle.OnLoadComplete for shop integration, with idempotent implementation checks

Contributions by Author

Author Lines Added Lines Removed
Seth W 301 38

- 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
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

📝 Walkthrough

Walkthrough

Adds new public APIs to the Player entity for avatar and clothing management, including methods to retrieve settings, equip clothing with color customization, and refresh appearance. Extends ClothingItemDefinition with instance creation overloads, updates ItemManager with registration utility methods, and refactors documentation examples to use a two-phase initialization lifecycle pattern.

Changes

Cohort / File(s) Summary
Player Avatar & Clothing APIs
S1API/Entities/Player.cs
Added six new public members: getter for current avatar settings, methods to insert/equip clothing, refresh appearance, send appearance via avatar settings, and an OnRevive event connected to S1Player health system.
Clothing Item Instance Creation
S1API/Items/ClothingItemDefinition.cs
Added three overloads to CreateInstance: one using definition's default color, one accepting a color parameter, and one accepting both quantity and color for direct native instance construction.
Clothing Definition Builder UI Template Sharing
S1API/Items/ClothingItemDefinitionBuilder.cs
Added deduplication logic to automatically populate missing CustomItemUI by scanning registry for an existing native definition with a valid UI template, including dedup set to log warnings exactly once per reason. Changed default StackLimit from 10 to 1.
Item Registry Utilities
S1API/Items/ItemManager.cs
Added IsItemRegistered for null-safe registration lookup and EnsureItemRegistered for conditional registration with validation. Refactored GetItemDefinition to call registry directly.
Documentation Updates
S1API/Items/ClothingItemCreator.cs, S1API/docs/clothing-items.md
Updated XML docs to reference correct method overload signatures. Refactored example to implement idempotent initialization across two lifecycle phases (OnPreLoad and OnLoadComplete) with registration checks and resource validation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • v0.4.4f10 Game Update #58 — Modifies clothing item creation and definition APIs with related builder logic and definition class changes.

Poem

🐰 Carrots and threads now neatly aligned,
Avatars dressed in colors refined,
Registry templates no longer lost,
Fashion initialized with minimal cost!
A rabbit hops forth, wardrobe complete.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main changes: adding clothing wrappers (Player APIs, ClothingItemDefinition/Instance methods) and registry helpers (IsItemRegistered, EnsureItemRegistered).
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 70.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the game update Fixes the API for a Schedule 1 game update label Apr 26, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
S1API/docs/clothing-items.md (1)

145-202: ⚠️ Potential issue | 🟡 Minor

Example may re-register the clothing item on every save load.

GameLifecycle.OnPreLoad fires on every save load (per S1API/Lifecycle/GameLifecycle.cs), but InitializeCustomClothing is subscribed once with no idempotency check. Each subsequent load will re-run AccessoryFactory.CreateAndRegisterAccessory(...) and ClothingItemCreator.CloneFrom("cap").Build(), which calls S1Registry.Instance.AddToRegistry(_definition) unconditionally — likely producing duplicate registry entries for custom_cap.

Since this PR introduces ItemManager.IsItemRegistered / EnsureItemRegistered for exactly this scenario, the canonical example should leverage them so mods following this guide don't accidentally double-register.

📝 Suggested guard
     private void InitializeCustomClothing()
     {
+        if (ItemManager.IsItemRegistered("custom_cap"))
+        {
+            customCap = ItemManager.GetItemDefinition("custom_cap") as ClothingItemDefinition;
+            return;
+        }
+
         // Step 1: Create and register custom accessory

Similarly, AddCustomClothingToShops will run on every load and re-add the item to compatible shops; consider noting whether ShopManager.AddToCompatibleShops is itself idempotent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/docs/clothing-items.md` around lines 145 - 202,
InitializeCustomClothing currently re-registers the accessory and clothing on
every GameLifecycle.OnPreLoad invocation, causing duplicate entries; before
calling AccessoryFactory.CreateAndRegisterAccessory and
ClothingItemCreator.CloneFrom("cap").Build (which triggers
S1Registry.Instance.AddToRegistry), use
ItemManager.IsItemRegistered("custom_cap") or call
ItemManager.EnsureItemRegistered to make the registration idempotent, and only
perform the accessory registration/Build when the item is not already
registered; likewise, guard AddCustomClothingToShops by checking if the item is
already added to shops or rely on ShopManager.AddToCompatibleShops being
idempotent to avoid repeated additions.
🧹 Nitpick comments (5)
S1API/docs/clothing-items.md (1)

7-7: Tighten the wording around "after the Main scene starts".

The phrase "Hook setup after the Main scene starts, then register clothing during GameLifecycle.OnPreLoad" is slightly ambiguous — OnPreLoad fires before the save data loads, which can occur on any subsequent save load, not just first entry into Main. Consider clarifying that the Main-scene hook is for one-time event subscription and that the actual registration runs every time OnPreLoad fires (hence the need for an idempotency check via IsItemRegistered/EnsureItemRegistered).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/docs/clothing-items.md` at line 7, Clarify the wording to distinguish
the one-time hook and the repeated registration: state that you should set up
the hook once when the Main scene starts (subscribe to GameLifecycle.OnPreLoad),
and that the clothing registration logic runs every time GameLifecycle.OnPreLoad
fires (so it must be idempotent); explicitly mention using IsItemRegistered or
EnsureItemRegistered in the documentation to show the required per-onPreLoad
idempotency check.
S1API/Items/ClothingItemDefinition.cs (1)

47-51: Consider also overriding the base CreateInstance(int quantity = 1).

ItemDefinition.CreateInstance(int quantity = 1) is virtual and still returns a plain ItemInstance. Callers that hold a ClothingItemDefinition as the base type, or that follow the base API and pass a quantity, will silently bypass this clothing-specific factory and miss the ClothingItemInstance wrapper / color initialization. Consider overriding the virtual to delegate to the new color-aware factory (e.g., using DefaultColor) so the two creation paths stay consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/Items/ClothingItemDefinition.cs` around lines 47 - 51, The base virtual
CreateInstance(int quantity = 1) on ClothingItemDefinition should be overridden
so callers using the base signature still get a ClothingItemInstance with color
initialized; add an override of CreateInstance(int quantity = 1) in
ClothingItemDefinition that delegates to the existing
CreateInstance(ClothingColor color) (or the color-aware factory) using the
class's DefaultColor and the provided quantity so both creation paths return a
ClothingItemInstance initialized consistently.
S1API/Entities/Player.cs (1)

283-289: Allocates a new wrapper on each access.

Each call to CurrentBasicAvatarSettings constructs a fresh BasicAvatarSettings wrapper. This is inexpensive but means player.CurrentBasicAvatarSettings == player.CurrentBasicAvatarSettings would be false unless BasicAvatarSettings defines value-style equality, which could surprise consumers comparing references. Consider noting in the XML doc that a new wrapper is returned per access (or caching it lazily if cheap to do safely).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/Entities/Player.cs` around lines 283 - 289, The property
Player.CurrentBasicAvatarSettings currently allocates a new BasicAvatarSettings
wrapper on every access (wrapping S1Player.CurrentAvatarSettings), which can
surprise consumers comparing references; either update the XML doc for
CurrentBasicAvatarSettings to explicitly state "returns a new
BasicAvatarSettings instance on each access" or implement a simple lazy cache on
the Player instance (store a private BasicAvatarSettings? field and refresh it
when S1Player.CurrentAvatarSettings changes) so repeated accesses return the
same reference; refer to the CurrentBasicAvatarSettings property, the
BasicAvatarSettings type, and S1Player.CurrentAvatarSettings when applying the
change.
S1API/Items/ItemManager.cs (1)

96-130: LGTM with a small consistency nit.

The null/whitespace + S1Registry.Instance null guards, the early-return-on-already-registered fast path, and the post-add re-verification are all sound and make the helpers safely idempotent.

Minor consistency nit: the file mixes the S1.Registry namespace alias and the S1Registry type alias for the same static class (e.g., S1.Registry.ItemExists on line 103 vs. S1Registry.Instance.AddToRegistry on line 128). Picking one alias for this type would slightly improve readability, but it's not a functional concern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/Items/ItemManager.cs` around lines 96 - 130, The code mixes two names
for the same registry (S1.Registry and S1Registry); make them consistent by
choosing one alias and using it everywhere—e.g., change the call in
IsItemRegistered from S1.Registry.ItemExists(itemID) to
S1Registry.ItemExists(itemID) (or conversely update uses of S1Registry.Instance
to S1.Registry.Instance) so that IsItemRegistered, EnsureItemRegistered, and the
AddToRegistry invocation all reference the same identifier consistently.
S1API/Items/ClothingItemDefinitionBuilder.cs (1)

244-269: Silent best-effort: consider logging when no UI is borrowed.

If S1Registry.Instance is null, GetAllItems() returns null, or no existing clothing has a CustomItemUI, this method silently leaves _definition.CustomItemUI as null and Build() proceeds. That can lead to a clothing item registering with a missing UI and the failure mode showing up only at runtime in the inventory UI. Consider emitting a debug/warning log on the no-match path so mod authors can diagnose ordering issues (e.g., calling Build() before any base-game clothing is registered).

Also worth noting: assigning the borrowed CustomItemUI reference shares the same prefab/object across all custom clothing items built this way. Please confirm the native clothing UI template is intended to be shared (versus cloned per-definition) — if the game ever mutates per-item state on CustomItemUI, sharing the reference could cause cross-item bleed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/Items/ClothingItemDefinitionBuilder.cs` around lines 244 - 269,
EnsureNativeClothingItemUi currently silently leaves _definition.CustomItemUI
null when S1Registry.Instance is null, GetAllItems() returns null, or no
matching clothing is found; update EnsureNativeClothingItemUi to emit a
debug/warning via your logger when it cannot borrow a UI (include context that
Build() was called before any native clothing registered), and when you
successfully borrow a CustomItemUI from another S1Clothing.ClothingDefinition,
either clone/instantiate the prefab/object before assigning to
_definition.CustomItemUI (to avoid shared mutable state) or clearly
document/guard the shared-reference behavior; key symbols:
EnsureNativeClothingItemUi, S1Registry.Instance, GetAllItems(),
S1Clothing.ClothingDefinition.CustomItemUI, and _definition.CustomItemUI.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@S1API/docs/clothing-items.md`:
- Around line 145-202: InitializeCustomClothing currently re-registers the
accessory and clothing on every GameLifecycle.OnPreLoad invocation, causing
duplicate entries; before calling AccessoryFactory.CreateAndRegisterAccessory
and ClothingItemCreator.CloneFrom("cap").Build (which triggers
S1Registry.Instance.AddToRegistry), use
ItemManager.IsItemRegistered("custom_cap") or call
ItemManager.EnsureItemRegistered to make the registration idempotent, and only
perform the accessory registration/Build when the item is not already
registered; likewise, guard AddCustomClothingToShops by checking if the item is
already added to shops or rely on ShopManager.AddToCompatibleShops being
idempotent to avoid repeated additions.

---

Nitpick comments:
In `@S1API/docs/clothing-items.md`:
- Line 7: Clarify the wording to distinguish the one-time hook and the repeated
registration: state that you should set up the hook once when the Main scene
starts (subscribe to GameLifecycle.OnPreLoad), and that the clothing
registration logic runs every time GameLifecycle.OnPreLoad fires (so it must be
idempotent); explicitly mention using IsItemRegistered or EnsureItemRegistered
in the documentation to show the required per-onPreLoad idempotency check.

In `@S1API/Entities/Player.cs`:
- Around line 283-289: The property Player.CurrentBasicAvatarSettings currently
allocates a new BasicAvatarSettings wrapper on every access (wrapping
S1Player.CurrentAvatarSettings), which can surprise consumers comparing
references; either update the XML doc for CurrentBasicAvatarSettings to
explicitly state "returns a new BasicAvatarSettings instance on each access" or
implement a simple lazy cache on the Player instance (store a private
BasicAvatarSettings? field and refresh it when S1Player.CurrentAvatarSettings
changes) so repeated accesses return the same reference; refer to the
CurrentBasicAvatarSettings property, the BasicAvatarSettings type, and
S1Player.CurrentAvatarSettings when applying the change.

In `@S1API/Items/ClothingItemDefinition.cs`:
- Around line 47-51: The base virtual CreateInstance(int quantity = 1) on
ClothingItemDefinition should be overridden so callers using the base signature
still get a ClothingItemInstance with color initialized; add an override of
CreateInstance(int quantity = 1) in ClothingItemDefinition that delegates to the
existing CreateInstance(ClothingColor color) (or the color-aware factory) using
the class's DefaultColor and the provided quantity so both creation paths return
a ClothingItemInstance initialized consistently.

In `@S1API/Items/ClothingItemDefinitionBuilder.cs`:
- Around line 244-269: EnsureNativeClothingItemUi currently silently leaves
_definition.CustomItemUI null when S1Registry.Instance is null, GetAllItems()
returns null, or no matching clothing is found; update
EnsureNativeClothingItemUi to emit a debug/warning via your logger when it
cannot borrow a UI (include context that Build() was called before any native
clothing registered), and when you successfully borrow a CustomItemUI from
another S1Clothing.ClothingDefinition, either clone/instantiate the
prefab/object before assigning to _definition.CustomItemUI (to avoid shared
mutable state) or clearly document/guard the shared-reference behavior; key
symbols: EnsureNativeClothingItemUi, S1Registry.Instance, GetAllItems(),
S1Clothing.ClothingDefinition.CustomItemUI, and _definition.CustomItemUI.

In `@S1API/Items/ItemManager.cs`:
- Around line 96-130: The code mixes two names for the same registry
(S1.Registry and S1Registry); make them consistent by choosing one alias and
using it everywhere—e.g., change the call in IsItemRegistered from
S1.Registry.ItemExists(itemID) to S1Registry.ItemExists(itemID) (or conversely
update uses of S1Registry.Instance to S1.Registry.Instance) so that
IsItemRegistered, EnsureItemRegistered, and the AddToRegistry invocation all
reference the same identifier consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: caa14fea-c005-4ed4-a504-57f61b0f3b72

📥 Commits

Reviewing files that changed from the base of the PR and between 88359bc and 927a5aa.

📒 Files selected for processing (8)
  • S1API/Entities/Player.cs
  • S1API/Items/ClothingItemCreator.cs
  • S1API/Items/ClothingItemDefinition.cs
  • S1API/Items/ClothingItemDefinitionBuilder.cs
  • S1API/Items/ClothingItemInstance.cs
  • S1API/Items/ItemDefinition.cs
  • S1API/Items/ItemManager.cs
  • S1API/docs/clothing-items.md

@sethwalker1 sethwalker1 changed the title Clothing API Update Add Clothing Wrappers and Registry Helpers Apr 26, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
S1API/Items/ClothingItemDefinitionBuilder.cs (2)

29-29: Minor: OrdinalIgnoreCase on internally-authored warning keys is unnecessary.

The reasons fed into WarnedMissingNativeClothingItemUiReasons are hard-coded English strings produced by this class itself, so case-insensitive comparison adds no value and can mask near-duplicate keys that happen to differ only in casing. Plain Ordinal (the default) is sufficient.

♻️ Suggested change
-        private static readonly HashSet<string> WarnedMissingNativeClothingItemUiReasons = new HashSet<string>(System.StringComparer.OrdinalIgnoreCase);
+        private static readonly HashSet<string> WarnedMissingNativeClothingItemUiReasons = new HashSet<string>();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/Items/ClothingItemDefinitionBuilder.cs` at line 29, The HashSet
WarnedMissingNativeClothingItemUiReasons is initialized with
StringComparer.OrdinalIgnoreCase unnecessarily; change its initialization to use
the default ordinal comparer (either remove the StringComparer parameter and use
new HashSet<string>() or explicitly use System.StringComparer.Ordinal) so keys
remain case-sensitive and near-duplicate keys differing only by case are not
masked; update the static field declaration for
WarnedMissingNativeClothingItemUiReasons accordingly.

41-41: Address PR open question: default StackLimit for clothing should be 1.

The PR description flags this as open. Clothing items are inherently non-stackable in S1 (each instance carries its own Color/per-instance state), so a default of 10 is a footgun: mods that don't think to set StackLimit = 1 will produce stackable hats/shirts that lose per-instance color/equip state when stacked. Defaulting to 1 matches the domain semantics and pushes any opt-in for stacking onto the (rare) caller that actually wants it. The compatibility argument is weak here because today all clothing definitions in the game ship with StackLimit = 1 anyway, so flipping the builder default aligns with reality rather than diverging from it.

♻️ Suggested change
-            _definition.StackLimit = 10;
+            // Clothing instances carry per-instance state (color, equipped flag). Stacking
+            // would discard that state, so default to non-stackable. Callers can opt into
+            // larger stacks explicitly if they really want it.
+            _definition.StackLimit = 1;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/Items/ClothingItemDefinitionBuilder.cs` at line 41, The builder
currently sets _definition.StackLimit = 10 which makes clothing incorrectly
stackable; change the default in ClothingItemDefinitionBuilder to
_definition.StackLimit = 1 so clothing items are non-stackable by default
(update the constructor or Build/Init method that assigns
_definition.StackLimit), ensuring existing callers that rely on per-instance
state (Color/equip) keep correct semantics and only opt into stacking by
explicitly overriding StackLimit.
S1API/docs/clothing-items.md (1)

165-190: Doc example: missing null-check on customTexture before use.

TextureUtils.LoadTextureFromResource can return null if the embedded resource name is mistyped (a common copy-paste mistake for readers following the doc). The example then puts that null into the texture-replacement dictionary and hands it to AccessoryFactory.CreateAndRegisterAccessory, which is at best a confusing failure mode. A short guard makes the example safer to copy.

📘 Suggested doc change
             var customTexture = TextureUtils.LoadTextureFromResource(
                 assembly,
                 "MyMod.Resources.CustomCap.custom_cap_texture.png");
+
+            if (customTexture == null)
+            {
+                MelonLogger.Error("Failed to load custom cap texture from embedded resources.");
+                return;
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/docs/clothing-items.md` around lines 165 - 190, Add a null-check after
calling TextureUtils.LoadTextureFromResource to ensure customTexture is not null
before it is inserted into textureReplacements and passed to
AccessoryFactory.CreateAndRegisterAccessory; if
TextureUtils.LoadTextureFromResource returns null, call MelonLogger.Error with a
clear message including the resource name and return early to avoid passing null
into textureReplacements and subsequent registration code (keep references to
RuntimeResourceRegistry.IsRegistered, TextureUtils.LoadTextureFromResource,
textureReplacements, and AccessoryFactory.CreateAndRegisterAccessory to locate
the change).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@S1API/docs/clothing-items.md`:
- Around line 157-161: The example uses a silent "as" cast that can produce a
null customCap and an early return that hides the problem: update the example
around ItemManager.IsItemRegistered/ItemManager.GetItemDefinition and the
CustomItemId flow to defensively check the type result (i.e., check if the cast
to ClothingItemDefinition returned null), and if null log a clear error or
warning via your logger (or throw) explaining that the registered ID is not a
ClothingItemDefinition before returning; ensure AddCustomClothingToShops only
proceeds when customCap != null and show the log/check in the example so readers
copy the safer pattern.

---

Nitpick comments:
In `@S1API/docs/clothing-items.md`:
- Around line 165-190: Add a null-check after calling
TextureUtils.LoadTextureFromResource to ensure customTexture is not null before
it is inserted into textureReplacements and passed to
AccessoryFactory.CreateAndRegisterAccessory; if
TextureUtils.LoadTextureFromResource returns null, call MelonLogger.Error with a
clear message including the resource name and return early to avoid passing null
into textureReplacements and subsequent registration code (keep references to
RuntimeResourceRegistry.IsRegistered, TextureUtils.LoadTextureFromResource,
textureReplacements, and AccessoryFactory.CreateAndRegisterAccessory to locate
the change).

In `@S1API/Items/ClothingItemDefinitionBuilder.cs`:
- Line 29: The HashSet WarnedMissingNativeClothingItemUiReasons is initialized
with StringComparer.OrdinalIgnoreCase unnecessarily; change its initialization
to use the default ordinal comparer (either remove the StringComparer parameter
and use new HashSet<string>() or explicitly use System.StringComparer.Ordinal)
so keys remain case-sensitive and near-duplicate keys differing only by case are
not masked; update the static field declaration for
WarnedMissingNativeClothingItemUiReasons accordingly.
- Line 41: The builder currently sets _definition.StackLimit = 10 which makes
clothing incorrectly stackable; change the default in
ClothingItemDefinitionBuilder to _definition.StackLimit = 1 so clothing items
are non-stackable by default (update the constructor or Build/Init method that
assigns _definition.StackLimit), ensuring existing callers that rely on
per-instance state (Color/equip) keep correct semantics and only opt into
stacking by explicitly overriding StackLimit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 02696603-fa33-4dca-b009-34f02c1e796b

📥 Commits

Reviewing files that changed from the base of the PR and between 4e54811 and e6ef4bb.

📒 Files selected for processing (6)
  • S1API/Entities/Player.cs
  • S1API/Items/ClothingItemDefinition.cs
  • S1API/Items/ClothingItemDefinitionBuilder.cs
  • S1API/Items/ClothingItemInstance.cs
  • S1API/Items/ItemDefinition.cs
  • S1API/docs/clothing-items.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • S1API/Items/ItemDefinition.cs
  • S1API/Items/ClothingItemDefinition.cs
  • S1API/Entities/Player.cs

Comment thread S1API/docs/clothing-items.md
Copy link
Copy Markdown
Owner

@ifBars ifBars left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is useful clothing DX in this PR, but I’m not comfortable merging it as-is.

The main thing holding me back is that it mixes some solid wrapper improvements with new public Native* escape hatches that don’t feel aligned with S1API’s abstraction and cross-compatibility goals. Player.EquipClothing also currently ignores ClothingItemDefinition.DefaultColor.

I also think the stack-limit question should be resolved a bit more clearly. To me, 10 reads more like a generic storable-item default that leaked into the clothing builder rather than something clothing-specific. Since clothing instances carry per-instance state and feed into avatar/equip behavior, non-stackable feels like the safer default, and mods that really want a higher stack size can still opt into that explicitly.

I think this would benefit from another pass before merge:

  • remove or rethink the public Native* properties
  • fix EquipClothing so the no-argument path respects the definition’s default color
  • keep the PR focused on wrapper improvements that fit S1API’s public API direction
  • either resolve the clothing stack-limit direction here or pull it out of this PR’s scope

There is definitely mergeable value here, but I think the public API additions need a bit more tightening so they line up with the design goals of S1API.

Comment thread S1API/Entities/Player.cs Outdated
Comment thread S1API/Entities/Player.cs Outdated
Comment thread S1API/Items/ClothingItemDefinition.cs Outdated
Comment thread S1API/Items/ClothingItemInstance.cs Outdated
Comment thread S1API/Items/ItemDefinition.cs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
S1API/Items/ClothingItemDefinitionBuilder.cs (1)

83-103: ⚠️ Potential issue | 🟠 Major

Handle null source.SlotsToBlock in the Mono clone path.

Line 101 can throw ArgumentNullException when source.SlotsToBlock is null, while the IL2CPP branch already handles null safely. Please align both paths to avoid runtime crashes during cloning.

Proposed fix
 internal ClothingItemDefinitionBuilder(S1Clothing.ClothingDefinition source)
 {
+    if (source == null)
+        throw new System.ArgumentNullException(nameof(source));
+
     _definition = ScriptableObject.CreateInstance<S1Clothing.ClothingDefinition>();
@@
 `#else`
-    _definition.SlotsToBlock = new List<S1Clothing.EClothingSlot>(source.SlotsToBlock);
+    _definition.SlotsToBlock = source.SlotsToBlock != null
+        ? new List<S1Clothing.EClothingSlot>(source.SlotsToBlock)
+        : new List<S1Clothing.EClothingSlot>();
 `#endif`
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/Items/ClothingItemDefinitionBuilder.cs` around lines 83 - 103, The
Mono-path can throw ArgumentNullException when assigning
_definition.SlotsToBlock = new
List<S1Clothing.EClothingSlot>(source.SlotsToBlock) if source.SlotsToBlock is
null; make it mirror the IL2CPP branch by checking for null before copying: if
source.SlotsToBlock is null set _definition.SlotsToBlock to an empty
List<S1Clothing.EClothingSlot> (or new List<...>()), otherwise create the new
List from source.SlotsToBlock; update the code around _definition.SlotsToBlock
and source.SlotsToBlock in ClothingItemDefinitionBuilder.cs to perform this
null-safe assignment.
🧹 Nitpick comments (2)
S1API/Items/ClothingItemDefinitionBuilder.cs (2)

249-286: Cache the borrowed native CustomItemUI template after first resolution.

EnsureNativeClothingItemUi() scans the full registry every time CustomItemUI is missing. For bulk mod registration, a static cache of the first valid template would avoid repeated O(N) scans.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/Items/ClothingItemDefinitionBuilder.cs` around lines 249 - 286,
EnsureNativeClothingItemUi currently scans S1Registry.Instance.GetAllItems()
every time _definition.CustomItemUI is null; add a static cached template field
(e.g., a private static Il2CppObject/CustomItemUIType
s_cachedNativeCustomItemUI) and check it first in EnsureNativeClothingItemUi,
assign _definition.CustomItemUI from the static cache if present, and when you
find a valid S1Clothing.ClothingDefinition.CustomItemUI during the scan set both
_definition.CustomItemUI and the static cache so subsequent calls skip the O(N)
scan.

28-29: Add synchronization to protect the static warning dedup set from concurrent access.

The static WarnedMissingNativeClothingItemUiReasons HashSet is mutated at line 296 without synchronization. While the current codebase shows no threading patterns around clothing builder invocations, this represents a potential race condition if BuildInternal() is ever called concurrently from multiple threads. Wrap the Add call in a lock to ensure thread safety.

Proposed fix
 private static readonly HashSet<string> WarnedMissingNativeClothingItemUiReasons = new HashSet<string>();
+private static readonly object WarnedMissingNativeClothingItemUiLock = new object();
@@
-    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.");
-    }
+    lock (WarnedMissingNativeClothingItemUiLock)
+    {
+        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.");
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/Items/ClothingItemDefinitionBuilder.cs` around lines 28 - 29, The
static HashSet WarnedMissingNativeClothingItemUiReasons is mutated without
synchronization in BuildInternal(); introduce a private static readonly object
(e.g., WarnedMissingNativeClothingItemUiReasonsLock) and wrap the
HashSet.Add(...) call inside a
lock(WarnedMissingNativeClothingItemUiReasonsLock) block to prevent concurrent
access; update references in ClothingItemDefinitionBuilder to use this lock when
adding to the set to ensure thread safety.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@S1API/Items/ClothingItemDefinitionBuilder.cs`:
- Around line 83-103: The Mono-path can throw ArgumentNullException when
assigning _definition.SlotsToBlock = new
List<S1Clothing.EClothingSlot>(source.SlotsToBlock) if source.SlotsToBlock is
null; make it mirror the IL2CPP branch by checking for null before copying: if
source.SlotsToBlock is null set _definition.SlotsToBlock to an empty
List<S1Clothing.EClothingSlot> (or new List<...>()), otherwise create the new
List from source.SlotsToBlock; update the code around _definition.SlotsToBlock
and source.SlotsToBlock in ClothingItemDefinitionBuilder.cs to perform this
null-safe assignment.

---

Nitpick comments:
In `@S1API/Items/ClothingItemDefinitionBuilder.cs`:
- Around line 249-286: EnsureNativeClothingItemUi currently scans
S1Registry.Instance.GetAllItems() every time _definition.CustomItemUI is null;
add a static cached template field (e.g., a private static
Il2CppObject/CustomItemUIType s_cachedNativeCustomItemUI) and check it first in
EnsureNativeClothingItemUi, assign _definition.CustomItemUI from the static
cache if present, and when you find a valid
S1Clothing.ClothingDefinition.CustomItemUI during the scan set both
_definition.CustomItemUI and the static cache so subsequent calls skip the O(N)
scan.
- Around line 28-29: The static HashSet WarnedMissingNativeClothingItemUiReasons
is mutated without synchronization in BuildInternal(); introduce a private
static readonly object (e.g., WarnedMissingNativeClothingItemUiReasonsLock) and
wrap the HashSet.Add(...) call inside a
lock(WarnedMissingNativeClothingItemUiReasonsLock) block to prevent concurrent
access; update references in ClothingItemDefinitionBuilder to use this lock when
adding to the set to ensure thread safety.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c11e32e3-32cb-4205-879d-303e99c51233

📥 Commits

Reviewing files that changed from the base of the PR and between e6ef4bb and 06fc96e.

📒 Files selected for processing (4)
  • S1API/Entities/Player.cs
  • S1API/Items/ClothingItemDefinition.cs
  • S1API/Items/ClothingItemDefinitionBuilder.cs
  • S1API/docs/clothing-items.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • S1API/Entities/Player.cs
  • S1API/Items/ClothingItemDefinition.cs
  • S1API/docs/clothing-items.md

@sethwalker1
Copy link
Copy Markdown
Author

I was also feeling non-stackable clothing would be a better default, but wasn't sure how to proceed due to the default already being 10. I've now adjusted it to 1.

@sethwalker1 sethwalker1 requested a review from ifBars April 28, 2026 04:37
Copy link
Copy Markdown
Owner

@ifBars ifBars left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks much cleaner now. Thanks for taking another pass on it.

@ifBars ifBars merged commit fca0f3f into ifBars:stable Apr 28, 2026
3 of 5 checks passed
@sethwalker1 sethwalker1 deleted the feat/clothing-defs branch May 1, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

game update Fixes the API for a Schedule 1 game update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants