Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4099782
Expose native wrappers and add utility APIs
sethwalker1 Apr 26, 2026
91fa4e9
Update documentation + comments on Clothing APIs
sethwalker1 Apr 26, 2026
05e57d6
Ensure custom clothing is recognized as clothing
sethwalker1 Apr 26, 2026
7d1be93
Simplify clothing equips
sethwalker1 Apr 26, 2026
ef85b8d
Add OnRevive wrapper
sethwalker1 Apr 26, 2026
927a5aa
Maintain existing clothing stack quantity behavior
sethwalker1 Apr 26, 2026
bc22503
docs: clarify clothing registration idempotency
sethwalker1 Apr 26, 2026
58276e2
fix: respect clothing instance quantity
sethwalker1 Apr 26, 2026
15ea7cb
docs: clarify avatar settings wrapper
sethwalker1 Apr 26, 2026
97b90a1
refactor: use registry alias consistently
sethwalker1 Apr 26, 2026
8abf293
fix: document clothing UI template sharing
sethwalker1 Apr 26, 2026
4632029
Align clothing UI missing-warning dedupe with existing keyed warning …
sethwalker1 Apr 26, 2026
4e54811
Clarify CurrentBasicAvatarSettings returns a new wrapper per access
sethwalker1 Apr 26, 2026
2ca5ce7
Clarify clothing registration guards
sethwalker1 Apr 26, 2026
e6ef4bb
chore: address code rabbit warnings
sethwalker1 Apr 27, 2026
6463fbe
Remove public Native* properties
sethwalker1 Apr 28, 2026
2ab08a8
Make EquipClothing honor definition defaults
sethwalker1 Apr 28, 2026
97b7070
chore: resolve code rabbit nitpicks
sethwalker1 Apr 28, 2026
06fc96e
Default clothing to non-stackable
sethwalker1 Apr 28, 2026
9a79094
Add thread-safe synchronization to the static warning dedup set
sethwalker1 Apr 28, 2026
2e84797
Handle null source.SlotsToBlock in the Mono clone path
sethwalker1 Apr 28, 2026
84f47d5
Cache the resolved native CustomItemUI template on first access
sethwalker1 Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions S1API/Entities/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -273,6 +275,64 @@ public PropertyWrapper? LastVisitedProperty {
/// </summary>
public object CurrentAvatarSettings => S1Player.CurrentAvatarSettings;

/// <summary>
/// Retrieves the player's current avatar settings as an S1API <see cref="BasicAvatarSettings"/> wrapper.
/// Returns a new <see cref="BasicAvatarSettings"/> instance on each call when S1Player.CurrentAvatarSettings is available.
/// </summary>
public BasicAvatarSettings? GetCurrentBasicAvatarSettings() =>
S1Player.CurrentAvatarSettings == null
? null
: new BasicAvatarSettings(S1Player.CurrentAvatarSettings);

/// <summary>
/// Inserts clothing into the matching player clothing slot.
/// </summary>
public void InsertClothing(ClothingItemInstance clothing)
{
if (clothing == null)
{
throw new ArgumentNullException(nameof(clothing));
}

S1Player.Clothing.InsertClothing(clothing.S1ClothingInstance);
}

/// <summary>
/// Creates clothing with the definition's default color and inserts it into the matching player clothing slot.
/// </summary>
/// <param name="definition">The clothing item definition to equip.</param>
/// <returns>The clothing instance that was inserted.</returns>
public ClothingItemInstance EquipClothing(ClothingItemDefinition definition)
{
if (definition == null)
{
throw new ArgumentNullException(nameof(definition));
}

ClothingItemInstance clothing = definition.CreateInstance(definition.DefaultColor);
InsertClothing(clothing);
return clothing;
}

/// <summary>
/// Refreshes the player's avatar from the current clothing slots.
/// </summary>
public void RefreshClothingAppearance() =>
S1Player.Clothing.RefreshAppearance();

/// <summary>
/// Sends updated appearance settings through the game's native appearance flow.
/// </summary>
public void SendAppearance(BasicAvatarSettings settings)
{
if (settings == null)
{
throw new ArgumentNullException(nameof(settings));
}

S1Player.SendAppearance(settings.S1BasicAvatarSettings);
}

/// <summary>
/// Revives the player.
/// </summary>
Expand Down Expand Up @@ -318,6 +378,15 @@ public event Action OnDeath
remove => EventHelper.RemoveListener(value, S1Player.Health.onDie);
}

/// <summary>
/// Called when the player revives.
/// </summary>
public event Action OnRevive
{
add => EventHelper.AddListener(value, S1Player.Health.onRevive);
remove => EventHelper.RemoveListener(value, S1Player.Health.onRevive);
}

private static readonly HashSet<S1PlayerScripts.Player> InvinciblePlayers = new HashSet<S1PlayerScripts.Player>();

internal static bool IsPlayerInvincible(S1PlayerScripts.Player player) => InvinciblePlayers.Contains(player);
Expand Down
2 changes: 1 addition & 1 deletion S1API/Items/ClothingItemCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace S1API.Items
{
/// <summary>
/// Provides convenient static methods for creating custom clothing items.
/// Use <see cref="CreateBuilder"/> for flexible configuration or <see cref="CloneFrom"/> for variants.
/// Use <see cref="CreateBuilder()"/> for flexible configuration or <see cref="CloneFrom(string)"/> for variants.
/// </summary>
public static class ClothingItemCreator
{
Expand Down
32 changes: 30 additions & 2 deletions S1API/Items/ClothingItemDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ namespace S1API.Items
/// Extends <see cref="StorableItemDefinition"/> with clothing-specific properties.
/// </summary>
/// <remarks>
/// Use <see cref="ClothingItemCreator.CreateBuilder"/> to create new clothing items,
/// or <see cref="ClothingItemCreator.CloneFrom"/> to create variants of existing items.
/// Use <see cref="ClothingItemCreator.CreateBuilder()"/> to create new clothing items,
/// or <see cref="ClothingItemCreator.CloneFrom(string)"/> to create variants of existing items.
/// </remarks>
public sealed class ClothingItemDefinition : StorableItemDefinition
{
Expand All @@ -34,6 +34,34 @@ internal ClothingItemDefinition(S1Clothing.ClothingDefinition definition)
/// </summary>
internal S1Clothing.ClothingDefinition S1ClothingDefinition { get; }

/// <summary>
/// Creates a clothing instance from this definition using the default color.
/// </summary>
/// <param name="quantity">The quantity to apply to the created clothing instance.</param>
/// <returns>A clothing item instance using this definition's default color.</returns>
public override ItemInstance CreateInstance(int quantity = 1) =>
CreateInstance(quantity, DefaultColor);

/// <summary>
/// Creates a clothing instance from this definition with the specified color.
/// </summary>
/// <param name="color">The clothing color to apply to the created instance.</param>
/// <returns>A clothing instance using the specified color.</returns>
public ClothingItemInstance CreateInstance(ClothingColor color) =>
CreateInstance(1, color);

/// <summary>
/// Creates a clothing instance from this definition with the specified quantity and color.
/// </summary>
/// <param name="quantity">The quantity to apply to the created clothing instance.</param>
/// <param name="color">The clothing color to apply to the created instance.</param>
/// <returns>A clothing instance using the specified quantity and color.</returns>
public ClothingItemInstance CreateInstance(int quantity, ClothingColor color) =>
new ClothingItemInstance(new S1Clothing.ClothingInstance(
S1ClothingDefinition,
quantity,
(S1Clothing.EClothingColor)color));

/// <summary>
/// The clothing slot this item occupies.
/// </summary>
Expand Down
95 changes: 93 additions & 2 deletions S1API/Items/ClothingItemDefinitionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@
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

using System.Collections.Generic;
using S1API.Internal.Utils;
using S1API.Logging;
using UnityEngine;

namespace S1API.Items
Expand All @@ -23,6 +27,11 @@ namespace S1API.Items
/// </summary>
public sealed class ClothingItemDefinitionBuilder
{
private static readonly Log Logger = new Log("ClothingItemDefinitionBuilder");
private static readonly HashSet<string> WarnedMissingNativeClothingItemUiReasons = new HashSet<string>();
private static readonly object WarnedMissingNativeClothingItemUiLock = new object();
private static S1UiItems.ItemUI? s_cachedNativeCustomItemUI;

private readonly S1Clothing.ClothingDefinition _definition;

/// <summary>
Expand All @@ -33,7 +42,7 @@ internal ClothingItemDefinitionBuilder()
_definition = ScriptableObject.CreateInstance<S1Clothing.ClothingDefinition>();

// Set defaults
_definition.StackLimit = 10;
_definition.StackLimit = 1;
_definition.BasePurchasePrice = 10f;
_definition.ResellMultiplier = 0.5f;
_definition.Category = S1CoreItemFramework.EItemCategory.Clothing;
Expand All @@ -59,6 +68,11 @@ internal ClothingItemDefinitionBuilder()
/// </summary>
internal ClothingItemDefinitionBuilder(S1Clothing.ClothingDefinition source)
{
if (source == null)
{
throw new System.ArgumentNullException(nameof(source));
}

_definition = ScriptableObject.CreateInstance<S1Clothing.ClothingDefinition>();

// Copy all StorableItemDefinition properties
Expand All @@ -75,6 +89,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;
Expand All @@ -92,7 +107,9 @@ internal ClothingItemDefinitionBuilder(S1Clothing.ClothingDefinition source)
}
}
#else
_definition.SlotsToBlock = new List<S1Clothing.EClothingSlot>(source.SlotsToBlock);
_definition.SlotsToBlock = source.SlotsToBlock == null
? new List<S1Clothing.EClothingSlot>()
: new List<S1Clothing.EClothingSlot>(source.SlotsToBlock);
#endif
}

Expand Down Expand Up @@ -220,6 +237,8 @@ public ClothingItemDefinitionBuilder WithPricing(float basePurchasePrice, float
/// <returns>A wrapper around the created clothing item definition.</returns>
public ClothingItemDefinition Build()
{
EnsureNativeClothingItemUi();

// Register with the game's registry
S1Registry.Instance.AddToRegistry(_definition);

Expand All @@ -233,8 +252,80 @@ public ClothingItemDefinition Build()
/// </summary>
internal S1Clothing.ClothingDefinition BuildInternal()
{
EnsureNativeClothingItemUi();

return _definition;
}

private void EnsureNativeClothingItemUi()
{
if (_definition.CustomItemUI != null)
{
return;
}

if (s_cachedNativeCustomItemUI != null)
{
_definition.CustomItemUI = s_cachedNativeCustomItemUI;
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;
}

foreach (var item in allItems)
{
if (item == null ||
!CrossType.Is(item, out S1Clothing.ClothingDefinition clothingDefinition))
{
continue;
}

var customItemUI = clothingDefinition.CustomItemUI;
if (customItemUI == null)
{
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.
s_cachedNativeCustomItemUI = customItemUI;
_definition.CustomItemUI = customItemUI;
return;
}

WarnMissingNativeClothingItemUi("no S1Clothing.ClothingDefinition with S1Clothing.ClothingDefinition.CustomItemUI was found");
}

private static void WarnMissingNativeClothingItemUi(string reason)
{
if (string.IsNullOrWhiteSpace(reason))
{
return;
}

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.");
}
}
}
}

45 changes: 42 additions & 3 deletions S1API/Items/ItemManager.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -35,7 +33,7 @@ public static class ItemManager
/// <returns>An instance of the item definition.</returns>
public static ItemDefinition GetItemDefinition(string itemID)
{
S1ItemFramework.ItemDefinition itemDefinition = S1.Registry.GetItem(itemID);
S1ItemFramework.ItemDefinition itemDefinition = S1Registry.GetItem(itemID);

if (itemDefinition == null)
return null;
Expand Down Expand Up @@ -88,6 +86,47 @@ public static void RegisterItem(ItemDefinition definition)
S1Registry.Instance.AddToRegistry(definition.S1ItemDefinition);
}

/// <summary>
/// Checks whether an item ID is present in the active game registry.
/// </summary>
/// <param name="itemID">The ID of the item to look up.</param>
/// <returns>True if the item is registered; otherwise, false.</returns>
public static bool IsItemRegistered(string itemID)
{
if (string.IsNullOrWhiteSpace(itemID) || S1Registry.Instance == null)
{
return false;
}

return S1Registry.ItemExists(itemID);
}

/// <summary>
/// Registers an item only when it is missing from the active game registry.
/// </summary>
/// <param name="definition">The item definition to register.</param>
/// <returns>True if the item is registered after the call; otherwise, false.</returns>
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);
}

/// <summary>
/// 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.
Expand Down
Loading
Loading