diff --git a/S1API/Items/Additive/AdditiveDefinition.cs b/S1API/Items/Additive/AdditiveDefinition.cs
new file mode 100644
index 0000000..cc1fc2d
--- /dev/null
+++ b/S1API/Items/Additive/AdditiveDefinition.cs
@@ -0,0 +1,57 @@
+#if (IL2CPPMELON)
+using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
+#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+using S1ItemFramework = ScheduleOne.ItemFramework;
+#endif
+using System;
+using UnityEngine;
+
+namespace S1API.Items.Additive
+{
+ ///
+ /// Represents an additive item definition.
+ /// Extends with additive-specific properties.
+ ///
+ ///
+ /// Builder-only: these properties are intentionally read-only to avoid runtime surprises from mutating
+ /// globally-registered ScriptableObject definitions mid-session. Use to create
+ /// additives with configured effects.
+ ///
+ public sealed class AdditiveDefinition : Storable.StorableItemDefinition
+ {
+ ///
+ /// INTERNAL: Wraps an existing native additive definition.
+ ///
+ internal AdditiveDefinition(S1ItemFramework.AdditiveDefinition definition)
+ : base(definition)
+ {
+ S1AdditiveDefinition = definition;
+ }
+
+ ///
+ /// INTERNAL: A reference to the native game additive definition.
+ ///
+ internal S1ItemFramework.AdditiveDefinition S1AdditiveDefinition { get; }
+
+ ///
+ /// Display material used for the additive (if applicable).
+ ///
+ public Material DisplayMaterial => S1AdditiveDefinition.DisplayMaterial;
+
+ ///
+ /// Quality modifier applied by this additive.
+ ///
+ public float QualityChange => S1AdditiveDefinition.QualityChange;
+
+ ///
+ /// Yield multiplier applied by this additive.
+ ///
+ public float YieldMultiplier => S1AdditiveDefinition.YieldMultiplier;
+
+ ///
+ /// Instant growth fraction applied by this additive (0..1).
+ ///
+ public float InstantGrowth => S1AdditiveDefinition.InstantGrowth;
+ }
+}
+
diff --git a/S1API/Items/Additive/AdditiveDefinitionBuilder.cs b/S1API/Items/Additive/AdditiveDefinitionBuilder.cs
new file mode 100644
index 0000000..e8bf14f
--- /dev/null
+++ b/S1API/Items/Additive/AdditiveDefinitionBuilder.cs
@@ -0,0 +1,136 @@
+#if (IL2CPPMELON)
+using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
+using S1CoreItemFramework = Il2CppScheduleOne.Core.Items.Framework;
+#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+using S1ItemFramework = ScheduleOne.ItemFramework;
+using S1CoreItemFramework = ScheduleOne.Core.Items.Framework;
+#endif
+using S1API.Internal.Utils;
+using S1API.Items.Storable;
+using S1API.Logging;
+using UnityEngine;
+
+namespace S1API.Items.Additive
+{
+ ///
+ /// Builder for composing additive definitions at runtime.
+ /// Use fluent methods to configure additive properties before calling .
+ ///
+ public sealed class AdditiveDefinitionBuilder
+ : StorableItemDefinitionBuilderBase
+ {
+ private static readonly Log Logger = new Log("AdditiveDefinitionBuilder");
+
+ private S1ItemFramework.AdditiveDefinition AdditiveDefinition =>
+ CrossType.As(Definition);
+
+ ///
+ /// INTERNAL: Creates a new builder instance with a fresh AdditiveDefinition.
+ /// Only can instantiate this.
+ ///
+ internal AdditiveDefinitionBuilder()
+ : base(ScriptableObject.CreateInstance)
+ {
+ Definition.Category = S1CoreItemFramework.EItemCategory.Agriculture;
+ }
+
+ ///
+ /// INTERNAL: Creates a builder instance initialized by cloning an existing additive.
+ ///
+ internal AdditiveDefinitionBuilder(
+ S1ItemFramework.AdditiveDefinition source)
+ : base(source,
+ ScriptableObject.CreateInstance)
+ {
+ }
+
+ ///
+ protected override void CopyPropertiesFrom(
+ S1ItemFramework.StorableItemDefinition source)
+ {
+ base.CopyPropertiesFrom(source);
+
+ var additiveSource = CrossType.As(source);
+
+ // AdditiveDefinition properties (auto-properties with private set in Mono)
+ AutoPropertySetter.TrySet(AdditiveDefinition, nameof(S1ItemFramework.AdditiveDefinition.DisplayMaterial),
+ additiveSource.DisplayMaterial);
+ AutoPropertySetter.TrySet(AdditiveDefinition, nameof(S1ItemFramework.AdditiveDefinition.QualityChange),
+ additiveSource.QualityChange);
+ AutoPropertySetter.TrySet(AdditiveDefinition, nameof(S1ItemFramework.AdditiveDefinition.YieldMultiplier),
+ additiveSource.YieldMultiplier);
+ AutoPropertySetter.TrySet(AdditiveDefinition, nameof(S1ItemFramework.AdditiveDefinition.InstantGrowth),
+ additiveSource.InstantGrowth);
+ }
+
+ ///
+ /// Sets the display material for this additive.
+ ///
+ public AdditiveDefinitionBuilder WithDisplayMaterial(Material material)
+ {
+ if (!AutoPropertySetter.TrySet(AdditiveDefinition, nameof(S1ItemFramework.AdditiveDefinition.DisplayMaterial),
+ material))
+ {
+ Logger.Warning(
+ $"Failed to set DisplayMaterial on AdditiveDefinition '{AdditiveDefinition.ID ?? ""}'.");
+ }
+
+ return this;
+ }
+
+ ///
+ /// Sets the effect values for this additive.
+ ///
+ public AdditiveDefinitionBuilder WithEffects(float yieldMultiplier, float instantGrowth, float qualityChange)
+ {
+ instantGrowth = Mathf.Clamp01(instantGrowth);
+ if (!AutoPropertySetter.TrySet(AdditiveDefinition, nameof(S1ItemFramework.AdditiveDefinition.YieldMultiplier),
+ yieldMultiplier))
+ {
+ Logger.Warning(
+ $"Failed to set YieldMultiplier on AdditiveDefinition '{AdditiveDefinition.ID ?? ""}'.");
+ }
+
+ if (!AutoPropertySetter.TrySet(AdditiveDefinition, nameof(S1ItemFramework.AdditiveDefinition.InstantGrowth),
+ instantGrowth))
+ {
+ Logger.Warning(
+ $"Failed to set InstantGrowth on AdditiveDefinition '{AdditiveDefinition.ID ?? ""}'.");
+ }
+
+ if (!AutoPropertySetter.TrySet(AdditiveDefinition, nameof(S1ItemFramework.AdditiveDefinition.QualityChange),
+ qualityChange))
+ {
+ Logger.Warning(
+ $"Failed to set QualityChange on AdditiveDefinition '{AdditiveDefinition.ID ?? ""}'.");
+ }
+
+ return this;
+ }
+
+ ///
+ /// Builds the item definition, registers it with the game's registry, and returns a wrapper.
+ ///
+ /// A wrapper around the created additive definition.
+ public new AdditiveDefinition Build()
+ {
+ return (AdditiveDefinition)base.Build();
+ }
+
+ ///
+ /// INTERNAL: Builds and returns the raw game item definition without registering.
+ /// Used internally by S1API. Modders should use instead.
+ ///
+ internal new S1ItemFramework.AdditiveDefinition BuildInternal()
+ {
+ return AdditiveDefinition;
+ }
+
+ ///
+ protected override Storable.StorableItemDefinition CreateWrapper(
+ S1ItemFramework.StorableItemDefinition definition)
+ {
+ return new AdditiveDefinition(AdditiveDefinition);
+ }
+ }
+}
\ No newline at end of file
diff --git a/S1API/Items/Additive/AdditiveItemCreator.cs b/S1API/Items/Additive/AdditiveItemCreator.cs
new file mode 100644
index 0000000..98c6893
--- /dev/null
+++ b/S1API/Items/Additive/AdditiveItemCreator.cs
@@ -0,0 +1,71 @@
+#if (IL2CPPMELON)
+using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
+using S1Registry = Il2CppScheduleOne.Registry;
+#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+using S1ItemFramework = ScheduleOne.ItemFramework;
+using S1Registry = ScheduleOne.Registry;
+#endif
+using System;
+using S1API.Internal.Utils;
+
+namespace S1API.Items.Additive
+{
+ ///
+ /// Provides convenient static methods for creating custom additive items.
+ /// Use for creating additives from scratch, or for variants.
+ ///
+ public static class AdditiveItemCreator
+ {
+ ///
+ /// Creates a new builder for composing an additive definition with full flexibility.
+ /// Use fluent methods to configure the additive, then call Build() to register it.
+ ///
+ public static AdditiveDefinitionBuilder CreateBuilder()
+ {
+ return new AdditiveDefinitionBuilder();
+ }
+
+ ///
+ /// Creates a new additive builder by cloning an existing additive by ID.
+ ///
+ /// The ID of the additive to clone.
+ /// A builder pre-configured with the source additive's properties.
+ /// Thrown if the source item does not exist or is not an additive.
+ public static AdditiveDefinitionBuilder CloneFrom(string sourceItemId)
+ {
+ if (string.IsNullOrWhiteSpace(sourceItemId))
+ {
+ throw new ArgumentException("Source item ID cannot be null or whitespace", nameof(sourceItemId));
+ }
+
+ var sourceDefinition = S1Registry.GetItem(sourceItemId);
+ if (sourceDefinition == null)
+ {
+ throw new ArgumentException($"Source item with ID '{sourceItemId}' not found in registry", nameof(sourceItemId));
+ }
+
+ if (!CrossType.Is(sourceDefinition, out S1ItemFramework.AdditiveDefinition additiveDef))
+ {
+ throw new ArgumentException($"Item '{sourceItemId}' is not an AdditiveDefinition", nameof(sourceItemId));
+ }
+
+ return new AdditiveDefinitionBuilder(additiveDef);
+ }
+
+ ///
+ /// Creates a new additive builder by cloning an existing additive wrapper.
+ ///
+ /// The additive definition to clone from.
+ /// A builder pre-configured with the source additive's properties.
+ public static AdditiveDefinitionBuilder CloneFrom(AdditiveDefinition source)
+ {
+ if (source == null)
+ {
+ throw new ArgumentNullException(nameof(source), "Source additive definition cannot be null");
+ }
+
+ return new AdditiveDefinitionBuilder(source.S1AdditiveDefinition);
+ }
+ }
+}
+
diff --git a/S1API/Items/AdditiveDefinition.cs b/S1API/Items/AdditiveDefinition.cs
index f7ebf5c..70882ca 100644
--- a/S1API/Items/AdditiveDefinition.cs
+++ b/S1API/Items/AdditiveDefinition.cs
@@ -4,6 +4,7 @@
using S1ItemFramework = ScheduleOne.ItemFramework;
#endif
+using System;
using UnityEngine;
namespace S1API.Items
@@ -17,6 +18,7 @@ namespace S1API.Items
/// globally-registered ScriptableObject definitions mid-session. Use to create
/// additives with configured effects.
///
+ [Obsolete("Use S1API.Items.Additive.AdditiveDefinition instead")]
public sealed class AdditiveDefinition : StorableItemDefinition
{
///
diff --git a/S1API/Items/AdditiveDefinitionBuilder.cs b/S1API/Items/AdditiveDefinitionBuilder.cs
index d78cb39..00ffcfe 100644
--- a/S1API/Items/AdditiveDefinitionBuilder.cs
+++ b/S1API/Items/AdditiveDefinitionBuilder.cs
@@ -22,6 +22,7 @@ namespace S1API.Items
/// Builder for composing additive definitions at runtime.
/// Use fluent methods to configure additive properties before calling .
///
+ [Obsolete("Use S1API.Items.Additive.AdditiveDefinitionBuilder instead")]
public sealed class AdditiveDefinitionBuilder
{
private static readonly Log Logger = new Log("AdditiveDefinitionBuilder");
diff --git a/S1API/Items/AdditiveItemCreator.cs b/S1API/Items/AdditiveItemCreator.cs
index 18d0523..6e87d8b 100644
--- a/S1API/Items/AdditiveItemCreator.cs
+++ b/S1API/Items/AdditiveItemCreator.cs
@@ -15,6 +15,7 @@ namespace S1API.Items
/// Provides convenient static methods for creating custom additive items.
/// Use for creating additives from scratch, or for variants.
///
+ [Obsolete("Use S1API.Items.Additive.AdditiveItemCreator instead")]
public static class AdditiveItemCreator
{
///
diff --git a/S1API/Items/Buildable/BuildableItemCreator.cs b/S1API/Items/Buildable/BuildableItemCreator.cs
new file mode 100644
index 0000000..65b7a69
--- /dev/null
+++ b/S1API/Items/Buildable/BuildableItemCreator.cs
@@ -0,0 +1,109 @@
+#if (IL2CPPMELON)
+using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
+using S1Registry = Il2CppScheduleOne.Registry;
+#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+using S1ItemFramework = ScheduleOne.ItemFramework;
+using S1Registry = ScheduleOne.Registry;
+#endif
+using System;
+using S1API.Internal.Utils;
+
+namespace S1API.Items.Buildable
+{
+ ///
+ /// Provides convenient static methods for creating custom buildable items.
+ ///
+ ///
+ /// Use for creating items from scratch,
+ /// or for creating variants of existing buildable items.
+ ///
+ public static class BuildableItemCreator
+ {
+ ///
+ /// Creates a new builder for composing a buildable item definition with full flexibility.
+ /// Use fluent methods to configure the item, then call Build() to register it.
+ ///
+ /// A new BuildableItemDefinitionBuilder instance for fluent configuration.
+ ///
+ ///
+ /// var item = BuildableItemCreator.CreateBuilder()
+ /// .WithBasicInfo("my_rack", "Custom Storage Rack", "A custom storage rack")
+ /// .WithBuildSound(BuildSoundType.Metal)
+ /// .WithPricing(75f, 0.5f)
+ /// .Build();
+ ///
+ ///
+ public static BuildableItemDefinitionBuilder CreateBuilder()
+ {
+ return new BuildableItemDefinitionBuilder();
+ }
+
+ ///
+ /// Creates a new buildable item by cloning an existing item's properties.
+ /// This is useful for creating variants of existing items (e.g., different materials, sizes).
+ ///
+ /// The ID of the existing buildable item to clone from.
+ /// A builder initialized with the source item's properties, ready for customization.
+ ///
+ ///
+ /// var metalRack = BuildableItemCreator.CloneFrom("StorageRack-1x0.5")
+ /// .WithBasicInfo("metal_rack_small", "Small Metal Storage Rack", "A metal version")
+ /// .WithBuildSound(BuildSoundType.Metal)
+ /// .WithPricing(72f, 0.5f)
+ /// .Build();
+ ///
+ ///
+ public static BuildableItemDefinitionBuilder CloneFrom(string sourceItemId)
+ {
+ if (string.IsNullOrWhiteSpace(sourceItemId))
+ {
+ throw new ArgumentException("Source item ID cannot be null or whitespace", nameof(sourceItemId));
+ }
+
+ var sourceDefinition = S1Registry.GetItem(sourceItemId);
+
+ if (sourceDefinition == null)
+ {
+ throw new System.ArgumentException(
+ $"Source item with ID '{sourceItemId}' not found in registry",
+ nameof(sourceItemId)
+ );
+ }
+
+ // Try to cast to BuildableItemDefinition
+ if (!CrossType.Is(sourceDefinition, out S1ItemFramework.BuildableItemDefinition buildableDef))
+ {
+ throw new System.ArgumentException(
+ $"Item '{sourceItemId}' is not a BuildableItemDefinition",
+ nameof(sourceItemId)
+ );
+ }
+
+ return new BuildableItemDefinitionBuilder(buildableDef);
+ }
+
+ ///
+ /// Creates a new buildable item by cloning from an existing BuildableItemDefinition wrapper.
+ ///
+ /// The buildable item definition to clone from.
+ /// A builder initialized with the source item's properties, ready for customization.
+ ///
+ ///
+ /// var originalRack = ItemManager.GetDefinition("StorageRack-1x0.5") as BuildableItemDefinition;
+ /// var metalRack = BuildableItemCreator.CloneFrom(originalRack)
+ /// .WithBasicInfo("metal_rack_small", "Small Metal Storage Rack", "A metal version")
+ /// .WithBuildSound(BuildSoundType.Metal)
+ /// .Build();
+ ///
+ ///
+ public static BuildableItemDefinitionBuilder CloneFrom(BuildableItemDefinition source)
+ {
+ if (source == null)
+ {
+ throw new System.ArgumentNullException(nameof(source), "Source item definition cannot be null");
+ }
+
+ return new BuildableItemDefinitionBuilder(source.S1BuildableItemDefinition);
+ }
+ }
+}
diff --git a/S1API/Items/Buildable/BuildableItemDefinition.cs b/S1API/Items/Buildable/BuildableItemDefinition.cs
new file mode 100644
index 0000000..8565cd0
--- /dev/null
+++ b/S1API/Items/Buildable/BuildableItemDefinition.cs
@@ -0,0 +1,59 @@
+#if (IL2CPPMELON)
+using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
+#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+using S1ItemFramework = ScheduleOne.ItemFramework;
+#endif
+using System;
+
+namespace S1API.Items.Buildable
+{
+ ///
+ /// Represents a buildable item definition that can be placed in the game world.
+ /// Extends with building-specific properties.
+ ///
+ ///
+ /// Use to create new buildable items,
+ /// or to create variants of existing items.
+ ///
+ public sealed class BuildableItemDefinition : Storable.StorableItemDefinition
+ {
+ ///
+ /// INTERNAL: Wraps an existing native buildable item definition.
+ ///
+ internal BuildableItemDefinition(S1ItemFramework.BuildableItemDefinition definition)
+ : base(definition)
+ {
+ S1BuildableItemDefinition = definition;
+ }
+
+ ///
+ /// INTERNAL: A reference to the native game buildable item definition.
+ ///
+ internal S1ItemFramework.BuildableItemDefinition S1BuildableItemDefinition { get; }
+
+ ///
+ /// The sound type played when this item is built.
+ ///
+ public BuildSoundType BuildSoundType
+ {
+ get => (BuildSoundType)S1BuildableItemDefinition.BuildSoundType;
+ set => S1BuildableItemDefinition.BuildSoundType = (S1ItemFramework.BuildableItemDefinition.EBuildSoundType)value;
+ }
+
+ }
+
+ ///
+ /// Specifies the sound type played when a buildable item is placed.
+ ///
+ public enum BuildSoundType
+ {
+ /// Wood building sound.
+ Wood,
+ /// Metal building sound.
+ Metal,
+ /// Plastic building sound.
+ Plastic,
+ /// Cardboard building sound.
+ Cardboard
+ }
+}
diff --git a/S1API/Items/Buildable/BuildableItemDefinitionBuilder.cs b/S1API/Items/Buildable/BuildableItemDefinitionBuilder.cs
new file mode 100644
index 0000000..b01f562
--- /dev/null
+++ b/S1API/Items/Buildable/BuildableItemDefinitionBuilder.cs
@@ -0,0 +1,90 @@
+#if (IL2CPPMELON)
+using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
+using S1CoreItemFramework = Il2CppScheduleOne.Core.Items.Framework;
+#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+using S1ItemFramework = ScheduleOne.ItemFramework;
+using S1CoreItemFramework = ScheduleOne.Core.Items.Framework;
+#endif
+using S1API.Internal.Utils;
+using S1API.Items.Storable;
+using UnityEngine;
+
+namespace S1API.Items.Buildable
+{
+ ///
+ /// Builder for composing buildable item definitions at runtime.
+ /// Use fluent methods to configure buildable item properties before calling .
+ ///
+ public class BuildableItemDefinitionBuilder
+ : StorableItemDefinitionBuilderBase
+ {
+ private S1ItemFramework.BuildableItemDefinition BuildableDefinition =>
+ CrossType.As(Definition);
+
+ ///
+ /// INTERNAL: Creates a new builder instance with a fresh BuildableItemDefinition.
+ /// Only can instantiate this.
+ ///
+ internal BuildableItemDefinitionBuilder()
+ : base(ScriptableObject.CreateInstance)
+ {
+ Definition.Category = S1CoreItemFramework.EItemCategory.Furniture;
+ BuildableDefinition.BuildSoundType =
+ S1ItemFramework.BuildableItemDefinition.EBuildSoundType.Wood;
+ }
+
+ ///
+ /// INTERNAL: Creates a builder instance initialized by cloning an existing item.
+ ///
+ internal BuildableItemDefinitionBuilder(S1ItemFramework.BuildableItemDefinition source)
+ : base(source, ScriptableObject.CreateInstance)
+ {
+ }
+
+ ///
+ protected override void CopyPropertiesFrom(S1ItemFramework.StorableItemDefinition source)
+ {
+ base.CopyPropertiesFrom(source);
+ var buildableSource = CrossType.As(source);
+
+ BuildableDefinition.BuildSoundType = buildableSource.BuildSoundType;
+ BuildableDefinition.BuiltItem = buildableSource.BuiltItem;
+ }
+
+ ///
+ /// Sets the sound type played when this item is built.
+ ///
+ /// The build sound type.
+ /// The builder instance for fluent chaining.
+ public BuildableItemDefinitionBuilder WithBuildSound(BuildSoundType soundType)
+ {
+ BuildableDefinition.BuildSoundType = (S1ItemFramework.BuildableItemDefinition.EBuildSoundType)soundType;
+ return this;
+ }
+
+ ///
+ /// Builds the item definition, registers it with the game's registry, and returns a wrapper.
+ ///
+ /// A wrapper around the created buildable item definition.
+ public new BuildableItemDefinition Build()
+ {
+ return (BuildableItemDefinition)base.Build();
+ }
+
+ ///
+ /// INTERNAL: Builds and returns the raw game item definition without registering.
+ /// Used internally by S1API. Modders should use instead.
+ ///
+ internal new S1ItemFramework.BuildableItemDefinition BuildInternal()
+ {
+ return BuildableDefinition;
+ }
+
+ ///
+ protected override Storable.StorableItemDefinition CreateWrapper(
+ S1ItemFramework.StorableItemDefinition definition)
+ {
+ return new BuildableItemDefinition(BuildableDefinition);
+ }
+ }
+}
\ No newline at end of file
diff --git a/S1API/Items/BuildableItemCreator.cs b/S1API/Items/BuildableItemCreator.cs
index e8fe4bd..ed78532 100644
--- a/S1API/Items/BuildableItemCreator.cs
+++ b/S1API/Items/BuildableItemCreator.cs
@@ -6,6 +6,7 @@
using S1Registry = ScheduleOne.Registry;
#endif
+using System;
using S1API.Internal.Utils;
namespace S1API.Items
@@ -17,6 +18,7 @@ namespace S1API.Items
/// Use for creating items from scratch,
/// or for creating variants of existing buildable items.
///
+ [Obsolete("Use S1API.Items.Buildable.BuildableItemCreator instead")]
public static class BuildableItemCreator
{
///
diff --git a/S1API/Items/BuildableItemDefinition.cs b/S1API/Items/BuildableItemDefinition.cs
index 65b0ca6..b190ea9 100644
--- a/S1API/Items/BuildableItemDefinition.cs
+++ b/S1API/Items/BuildableItemDefinition.cs
@@ -4,6 +4,7 @@
using S1ItemFramework = ScheduleOne.ItemFramework;
#endif
+using System;
using UnityEngine;
namespace S1API.Items
@@ -16,6 +17,7 @@ namespace S1API.Items
/// Use to create new buildable items,
/// or to create variants of existing items.
///
+ [Obsolete("Use S1API.Items.Buildable.BuildableItemDefinition instead")]
public sealed class BuildableItemDefinition : StorableItemDefinition
{
///
diff --git a/S1API/Items/BuildableItemDefinitionBuilder.cs b/S1API/Items/BuildableItemDefinitionBuilder.cs
index 3c4d1a8..07eaa93 100644
--- a/S1API/Items/BuildableItemDefinitionBuilder.cs
+++ b/S1API/Items/BuildableItemDefinitionBuilder.cs
@@ -8,6 +8,7 @@
using S1Registry = ScheduleOne.Registry;
#endif
+using System;
using UnityEngine;
namespace S1API.Items
@@ -16,6 +17,7 @@ namespace S1API.Items
/// Builder for composing buildable item definitions at runtime.
/// Use fluent methods to configure buildable item properties before calling .
///
+ [Obsolete("Use S1API.Items.Buildable.BuildableItemDefinitionBuilder instead")]
public sealed class BuildableItemDefinitionBuilder
{
private readonly S1ItemFramework.BuildableItemDefinition _definition;
@@ -189,6 +191,9 @@ public BuildableItemDefinitionBuilder WithEquippable(Equippable equippable)
/// A wrapper around the created buildable item definition.
public BuildableItemDefinition Build()
{
+ if (string.IsNullOrWhiteSpace(_definition.ID))
+ throw new ArgumentException("Item ID cannot be null, empty, or whitespace.", nameof(_definition.ID));
+
// Register with the game's registry
S1Registry.Instance.AddToRegistry(_definition);
diff --git a/S1API/Items/Clothing/ClothingApplicationType.cs b/S1API/Items/Clothing/ClothingApplicationType.cs
new file mode 100644
index 0000000..3b6aaf0
--- /dev/null
+++ b/S1API/Items/Clothing/ClothingApplicationType.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace S1API.Items.Clothing
+{
+ ///
+ /// Represents how a clothing item is applied to the avatar.
+ /// Mirrors ScheduleOne.Clothing.EClothingApplicationType.
+ ///
+ public enum ClothingApplicationType
+ {
+ /// Applied as a body layer (flat texture on body mesh).
+ BodyLayer = 0,
+ /// Applied as a face layer (flat texture on face mesh).
+ FaceLayer = 1,
+ /// Applied as a 3D accessory (separate mesh).
+ Accessory = 2
+ }
+}
\ No newline at end of file
diff --git a/S1API/Items/Clothing/ClothingColor.cs b/S1API/Items/Clothing/ClothingColor.cs
new file mode 100644
index 0000000..8fedc80
--- /dev/null
+++ b/S1API/Items/Clothing/ClothingColor.cs
@@ -0,0 +1,39 @@
+using System;
+
+namespace S1API.Items.Clothing
+{
+ ///
+ /// Represents available clothing colors.
+ /// Mirrors ScheduleOne.Clothing.EClothingColor.
+ ///
+ public enum ClothingColor
+ {
+ White = 0,
+ LightGrey = 1,
+ DarkGrey = 2,
+ Charcoal = 3,
+ Black = 4,
+ LightRed = 5,
+ Red = 6,
+ Crimson = 7,
+ Orange = 8,
+ Tan = 9,
+ Brown = 10,
+ Coral = 11,
+ Beige = 12,
+ Yellow = 13,
+ Lime = 14,
+ LightGreen = 15,
+ DarkGreen = 16,
+ Cyan = 17,
+ SkyBlue = 18,
+ Blue = 19,
+ DeepBlue = 20,
+ Navy = 21,
+ DeepPurple = 22,
+ Purple = 23,
+ Magenta = 24,
+ BrightPink = 25,
+ HotPink = 26
+ }
+}
\ No newline at end of file
diff --git a/S1API/Items/Clothing/ClothingItemCreator.cs b/S1API/Items/Clothing/ClothingItemCreator.cs
new file mode 100644
index 0000000..44c6a81
--- /dev/null
+++ b/S1API/Items/Clothing/ClothingItemCreator.cs
@@ -0,0 +1,110 @@
+#if (IL2CPPMELON)
+using S1 = Il2CppScheduleOne;
+using S1Clothing = Il2CppScheduleOne.Clothing;
+#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+using S1 = ScheduleOne;
+using S1Clothing = ScheduleOne.Clothing;
+#endif
+using System;
+using S1API.Internal.Utils;
+using S1API.Logging;
+
+namespace S1API.Items.Clothing
+{
+ ///
+ /// Provides convenient static methods for creating custom clothing items.
+ /// Use for flexible configuration or for variants.
+ ///
+ public static class ClothingItemCreator
+ {
+ private static readonly Log Logger = new Log("ClothingItemCreator");
+
+ ///
+ /// Creates a new builder for composing a clothing item definition with full flexibility.
+ /// Use fluent methods to configure the item, then call Build() to register it.
+ ///
+ /// A new ClothingItemDefinitionBuilder instance for fluent configuration.
+ ///
+ ///
+ /// var hat = ClothingItemCreator.CreateBuilder()
+ /// .WithBasicInfo("my_hat", "Custom Hat", "A fancy custom hat")
+ /// .WithSlot(ClothingSlot.Head)
+ /// .WithApplicationType(ClothingApplicationType.Accessory)
+ /// .WithClothingAsset("MyMod/Accessories/CustomHat")
+ /// .WithDefaultColor(ClothingColor.Black)
+ /// .Build();
+ ///
+ ///
+ public static ClothingItemDefinitionBuilder CreateBuilder()
+ {
+ return new ClothingItemDefinitionBuilder();
+ }
+
+ ///
+ /// Creates a new clothing item by cloning an existing one by ID.
+ /// Returns a builder pre-configured with all properties of the source item.
+ /// You can then override specific properties before calling Build().
+ ///
+ /// The ID of the clothing item to clone.
+ /// A builder pre-configured with the source item's properties.
+ ///
+ ///
+ /// // Clone the base game cap and customize it
+ /// var customCap = ClothingItemCreator.CloneFrom("cap")
+ /// .WithBasicInfo("stay_silly_cap", "Stay Silly Cap", "A silly custom cap")
+ /// .WithClothingAsset("BigWillyMod/Accessories/StaySillyCap")
+ /// .WithColorable(false)
+ /// .Build();
+ ///
+ ///
+ public static ClothingItemDefinitionBuilder CloneFrom(string sourceItemId)
+ {
+ if (string.IsNullOrWhiteSpace(sourceItemId))
+ {
+ throw new ArgumentException("Source item ID cannot be null or whitespace", nameof(sourceItemId));
+ }
+
+ var sourceDefinition = S1.Registry.GetItem(sourceItemId);
+ if (sourceDefinition == null)
+ {
+ Logger.Error($"Cannot clone clothing item '{sourceItemId}': source item not found in registry");
+ return null;
+ }
+
+ // Use CrossType for proper IL2CPP/Mono type checking
+ if (!CrossType.Is(sourceDefinition, out S1Clothing.ClothingDefinition clothingDef))
+ {
+ Logger.Error($"Cannot clone item '{sourceItemId}': it is not a clothing item (found type: {sourceDefinition.GetType().Name})");
+ return null;
+ }
+
+ return new ClothingItemDefinitionBuilder(clothingDef);
+ }
+
+ ///
+ /// Creates a new clothing item by cloning an existing one.
+ /// Returns a builder pre-configured with all properties of the source item.
+ ///
+ /// The clothing item definition to clone.
+ /// A builder pre-configured with the source item's properties.
+ ///
+ ///
+ /// var existingCap = ItemManager.GetItemDefinition("cap") as ClothingItemDefinition;
+ /// var variant = ClothingItemCreator.CloneFrom(existingCap)
+ /// .WithBasicInfo("variant_cap", "Cap Variant", "A variant of the cap")
+ /// .Build();
+ ///
+ ///
+ public static ClothingItemDefinitionBuilder CloneFrom(ClothingItemDefinition source)
+ {
+ if (source == null)
+ {
+ Logger.Error("Cannot clone from null clothing item definition");
+ return null;
+ }
+
+ return new ClothingItemDefinitionBuilder(source.S1ClothingDefinition);
+ }
+ }
+}
+
diff --git a/S1API/Items/Clothing/ClothingItemDefinition.cs b/S1API/Items/Clothing/ClothingItemDefinition.cs
new file mode 100644
index 0000000..2a58ffc
--- /dev/null
+++ b/S1API/Items/Clothing/ClothingItemDefinition.cs
@@ -0,0 +1,144 @@
+#if (IL2CPPMELON)
+using S1Clothing = Il2CppScheduleOne.Clothing;
+using Il2CppCollections = Il2CppSystem.Collections.Generic;
+#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+using S1Clothing = ScheduleOne.Clothing;
+#endif
+using System;
+using System.Collections.Generic;
+
+namespace S1API.Items.Clothing
+{
+ ///
+ /// Represents a clothing item definition that can be worn by the player.
+ /// Extends with clothing-specific properties.
+ ///
+ ///
+ /// Use to create new clothing items,
+ /// or to create variants of existing items.
+ ///
+ public sealed class ClothingItemDefinition : Storable.StorableItemDefinition
+ {
+ ///
+ /// INTERNAL: Wraps an existing native clothing item definition.
+ ///
+ internal ClothingItemDefinition(S1Clothing.ClothingDefinition definition)
+ : base(definition)
+ {
+ S1ClothingDefinition = definition;
+ }
+
+ ///
+ /// INTERNAL: A reference to the native game clothing item definition.
+ ///
+ internal S1Clothing.ClothingDefinition S1ClothingDefinition { get; }
+
+ ///
+ /// 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,
+ quantity,
+ (S1Clothing.EClothingColor)color));
+
+ ///
+ /// The clothing slot this item occupies.
+ ///
+ public ClothingSlot Slot
+ {
+ get => (ClothingSlot)S1ClothingDefinition.Slot;
+ set => S1ClothingDefinition.Slot = (S1Clothing.EClothingSlot)value;
+ }
+
+ ///
+ /// How this clothing item is applied to the avatar.
+ ///
+ public ClothingApplicationType ApplicationType
+ {
+ get => (ClothingApplicationType)S1ClothingDefinition.ApplicationType;
+ set => S1ClothingDefinition.ApplicationType = (S1Clothing.EClothingApplicationType)value;
+ }
+
+ ///
+ /// The asset path to the clothing prefab or layer in Resources.
+ ///
+ public string ClothingAssetPath
+ {
+ get => S1ClothingDefinition.ClothingAssetPath;
+ set => S1ClothingDefinition.ClothingAssetPath = value;
+ }
+
+ ///
+ /// Whether this clothing item can be colored by the player.
+ ///
+ public bool Colorable
+ {
+ get => S1ClothingDefinition.Colorable;
+ set => S1ClothingDefinition.Colorable = value;
+ }
+
+ ///
+ /// The default color for this clothing item.
+ ///
+ public ClothingColor DefaultColor
+ {
+ get => (ClothingColor)S1ClothingDefinition.DefaultColor;
+ set => S1ClothingDefinition.DefaultColor = (S1Clothing.EClothingColor)value;
+ }
+
+ ///
+ /// List of clothing slots this item blocks when equipped.
+ ///
+ public List SlotsToBlock
+ {
+ get
+ {
+ var result = new List();
+ if (S1ClothingDefinition.SlotsToBlock != null)
+ {
+ foreach (var slot in S1ClothingDefinition.SlotsToBlock)
+ {
+ result.Add((ClothingSlot)slot);
+ }
+ }
+ return result;
+ }
+ set
+ {
+#if (IL2CPPMELON)
+ S1ClothingDefinition.SlotsToBlock = new Il2CppCollections.List();
+#else
+ S1ClothingDefinition.SlotsToBlock = new List();
+#endif
+ if (value != null)
+ {
+ foreach (var slot in value)
+ {
+ S1ClothingDefinition.SlotsToBlock.Add((S1Clothing.EClothingSlot)slot);
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/S1API/Items/Clothing/ClothingItemDefinitionBuilder.cs b/S1API/Items/Clothing/ClothingItemDefinitionBuilder.cs
new file mode 100644
index 0000000..fde0f8a
--- /dev/null
+++ b/S1API/Items/Clothing/ClothingItemDefinitionBuilder.cs
@@ -0,0 +1,274 @@
+#if (IL2CPPMELON)
+using S1Clothing = Il2CppScheduleOne.Clothing;
+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;
+#endif
+using System.Collections.Generic;
+using S1API.Internal.Utils;
+using S1API.Items.Storable;
+using S1API.Logging;
+using UnityEngine;
+
+namespace S1API.Items.Clothing
+{
+ ///
+ /// Builder for composing clothing item definitions at runtime.
+ /// Use fluent methods to configure clothing properties before calling .
+ ///
+ public class ClothingItemDefinitionBuilder
+ : StorableItemDefinitionBuilderBase
+ {
+ private S1Clothing.ClothingDefinition ClothingDefinition =>
+ CrossType.As(Definition);
+
+ 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;
+
+ ///
+ /// INTERNAL: Creates a new builder instance with a fresh ClothingDefinition.
+ ///
+ internal ClothingItemDefinitionBuilder()
+ : base(ScriptableObject.CreateInstance())
+ {
+ Definition.Category = S1CoreItemFramework.EItemCategory.Clothing;
+
+ // Clothing-specific defaults
+ ClothingDefinition.Slot = S1Clothing.EClothingSlot.Head;
+ ClothingDefinition.ApplicationType = S1Clothing.EClothingApplicationType.Accessory;
+ ClothingDefinition.ClothingAssetPath = "Path/To/Clothing/Asset";
+ ClothingDefinition.Colorable = true;
+ ClothingDefinition.DefaultColor = S1Clothing.EClothingColor.White;
+#if (IL2CPPMELON)
+ ClothingDefinition.SlotsToBlock = new Il2CppCollections.List();
+#else
+ ClothingDefinition.SlotsToBlock = new List();
+#endif
+ }
+
+ ///
+ /// INTERNAL: Creates a builder from an existing clothing definition (for cloning).
+ ///
+ internal ClothingItemDefinitionBuilder(S1Clothing.ClothingDefinition source)
+ : base(source, ScriptableObject.CreateInstance)
+ {
+ }
+
+ ///
+ protected override void CopyPropertiesFrom(
+ S1ItemFramework.StorableItemDefinition source)
+ {
+ base.CopyPropertiesFrom(source);
+
+ var clothingSource = CrossType.As(source);
+
+ ClothingDefinition.Slot = clothingSource.Slot;
+ ClothingDefinition.ApplicationType = clothingSource.ApplicationType;
+ ClothingDefinition.ClothingAssetPath = clothingSource.ClothingAssetPath;
+ ClothingDefinition.Colorable = clothingSource.Colorable;
+ ClothingDefinition.DefaultColor = clothingSource.DefaultColor;
+#if (IL2CPPMELON)
+ ClothingDefinition.SlotsToBlock = new Il2CppCollections.List();
+ if (clothingSource.SlotsToBlock != null)
+ {
+ foreach (var slot in clothingSource.SlotsToBlock)
+ {
+ ClothingDefinition.SlotsToBlock.Add(slot);
+ }
+ }
+#else
+ ClothingDefinition.SlotsToBlock = clothingSource.SlotsToBlock == null
+ ? new List()
+ : new List(clothingSource.SlotsToBlock);
+#endif
+ }
+
+ ///
+ /// Sets the clothing slot this item occupies.
+ ///
+ /// The clothing slot.
+ /// The builder instance for fluent chaining.
+ public ClothingItemDefinitionBuilder WithSlot(Clothing.ClothingSlot slot)
+ {
+ ClothingDefinition.Slot = (S1Clothing.EClothingSlot)slot;
+ return this;
+ }
+
+ ///
+ /// Sets how this clothing item is applied to the avatar.
+ ///
+ /// The application type.
+ /// The builder instance for fluent chaining.
+ public ClothingItemDefinitionBuilder WithApplicationType(Clothing.ClothingApplicationType applicationType)
+ {
+ ClothingDefinition.ApplicationType = (S1Clothing.EClothingApplicationType)applicationType;
+ return this;
+ }
+
+ ///
+ /// Sets the asset path to the clothing prefab or layer.
+ ///
+ /// Resources path to the clothing asset.
+ /// The builder instance for fluent chaining.
+ public ClothingItemDefinitionBuilder WithClothingAsset(string assetPath)
+ {
+ ClothingDefinition.ClothingAssetPath = assetPath;
+ return this;
+ }
+
+ ///
+ /// Sets whether this clothing item can be colored.
+ ///
+ /// True if colorable, false otherwise.
+ /// The builder instance for fluent chaining.
+ public ClothingItemDefinitionBuilder WithColorable(bool colorable)
+ {
+ ClothingDefinition.Colorable = colorable;
+ return this;
+ }
+
+ ///
+ /// Sets the default color for this clothing item.
+ ///
+ /// The default color.
+ /// The builder instance for fluent chaining.
+ public ClothingItemDefinitionBuilder WithDefaultColor(Clothing.ClothingColor color)
+ {
+ ClothingDefinition.DefaultColor = (S1Clothing.EClothingColor)color;
+ return this;
+ }
+
+ ///
+ /// Sets the list of clothing slots this item blocks when equipped.
+ ///
+ /// Array of slots to block.
+ /// The builder instance for fluent chaining.
+ public ClothingItemDefinitionBuilder WithBlockedSlots(params Clothing.ClothingColor[] slots)
+ {
+#if (IL2CPPMELON)
+ ClothingDefinition.SlotsToBlock = new Il2CppCollections.List();
+ foreach (var slot in slots)
+ {
+ ClothingDefinition.SlotsToBlock.Add((S1Clothing.EClothingSlot)slot);
+ }
+#else
+ ClothingDefinition.SlotsToBlock = new List();
+ foreach (var slot in slots)
+ {
+ ClothingDefinition.SlotsToBlock.Add((S1Clothing.EClothingSlot)slot);
+ }
+#endif
+ return this;
+ }
+
+ ///
+ /// Builds the item definition, registers it with the game's registry, and returns a wrapper.
+ ///
+ /// A wrapper around the created clothing item definition.
+ public new ClothingItemDefinition Build()
+ {
+ EnsureNativeClothingItemUi();
+ return (ClothingItemDefinition)base.Build();
+ }
+
+ ///
+ /// INTERNAL: Builds and returns the raw game item definition without registering.
+ /// Used internally by S1API. Modders should use instead.
+ ///
+ internal new S1Clothing.ClothingDefinition BuildInternal()
+ {
+ EnsureNativeClothingItemUi();
+ return ClothingDefinition;
+ }
+
+ ///
+ protected override Storable.StorableItemDefinition CreateWrapper(
+ S1ItemFramework.StorableItemDefinition definition)
+ {
+ return new ClothingItemDefinition(ClothingDefinition);
+ }
+
+
+ private void EnsureNativeClothingItemUi()
+ {
+ if (ClothingDefinition.CustomItemUI != null)
+ {
+ return;
+ }
+
+ if (s_cachedNativeCustomItemUI != null)
+ {
+ ClothingDefinition.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;
+ ClothingDefinition.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.");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/S1API/Items/Clothing/ClothingItemInstance.cs b/S1API/Items/Clothing/ClothingItemInstance.cs
new file mode 100644
index 0000000..55ca5e5
--- /dev/null
+++ b/S1API/Items/Clothing/ClothingItemInstance.cs
@@ -0,0 +1,48 @@
+#if (IL2CPPMELON)
+using S1Clothing = Il2CppScheduleOne.Clothing;
+#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+using S1Clothing = ScheduleOne.Clothing;
+#endif
+using System;
+using S1API.Internal.Utils;
+
+namespace S1API.Items.Clothing
+{
+ ///
+ /// Represents a clothing item instance in the game world (physical clothing you own).
+ /// Extends with color information.
+ ///
+ public class ClothingItemInstance : ItemInstance
+ {
+ ///
+ /// INTERNAL: Reference to the in-game clothing item instance.
+ ///
+ internal readonly S1Clothing.ClothingInstance S1ClothingInstance;
+
+ ///
+ /// INTERNAL: Creates a ClothingItemInstance wrapper.
+ ///
+ /// In-game clothing item instance
+ internal ClothingItemInstance(S1Clothing.ClothingInstance itemInstance)
+ : base(itemInstance)
+ {
+ S1ClothingInstance = itemInstance;
+ }
+
+ ///
+ /// The color of this clothing instance.
+ ///
+ public ClothingColor Color
+ {
+ get => (ClothingColor)S1ClothingInstance.Color;
+ set => S1ClothingInstance.Color = (S1Clothing.EClothingColor)value;
+ }
+
+ ///
+ /// The clothing definition (template) this instance was created from.
+ ///
+ public new ClothingItemDefinition Definition =>
+ new ClothingItemDefinition(
+ CrossType.As(S1ClothingInstance.Definition));
+ }
+}
\ No newline at end of file
diff --git a/S1API/Items/Clothing/ClothingSlot.cs b/S1API/Items/Clothing/ClothingSlot.cs
new file mode 100644
index 0000000..e8dcaf4
--- /dev/null
+++ b/S1API/Items/Clothing/ClothingSlot.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace S1API.Items.Clothing
+{
+ ///
+ /// Represents the slot where a clothing item can be equipped.
+ /// Mirrors ScheduleOne.Clothing.EClothingSlot.
+ ///
+ public enum ClothingSlot
+ {
+ /// Feet slot (shoes, boots).
+ Feet = 0,
+ /// Bottom slot (pants, shorts).
+ Bottom = 1,
+ /// Waist slot (belts).
+ Waist = 2,
+ /// Top slot (shirts).
+ Top = 3,
+ /// Outerwear slot (jackets, coats).
+ Outerwear = 4,
+ /// Hands slot (gloves).
+ Hands = 5,
+ /// Neck slot (necklaces, scarves).
+ Neck = 6,
+ /// Eyes slot (glasses, sunglasses).
+ Eyes = 7,
+ /// Head slot (hats, caps, helmets).
+ Head = 8,
+ /// Wrist slot (watches, bracelets).
+ Wrist = 9
+ }
+}
\ No newline at end of file
diff --git a/S1API/Items/ClothingApplicationType.cs b/S1API/Items/ClothingApplicationType.cs
index 44d4b01..e1859ef 100644
--- a/S1API/Items/ClothingApplicationType.cs
+++ b/S1API/Items/ClothingApplicationType.cs
@@ -1,9 +1,12 @@
+using System;
+
namespace S1API.Items
{
///
/// Represents how a clothing item is applied to the avatar.
/// Mirrors ScheduleOne.Clothing.EClothingApplicationType.
///
+ [Obsolete("Use S1API.Items.Clothing.ClothingApplicationType instead")]
public enum ClothingApplicationType
{
/// Applied as a body layer (flat texture on body mesh).
diff --git a/S1API/Items/ClothingColor.cs b/S1API/Items/ClothingColor.cs
index d57b8e3..7e24164 100644
--- a/S1API/Items/ClothingColor.cs
+++ b/S1API/Items/ClothingColor.cs
@@ -1,9 +1,12 @@
+using System;
+
namespace S1API.Items
{
///
/// Represents available clothing colors.
/// Mirrors ScheduleOne.Clothing.EClothingColor.
///
+ [Obsolete("Use S1API.Items.Clothing.ClothingColor instead")]
public enum ClothingColor
{
White = 0,
diff --git a/S1API/Items/ClothingItemCreator.cs b/S1API/Items/ClothingItemCreator.cs
index 320a399..c92f1e8 100644
--- a/S1API/Items/ClothingItemCreator.cs
+++ b/S1API/Items/ClothingItemCreator.cs
@@ -6,6 +6,7 @@
using S1Clothing = ScheduleOne.Clothing;
#endif
+using System;
using S1API.Internal.Utils;
using S1API.Logging;
using UnityEngine;
@@ -16,6 +17,7 @@ namespace S1API.Items
/// Provides convenient static methods for creating custom clothing items.
/// Use for flexible configuration or for variants.
///
+ [Obsolete("Use S1API.Items.Clothing.ClothingItemCreator instead")]
public static class ClothingItemCreator
{
private static readonly Log Logger = new Log("ClothingItemCreator");
diff --git a/S1API/Items/ClothingItemDefinition.cs b/S1API/Items/ClothingItemDefinition.cs
index b6b25d5..ea100ec 100644
--- a/S1API/Items/ClothingItemDefinition.cs
+++ b/S1API/Items/ClothingItemDefinition.cs
@@ -6,6 +6,7 @@
using Il2CppCollections = System.Collections.Generic;
#endif
+using System;
using System.Collections.Generic;
namespace S1API.Items
@@ -18,6 +19,7 @@ namespace S1API.Items
/// Use to create new clothing items,
/// or to create variants of existing items.
///
+ [Obsolete("Use S1API.Items.Clothing.ClothingItemDefinition instead")]
public sealed class ClothingItemDefinition : StorableItemDefinition
{
///
diff --git a/S1API/Items/ClothingItemDefinitionBuilder.cs b/S1API/Items/ClothingItemDefinitionBuilder.cs
index 506bad1..4ccf82a 100644
--- a/S1API/Items/ClothingItemDefinitionBuilder.cs
+++ b/S1API/Items/ClothingItemDefinitionBuilder.cs
@@ -14,6 +14,7 @@
using Il2CppCollections = System.Collections.Generic;
#endif
+using System;
using System.Collections.Generic;
using S1API.Internal.Utils;
using S1API.Logging;
@@ -25,6 +26,7 @@ namespace S1API.Items
/// Builder for composing clothing item definitions at runtime.
/// Use fluent methods to configure clothing properties before calling .
///
+ [Obsolete("Use S1API.Items.Clothing.ClothingItemDefinitionBuilder instead")]
public sealed class ClothingItemDefinitionBuilder
{
private static readonly Log Logger = new Log("ClothingItemDefinitionBuilder");
@@ -237,6 +239,9 @@ public ClothingItemDefinitionBuilder WithPricing(float basePurchasePrice, float
/// A wrapper around the created clothing item definition.
public ClothingItemDefinition Build()
{
+ if (string.IsNullOrWhiteSpace(_definition.ID))
+ throw new ArgumentException("Item ID cannot be null, empty, or whitespace.", nameof(_definition.ID));
+
EnsureNativeClothingItemUi();
// Register with the game's registry
diff --git a/S1API/Items/ClothingItemInstance.cs b/S1API/Items/ClothingItemInstance.cs
index 71567be..4cb6c26 100644
--- a/S1API/Items/ClothingItemInstance.cs
+++ b/S1API/Items/ClothingItemInstance.cs
@@ -3,6 +3,7 @@
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using S1Clothing = ScheduleOne.Clothing;
#endif
+using System;
using S1API.Internal.Utils;
namespace S1API.Items
@@ -11,6 +12,7 @@ namespace S1API.Items
/// Represents a clothing item instance in the game world (physical clothing you own).
/// Extends with color information.
///
+ [Obsolete("Use S1API.Items.Clothing.ClothingItemInstance instead")]
public class ClothingItemInstance : ItemInstance
{
///
diff --git a/S1API/Items/ClothingSlot.cs b/S1API/Items/ClothingSlot.cs
index 4fdb3e9..9ef7360 100644
--- a/S1API/Items/ClothingSlot.cs
+++ b/S1API/Items/ClothingSlot.cs
@@ -1,9 +1,12 @@
+using System;
+
namespace S1API.Items
{
///
/// Represents the slot where a clothing item can be equipped.
/// Mirrors ScheduleOne.Clothing.EClothingSlot.
///
+ [Obsolete("Use S1API.Items.Clothing.ClothingSlot instead")]
public enum ClothingSlot
{
/// Feet slot (shoes, boots).
diff --git a/S1API/Items/ItemCreator.cs b/S1API/Items/ItemCreator.cs
index b447be6..2991f12 100644
--- a/S1API/Items/ItemCreator.cs
+++ b/S1API/Items/ItemCreator.cs
@@ -19,6 +19,7 @@ namespace S1API.Items
///
/// All items in Schedule One are storable items (StorableItemDefinition), so both methods create the same type.
///
+ [Obsolete("Use S1API.Items.Storable.ItemCreator instead")]
public static class ItemCreator
{
///
@@ -90,8 +91,7 @@ public static StorableItemDefinitionBuilder CloneFrom(StorableItemDefinition sou
/// Base price when buying from shops (default: 10).
/// Fraction of purchase price recovered when selling (default: 0.5).
/// Whether the item is legal or illegal (default: Legal).
- /// Whether purchasing the item requires a certain player rank (default: false).
- /// The player rank required to purchase the item, if applicable (default: null).
+ /// The player rank required to purchase the item, null if no rank required (default: null).
/// Optional sprite to use as the item icon.
/// Optional equippable component to attach.
/// A wrapper around the created item definition.
@@ -116,7 +116,6 @@ public static StorableItemDefinition CreateItem(
float basePurchasePrice = 10f,
float resellMultiplier = 0.5f,
LegalStatus legalStatus = LegalStatus.Legal,
- bool requiresLevelToPurchase = false,
FullRank? requiredRank = null,
Sprite icon = null,
Equippable equippable = null)
diff --git a/S1API/Items/ItemManager.cs b/S1API/Items/ItemManager.cs
index d28260b..cc6ab96 100644
--- a/S1API/Items/ItemManager.cs
+++ b/S1API/Items/ItemManager.cs
@@ -31,6 +31,7 @@ public static class ItemManager
///
/// The ID of the item.
/// An instance of the item definition.
+ [Obsolete("Use S1API.Items.ItemManager.GetDefinition instead.")]
public static ItemDefinition GetItemDefinition(string itemID)
{
S1ItemFramework.ItemDefinition itemDefinition = S1Registry.GetItem(itemID);
@@ -74,6 +75,54 @@ public static ItemDefinition GetItemDefinition(string itemID)
return new ItemDefinition(itemDefinition);
}
+ ///
+ /// Gets the definition of an item by its ID.
+ ///
+ /// The ID of the item.
+ /// An instance of the item definition.
+ public static ItemDefinition? GetDefinition(string itemID)
+ {
+ var itemDefinition = S1Registry.GetItem(itemID);
+
+ if (itemDefinition == null)
+ return null;
+
+ // Check for specific types first (most derived to least derived)
+ if (CrossType.Is(itemDefinition,
+ out S1Product.ProductDefinition productDefinition))
+ return new ProductDefinition(productDefinition);
+
+ if (CrossType.Is(itemDefinition,
+ out S1ItemFramework.CashDefinition cashDefinition))
+ return new CashDefinition(cashDefinition);
+
+ if (CrossType.Is(itemDefinition,
+ out S1Clothing.ClothingDefinition clothingDefinition))
+ return new Clothing.ClothingItemDefinition(clothingDefinition);
+
+ if (CrossType.Is(itemDefinition,
+ out S1ItemFramework.BuildableItemDefinition buildableItemDefinition))
+ return new Buildable.BuildableItemDefinition(buildableItemDefinition);
+
+ if (CrossType.Is(itemDefinition,
+ out S1Packaging.PackagingDefinition packagingDefinition))
+ return new PackagingDefinition(packagingDefinition);
+
+ if (CrossType.Is(itemDefinition,
+ out S1ItemFramework.AdditiveDefinition additiveDefinition))
+ return new Additive.AdditiveDefinition(additiveDefinition);
+
+ if (CrossType.Is(itemDefinition,
+ out S1ItemFramework.QualityItemDefinition qualityItemDefinition))
+ return new Quality.QualityItemDefinition(qualityItemDefinition);
+
+ if (CrossType.Is(itemDefinition,
+ out S1ItemFramework.StorableItemDefinition storableItemDefinition))
+ return new Storable.StorableItemDefinition(storableItemDefinition);
+
+ return new ItemDefinition(itemDefinition);
+ }
+
///
/// Manually registers an item definition with the game's registry.
/// This is typically handled automatically by methods,
@@ -159,7 +208,7 @@ public static bool UnregisterItem(string itemID)
return false;
}
- ItemDefinition definition = GetItemDefinition(itemID);
+ var definition = GetDefinition(itemID);
if (definition == null)
{
return false;
@@ -167,7 +216,7 @@ public static bool UnregisterItem(string itemID)
RemoveFromRuntimeCleanupQueue(definition.S1ItemDefinition, definition.ID);
S1Registry.Instance.RemoveFromRegistry(definition.S1ItemDefinition);
- return GetItemDefinition(itemID) == null;
+ return GetDefinition(itemID) == null;
}
///
@@ -206,7 +255,7 @@ public static System.Collections.Generic.List GetAllItemDefiniti
continue;
// Use GetItemDefinition to properly wrap the item with the correct type
- var wrappedItem = GetItemDefinition(itemId);
+ var wrappedItem = GetDefinition(itemId);
if (wrappedItem != null)
{
wrappedItems.Add(wrappedItem);
diff --git a/S1API/Items/Quality/QualityItemCreator.cs b/S1API/Items/Quality/QualityItemCreator.cs
new file mode 100644
index 0000000..73fe4d3
--- /dev/null
+++ b/S1API/Items/Quality/QualityItemCreator.cs
@@ -0,0 +1,72 @@
+#if (IL2CPPMELON)
+using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
+using S1Registry = Il2CppScheduleOne.Registry;
+#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+using S1ItemFramework = ScheduleOne.ItemFramework;
+using S1Registry = ScheduleOne.Registry;
+#endif
+using System;
+using S1API.Internal.Utils;
+
+namespace S1API.Items.Quality
+{
+ ///
+ /// Provides convenient static methods for creating custom quality items.
+ /// Use for flexible configuration
+ /// or for quick variants based on existing items.
+ ///
+ public class QualityItemCreator
+ {
+ ///
+ /// Creates a new builder for composing a quality item definition with full flexibility.
+ /// Use fluent methods to configure the definition, then call Build() to register it.
+ ///
+ public static QualityItemDefinitionBuilder CreateBuilder()
+ {
+ return new QualityItemDefinitionBuilder();
+ }
+
+ ///
+ /// Creates a new quality item builder by cloning an existing quality item by ID.
+ ///
+ /// The ID of the item to clone.
+ /// A builder pre-configured with the source item properties.
+ /// Thrown if the source item ID is not found or is not a quality item.
+ public static QualityItemDefinitionBuilder CloneFrom(string sourceItemId)
+ {
+ if (string.IsNullOrWhiteSpace(sourceItemId))
+ {
+ throw new ArgumentException("Source item ID cannot be null or whitespace", nameof(sourceItemId));
+ }
+
+ var sourceDefinition = S1Registry.GetItem(sourceItemId);
+ if (sourceDefinition == null)
+ {
+ throw new ArgumentException($"Source item with ID '{sourceItemId}' not found in registry", nameof(sourceItemId));
+ }
+
+ if (!CrossType.Is(sourceDefinition, out S1ItemFramework.QualityItemDefinition qualityDef))
+ {
+ throw new ArgumentException($"Item '{sourceItemId}' is not an QualityItemDefinition", nameof(sourceItemId));
+ }
+
+ return new QualityItemDefinitionBuilder(qualityDef);
+ }
+
+ ///
+ /// Creates a new quality item builder by cloning an existing quality item wrapper.
+ ///
+ /// The quality item definition to clone.
+ /// A builder pre-configured with the source item properties.
+ /// Thrown if the source definition is null.
+ public static QualityItemDefinitionBuilder CloneFrom(QualityItemDefinition source)
+ {
+ if (source == null)
+ {
+ throw new ArgumentNullException(nameof(source), "Source storable item definition cannot be null");
+ }
+
+ return new QualityItemDefinitionBuilder(source.S1QualityDefinition);
+ }
+ }
+}
\ No newline at end of file
diff --git a/S1API/Items/Quality/QualityItemDefinition.cs b/S1API/Items/Quality/QualityItemDefinition.cs
new file mode 100644
index 0000000..3ae4aaa
--- /dev/null
+++ b/S1API/Items/Quality/QualityItemDefinition.cs
@@ -0,0 +1,66 @@
+#if (IL2CPPMELON)
+using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
+#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+using S1ItemFramework = ScheduleOne.ItemFramework;
+#endif
+
+namespace S1API.Items.Quality
+{
+ ///
+ /// Represents a quality item definition that can be consumed or used in recipes
+ /// Extends with quality-specific properties.
+ ///
+ ///
+ /// Use
+ ///
+ public class QualityItemDefinition : Storable.StorableItemDefinition
+ {
+ ///
+ /// INTERNAL: Wraps an existing native quality item definition.
+ ///
+ internal QualityItemDefinition(S1ItemFramework.QualityItemDefinition definition) : base(definition)
+ {
+ S1QualityDefinition = definition;
+ }
+
+ ///
+ /// INTERNAL: The underlying S1 quality item definition instance.
+ ///
+ internal S1ItemFramework.QualityItemDefinition S1QualityDefinition { get; }
+
+ ///
+ /// Creates a quality item instance from this definition using the default quality.
+ ///
+ /// The quantity to apply to the created instance.
+ /// A quality item instance using this definition's default quality.
+ public override ItemInstance CreateInstance(int quantity = 1) => CreateInstance(quantity, DefaultQuality);
+
+ ///
+ /// Creates a quality item instance from this definition with the specified quality.
+ ///
+ /// The quality to apply to the created instance.
+ /// A quality item instance using the specified quality.
+ public QualityItemInstance CreateInstance(Products.Quality quality) => CreateInstance(1, quality);
+
+ ///
+ /// Creates a quality item instance from this definition with the specified quantity and quality.
+ ///
+ /// The quantity to apply to the created instance.
+ /// The quality to apply to the created instance.
+ /// A quality item instance using the specified quantity and quality.
+ public QualityItemInstance CreateInstance(int quantity, Products.Quality quality) =>
+ new QualityItemInstance(new S1ItemFramework.QualityItemInstance(
+ S1QualityDefinition,
+ quantity,
+ (S1ItemFramework.EQuality)quality));
+
+ ///
+ /// The default quality for this item.
+ ///
+ public Products.Quality DefaultQuality
+ {
+ get => (Products.Quality)S1QualityDefinition.DefaultQuality;
+ set => S1QualityDefinition.DefaultQuality = (S1ItemFramework.EQuality)value;
+ }
+ }
+}
\ No newline at end of file
diff --git a/S1API/Items/Quality/QualityItemDefinitionBuilder.cs b/S1API/Items/Quality/QualityItemDefinitionBuilder.cs
new file mode 100644
index 0000000..4fdafaf
--- /dev/null
+++ b/S1API/Items/Quality/QualityItemDefinitionBuilder.cs
@@ -0,0 +1,93 @@
+#if (IL2CPPMELON)
+using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
+#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+using S1ItemFramework = ScheduleOne.ItemFramework;
+#endif
+using S1API.Internal.Utils;
+using S1API.Items.Storable;
+using UnityEngine;
+
+namespace S1API.Items.Quality
+{
+ ///
+ /// Builder for composing quality item definitions at runtime.
+ /// Use fluent methods to configure item properties before calling
+ ///
+ public sealed class QualityItemDefinitionBuilder
+ : StorableItemDefinitionBuilderBase
+ {
+ private S1ItemFramework.QualityItemDefinition QualityDefinition =>
+ CrossType.As(Definition);
+
+ ///
+ /// INTERNAL: Creates a new builder instance with a fresh QualityItemDefinition.
+ /// Only can instantiate this.
+ ///
+ internal QualityItemDefinitionBuilder()
+ : base(ScriptableObject.CreateInstance)
+ {
+ QualityDefinition.DefaultQuality =
+ S1ItemFramework.EQuality.Standard;
+ }
+
+ ///
+ /// INTERNAL: Creates a builder instance initialized by cloning an existing quality item definition.
+ /// Only can instantiate this.
+ ///
+ /// The existing quality item definition to clone properties from.
+ internal QualityItemDefinitionBuilder(
+ S1ItemFramework.QualityItemDefinition source)
+ : base(
+ source,
+ ScriptableObject.CreateInstance)
+ {
+ }
+
+ ///
+ protected override void CopyPropertiesFrom(
+ S1ItemFramework.StorableItemDefinition source)
+ {
+ base.CopyPropertiesFrom(source);
+
+ var qualitySource = CrossType.As(source);
+
+ QualityDefinition.DefaultQuality = qualitySource.DefaultQuality;
+ }
+
+ ///
+ /// Assigns a default quality for this definition.
+ ///
+ /// The default quality to assign to items of this definition.
+ /// >The builder instance for fluent chaining.
+ public QualityItemDefinitionBuilder WithDefaultQuality(Products.Quality quality)
+ {
+ QualityDefinition.DefaultQuality = (S1ItemFramework.EQuality)quality;
+ return this;
+ }
+
+ ///
+ /// Builds the item definition, registers it with the game's registry, and returns a wrapper.
+ ///
+ /// A wrapper around the created quality item definition.
+ public new QualityItemDefinition Build()
+ {
+ return (QualityItemDefinition)base.Build();
+ }
+
+ ///
+ /// INTERNAL: Builds and returns the raw game item definition without registering.
+ /// Used internally by S1API. Modders should use instead.
+ ///
+ internal new S1ItemFramework.QualityItemDefinition BuildInternal()
+ {
+ return QualityDefinition;
+ }
+
+ ///
+ protected override Storable.StorableItemDefinition CreateWrapper(
+ S1ItemFramework.StorableItemDefinition definition)
+ {
+ return new QualityItemDefinition(QualityDefinition);
+ }
+ }
+}
\ No newline at end of file
diff --git a/S1API/Items/Quality/QualityItemInstance.cs b/S1API/Items/Quality/QualityItemInstance.cs
new file mode 100644
index 0000000..60b3afc
--- /dev/null
+++ b/S1API/Items/Quality/QualityItemInstance.cs
@@ -0,0 +1,46 @@
+#if (IL2CPPMELON)
+using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
+#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+using S1ItemFramework = ScheduleOne.ItemFramework;
+#endif
+using S1API.Internal.Utils;
+
+namespace S1API.Items.Quality
+{
+ ///
+ /// Represents a quality item instance in the game world (usable item).
+ /// Extends with quality information.
+ ///
+ public class QualityItemInstance : ItemInstance
+ {
+ ///
+ /// INTERNAL: Reference to the in-game quality item instance.
+ ///
+ internal readonly S1ItemFramework.QualityItemInstance S1QualityInstance;
+
+ ///
+ /// INTERNAL: Creates a QualityItemInstance wrapper.
+ ///
+ /// In-game quality item instance
+ internal QualityItemInstance(S1ItemFramework.QualityItemInstance itemInstance) : base(itemInstance)
+ {
+ S1QualityInstance = itemInstance;
+ }
+
+ ///
+ /// The quality of this item.
+ ///
+ public Products.Quality Quality
+ {
+ get => (Products.Quality)S1QualityInstance.Quality;
+ set => S1QualityInstance.Quality = (S1ItemFramework.EQuality)value;
+ }
+
+ ///
+ /// The quality item definition (template) this instance was created from.
+ ///
+ public new QualityItemDefinition Definition =>
+ new QualityItemDefinition(
+ CrossType.As(S1QualityInstance.Definition));
+ }
+}
\ No newline at end of file
diff --git a/S1API/Items/QualityItemCreator.cs b/S1API/Items/QualityItemCreator.cs
index 3255b15..67ef874 100644
--- a/S1API/Items/QualityItemCreator.cs
+++ b/S1API/Items/QualityItemCreator.cs
@@ -15,6 +15,7 @@ namespace S1API.Items
/// Use for flexible configuration
/// or for quick variants based on existing items.
///
+ [Obsolete("Use S1API.Items.Quality.QualityItemCreator instead")]
public class QualityItemCreator
{
///
diff --git a/S1API/Items/QualityItemDefinition.cs b/S1API/Items/QualityItemDefinition.cs
index 576474c..68ad6f3 100644
--- a/S1API/Items/QualityItemDefinition.cs
+++ b/S1API/Items/QualityItemDefinition.cs
@@ -3,6 +3,7 @@
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using S1ItemFramework = ScheduleOne.ItemFramework;
#endif
+using System;
using S1API.Products;
namespace S1API.Items
@@ -14,6 +15,7 @@ namespace S1API.Items
///
/// Use
///
+ [Obsolete("Use S1API.Items.Quality.QualityItemDefinition instead")]
public class QualityItemDefinition : StorableItemDefinition
{
///
@@ -41,7 +43,7 @@ internal QualityItemDefinition(S1ItemFramework.QualityItemDefinition definition)
///
/// The quality to apply to the created instance.
/// A quality item instance using the specified quality.
- public QualityItemInstance CreateInstance(Quality quality) => CreateInstance(1, quality);
+ public QualityItemInstance CreateInstance(Products.Quality quality) => CreateInstance(1, quality);
///
/// Creates a quality item instance from this definition with the specified quantity and quality.
@@ -49,7 +51,7 @@ internal QualityItemDefinition(S1ItemFramework.QualityItemDefinition definition)
/// The quantity to apply to the created instance.
/// The quality to apply to the created instance.
/// A quality item instance using the specified quantity and quality.
- public QualityItemInstance CreateInstance(int quantity, Quality quality) =>
+ public QualityItemInstance CreateInstance(int quantity, Products.Quality quality) =>
new QualityItemInstance(new S1ItemFramework.QualityItemInstance(
S1QualityDefinition,
quantity,
@@ -58,9 +60,9 @@ public QualityItemInstance CreateInstance(int quantity, Quality quality) =>
///
/// The default quality for this item.
///
- public Quality DefaultQuality
+ public Products.Quality DefaultQuality
{
- get => (Quality)S1QualityDefinition.DefaultQuality;
+ get => (Products.Quality)S1QualityDefinition.DefaultQuality;
set => S1QualityDefinition.DefaultQuality = (S1ItemFramework.EQuality)value;
}
}
diff --git a/S1API/Items/QualityItemDefinitionBuilder.cs b/S1API/Items/QualityItemDefinitionBuilder.cs
index db7fd0f..bfb912e 100644
--- a/S1API/Items/QualityItemDefinitionBuilder.cs
+++ b/S1API/Items/QualityItemDefinitionBuilder.cs
@@ -27,6 +27,7 @@ namespace S1API.Items
/// Builder for composing quality item definitions at runtime.
/// Use fluent methods to configure item properties before calling
///
+ [Obsolete("Use S1API.Items.Quality.QualityItemDefinitionBuilder instead")]
public sealed class QualityItemDefinitionBuilder
{
private static readonly Log Logger = new Log("QualityItemDefinitionBuilder");
@@ -262,7 +263,7 @@ public QualityItemDefinitionBuilder WithRequiredRank(Leveling.FullRank? rank)
///
/// The default quality to assign to items of this definition.
/// >The builder instance for fluent chaining.
- public QualityItemDefinitionBuilder WithDefaultQuality(Quality quality)
+ public QualityItemDefinitionBuilder WithDefaultQuality(Products.Quality quality)
{
_definition.DefaultQuality = (S1ItemFramework.EQuality)quality;
return this;
@@ -274,6 +275,8 @@ public QualityItemDefinitionBuilder WithDefaultQuality(Quality quality)
/// A wrapper around the created storable item definition.
public QualityItemDefinition Build()
{
+ if (string.IsNullOrWhiteSpace(_definition.ID))
+ throw new ArgumentException("Item ID cannot be null, empty, or whitespace.", nameof(_definition.ID));
if (!_hasCustomStoredItem && _definition.StoredItem != null)
{
// Ensure placeholder naming stays in sync after late changes.
@@ -409,4 +412,4 @@ private static void WarnIfStationItemMissingChemistryModules(S1StationFramework.
}
}
}
-}
+}
\ No newline at end of file
diff --git a/S1API/Items/QualityItemInstance.cs b/S1API/Items/QualityItemInstance.cs
index 121ec8e..9e76ae7 100644
--- a/S1API/Items/QualityItemInstance.cs
+++ b/S1API/Items/QualityItemInstance.cs
@@ -3,6 +3,7 @@
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using S1ItemFramework = ScheduleOne.ItemFramework;
#endif
+using System;
using S1API.Internal.Utils;
using S1API.Products;
@@ -12,6 +13,7 @@ namespace S1API.Items
/// Represents a quality item instance in the game world (usable item).
/// Extends with quality information.
///
+ [Obsolete("Use S1API.Items.Quality.QualityItemInstance instead")]
public class QualityItemInstance : ItemInstance
{
///
@@ -31,9 +33,9 @@ internal QualityItemInstance(S1ItemFramework.QualityItemInstance itemInstance) :
///
/// The quality of this item.
///
- public Quality Quality
+ public Products.Quality Quality
{
- get => (Quality)S1QualityInstance.Quality;
+ get => (Products.Quality)S1QualityInstance.Quality;
set => S1QualityInstance.Quality = (S1ItemFramework.EQuality)value;
}
diff --git a/S1API/Items/Storable/ItemCreator.cs b/S1API/Items/Storable/ItemCreator.cs
new file mode 100644
index 0000000..b93565b
--- /dev/null
+++ b/S1API/Items/Storable/ItemCreator.cs
@@ -0,0 +1,166 @@
+#if (IL2CPPMELON)
+using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
+using S1Registry = Il2CppScheduleOne.Registry;
+#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+using S1ItemFramework = ScheduleOne.ItemFramework;
+using S1Registry = ScheduleOne.Registry;
+#endif
+using System;
+using S1API.Internal.Utils;
+using S1API.Leveling;
+using UnityEngine;
+
+namespace S1API.Items.Storable
+{
+ ///
+ /// Provides convenient static methods for creating custom items.
+ /// Use for flexible configuration or for quick creation.
+ ///
+ ///
+ /// All items in Schedule One are storable items (StorableItemDefinition), so both methods create the same type.
+ ///
+ public static class ItemCreator
+ {
+ ///
+ /// Creates a new builder for composing an item definition with full flexibility.
+ /// Use fluent methods to configure the item, then call Build() to register it.
+ ///
+ /// A new StorableItemDefinitionBuilder instance for fluent configuration.
+ ///
+ ///
+ /// var item = ItemCreator.CreateBuilder()
+ /// .WithBasicInfo("my_tool", "Custom Tool", "A custom tool", ItemCategory.Tools)
+ /// .WithStackLimit(5)
+ /// .WithPricing(25f, 0.3f)
+ /// .Build();
+ ///
+ ///
+ public static StorableItemDefinitionBuilder CreateBuilder()
+ {
+ return new StorableItemDefinitionBuilder();
+ }
+
+ ///
+ /// Creates a new storable item builder by cloning an existing item by ID.
+ ///
+ /// The ID of the item to clone.
+ /// A builder pre-configured with the source item properties.
+ /// Thrown if the source item ID is not found or is not a storable item.
+ public static StorableItemDefinitionBuilder CloneFrom(string sourceItemId)
+ {
+ if (string.IsNullOrWhiteSpace(sourceItemId))
+ {
+ throw new ArgumentException("Source item ID cannot be null or whitespace", nameof(sourceItemId));
+ }
+
+ var sourceDefinition = S1Registry.GetItem(sourceItemId);
+ if (sourceDefinition == null)
+ {
+ throw new ArgumentException($"Source item with ID '{sourceItemId}' not found in registry", nameof(sourceItemId));
+ }
+
+ if (!CrossType.Is(sourceDefinition, out S1ItemFramework.StorableItemDefinition storableDef))
+ {
+ throw new ArgumentException($"Item '{sourceItemId}' is not an StorableItemDefinition", nameof(sourceItemId));
+ }
+
+ return new StorableItemDefinitionBuilder(storableDef);
+ }
+
+ ///
+ /// Creates a new storable item builder by cloning an existing storable item wrapper.
+ ///
+ /// The storable item definition to clone.
+ /// A builder pre-configured with the source item properties.
+ /// Thrown if the source definition is null.
+ public static StorableItemDefinitionBuilder CloneFrom(StorableItemDefinition source)
+ {
+ if (source == null)
+ {
+ throw new ArgumentNullException(nameof(source), "Source storable item definition cannot be null");
+ }
+
+ return new StorableItemDefinitionBuilder(source.S1StorableItemDefinition);
+ }
+
+ ///
+ /// Creates an item with common parameters in a single call.
+ /// The item is automatically registered with the game's registry.
+ ///
+ /// Unique identifier for the item (e.g., "my_custom_tool").
+ /// Display name shown in UI.
+ /// Item description shown in tooltips.
+ /// Item category for inventory organization.
+ /// Maximum quantity per inventory slot (default: 10).
+ /// Base price when buying from shops (default: 10).
+ /// Fraction of purchase price recovered when selling (default: 0.5).
+ /// Whether the item is legal or illegal (default: Legal).
+ /// The player rank required to purchase the item, null if no rank required (default: null).
+ /// Optional sprite to use as the item icon.
+ /// Optional equippable component to attach.
+ /// A wrapper around the created item definition.
+ ///
+ ///
+ /// var item = ItemCreator.CreateItem(
+ /// id: "my_tool",
+ /// name: "Custom Tool",
+ /// description: "A custom tool for crafting",
+ /// category: ItemCategory.Tools,
+ /// stackLimit: 5,
+ /// basePurchasePrice: 25f
+ /// );
+ ///
+ ///
+ public static StorableItemDefinition CreateItem(
+ string id,
+ string name,
+ string description,
+ ItemCategory category,
+ int stackLimit = 10,
+ float basePurchasePrice = 10f,
+ float resellMultiplier = 0.5f,
+ LegalStatus legalStatus = LegalStatus.Legal,
+ FullRank? requiredRank = null,
+ Sprite icon = null,
+ Equippable equippable = null)
+ {
+ var builder = new StorableItemDefinitionBuilder()
+ .WithBasicInfo(id, name, description, category)
+ .WithStackLimit(stackLimit)
+ .WithPricing(basePurchasePrice, resellMultiplier)
+ .WithRequiredRank(requiredRank)
+ .WithLegalStatus(legalStatus);
+
+ if (icon != null)
+ {
+ builder.WithIcon(icon);
+ }
+
+ if (equippable != null)
+ {
+ builder.WithEquippable(equippable);
+ }
+
+ return builder.Build();
+ }
+
+ ///
+ /// Creates a new equippable builder for creating custom equippable components.
+ /// Use this to create equippable behavior that can be attached to items.
+ ///
+ /// A new EquippableBuilder instance.
+ ///
+ ///
+ /// var equippable = ItemCreator.CreateEquippableBuilder()
+ /// .CreateBasicEquippable("MyEquippable")
+ /// .WithInteraction(canInteract: true, canPickup: true)
+ /// .Build();
+ ///
+ ///
+ public static EquippableBuilder CreateEquippableBuilder()
+ {
+ return new EquippableBuilder();
+ }
+ }
+}
+
diff --git a/S1API/Items/Storable/StorableItemDefinition.cs b/S1API/Items/Storable/StorableItemDefinition.cs
new file mode 100644
index 0000000..e917c94
--- /dev/null
+++ b/S1API/Items/Storable/StorableItemDefinition.cs
@@ -0,0 +1,94 @@
+#if (IL2CPPMELON)
+using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
+#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+using S1ItemFramework = ScheduleOne.ItemFramework;
+#endif
+using S1API.Leveling;
+using UnityEngine;
+
+namespace S1API.Items.Storable
+{
+ ///
+ /// Represents an item definition that can be purchased, sold, and stored in inventories.
+ /// Extends with economic properties.
+ ///
+ ///
+ /// In Schedule One, all items are StorableItemDefinition or subclasses thereof.
+ /// The base ItemDefinition class is abstract and not used directly in gameplay.
+ ///
+ public class StorableItemDefinition : ItemDefinition
+ {
+ ///
+ /// INTERNAL: Wraps an existing native storable item definition.
+ ///
+ internal StorableItemDefinition(S1ItemFramework.StorableItemDefinition definition)
+ : base(definition)
+ {
+ S1StorableItemDefinition = definition;
+ }
+
+ ///
+ /// INTERNAL: A reference to the native game storable item definition.
+ ///
+ internal S1ItemFramework.StorableItemDefinition S1StorableItemDefinition { get; }
+
+ ///
+ /// The base purchase price for this item in shops.
+ ///
+ public float BasePurchasePrice
+ {
+ get => S1StorableItemDefinition.BasePurchasePrice;
+ set => S1StorableItemDefinition.BasePurchasePrice = value;
+ }
+
+ ///
+ /// The resell multiplier (0.0 to 1.0) that determines how much of the purchase price
+ /// can be recovered when selling the item.
+ ///
+ public float ResellMultiplier
+ {
+ get => S1StorableItemDefinition.ResellMultiplier;
+ set => S1StorableItemDefinition.ResellMultiplier = value;
+ }
+
+ ///
+ /// Gets whether this item is currently unlocked (available for purchase/use).
+ ///
+ public bool IsUnlocked =>
+ S1StorableItemDefinition.IsUnlocked;
+
+ ///
+ /// Whether purchasing this item requires the player to be at or above a certain level.
+ ///
+ public bool RequiresLevelToPurchase
+ {
+ get => S1StorableItemDefinition.RequiresLevelToPurchase;
+ set => S1StorableItemDefinition.RequiresLevelToPurchase = value;
+ }
+
+ ///
+ /// The required player level to purchase this item, if is true.
+ ///
+ public FullRank RequiredRank
+ {
+ get => FullRank.FromNative(S1StorableItemDefinition.RequiredRank);
+ set => S1StorableItemDefinition.RequiredRank = value.ToNative();
+ }
+
+ ///
+ /// Gets whether this item has a StationItem assigned (used by station/minigame tasks, e.g., Chemistry Station).
+ ///
+ public bool HasStationItem =>
+ S1StorableItemDefinition.StationItem != null;
+
+ ///
+ /// Gets the StationItem prefab GameObject for this item, if any.
+ ///
+ ///
+ /// This is primarily used for debugging and tooling. Prefer configuring StationItem via
+ /// during build/registration.
+ ///
+ public GameObject? StationItemPrefab =>
+ S1StorableItemDefinition.StationItem?.gameObject;
+ }
+}
diff --git a/S1API/Items/Storable/StorableItemDefinitionBuilder.cs b/S1API/Items/Storable/StorableItemDefinitionBuilder.cs
new file mode 100644
index 0000000..3d63532
--- /dev/null
+++ b/S1API/Items/Storable/StorableItemDefinitionBuilder.cs
@@ -0,0 +1,500 @@
+#if (IL2CPPMELON)
+using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
+using S1CoreItemFramework = Il2CppScheduleOne.Core.Items.Framework;
+using S1Levelling = Il2CppScheduleOne.Levelling;
+using S1Registry = Il2CppScheduleOne.Registry;
+using S1StationFramework = Il2CppScheduleOne.StationFramework;
+using S1Storage = Il2CppScheduleOne.Storage;
+#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+using S1ItemFramework = ScheduleOne.ItemFramework;
+using S1CoreItemFramework = ScheduleOne.Core.Items.Framework;
+using S1Levelling = ScheduleOne.Levelling;
+using S1Registry = ScheduleOne.Registry;
+using S1StationFramework = ScheduleOne.StationFramework;
+using S1Storage = ScheduleOne.Storage;
+#endif
+using System;
+using System.Collections.Generic;
+using S1API.Logging;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace S1API.Items.Storable
+{
+ ///
+ /// Builder for composing item definitions at runtime.
+ /// Use fluent methods to configure item properties before calling .
+ ///
+ ///
+ /// All items in Schedule One are StorableItemDefinition (or subclasses thereof).
+ /// The base ItemDefinition class is never used directly in the game.
+ ///
+ public sealed class StorableItemDefinitionBuilder
+ : StorableItemDefinitionBuilderBase
+ {
+ ///
+ internal StorableItemDefinitionBuilder(
+ S1ItemFramework.StorableItemDefinition source)
+ : base(source)
+ {
+ }
+
+ ///
+ internal StorableItemDefinitionBuilder()
+ : base()
+ {
+ }
+
+ ///
+ /// Builds the item definition, registers it with the game's registry, and returns a wrapper.
+ ///
+ /// A wrapper around the created storable item definition.
+ public new Storable.StorableItemDefinition Build()
+ {
+ return base.Build();
+ }
+ }
+
+ ///
+ /// State class for managing shared resources and caches used by .
+ ///
+ internal static class StorableItemDefinitionBuilderState
+ {
+ internal static readonly Log Logger = new Log("StorableItemDefinitionBuilder");
+ internal static readonly object StationItemGate = new object();
+
+ internal static readonly Dictionary StationItemCache =
+ new Dictionary();
+
+ internal static readonly HashSet WarnedStationItemModuleMissing = new HashSet();
+ internal static GameObject _stationItemRoot;
+ }
+
+ ///
+ /// INTERNAL: Generic base builder for composing item definitions at runtime, with fluent methods returning the correct subclass type.
+ ///
+ /// The concrete builder type being implemented (e.g., StorableItemDefinitionBuilder).
+ public abstract class StorableItemDefinitionBuilderBase
+ where TSelf : StorableItemDefinitionBuilderBase
+ {
+ private static Log Logger => StorableItemDefinitionBuilderState.Logger;
+ private static object StationItemGate => StorableItemDefinitionBuilderState.StationItemGate;
+
+ private static Dictionary StationItemCache =>
+ StorableItemDefinitionBuilderState.StationItemCache;
+
+ private static HashSet WarnedStationItemModuleMissing =>
+ StorableItemDefinitionBuilderState.WarnedStationItemModuleMissing;
+
+ private static GameObject StationItemRoot
+ {
+ get => StorableItemDefinitionBuilderState._stationItemRoot;
+ set => StorableItemDefinitionBuilderState._stationItemRoot = value;
+ }
+
+ ///
+ /// INTERNAL: The underlying game item definition being composed by this builder.
+ ///
+ protected readonly S1ItemFramework.StorableItemDefinition Definition;
+ private readonly GameObject _storedItemPlaceholder;
+ private bool _hasCustomStoredItem;
+
+ private TSelf Self => (TSelf)this;
+
+ ///
+ /// INTERNAL: Creates a new builder instance with a fresh StorableItemDefinition.
+ /// Only can instantiate this.
+ ///
+ internal StorableItemDefinitionBuilderBase(
+ Func? definitionFactory = null)
+ {
+ Definition = definitionFactory != null
+ ? definitionFactory()
+ : ScriptableObject.CreateInstance();
+
+ ApplyDefaults();
+
+ _storedItemPlaceholder = CreateStoredItemPlaceholder();
+ }
+
+ ///
+ /// INTERNAL: Creates a builder instance initialized by cloning an existing item.
+ /// Only can instantiate this.
+ ///
+ /// The existing item definition to clone properties from.
+ /// Optional factory function to create the definition instance. If null, a default StorableItemDefinition will be created.
+ internal StorableItemDefinitionBuilderBase(
+ S1ItemFramework.StorableItemDefinition source,
+ Func? definitionFactory = null)
+ : this(definitionFactory)
+ {
+ // Intentionally virtual - Overrides should operate only
+ // on fields initialized by chained constructor i.e. Definition
+ CopyPropertiesFrom(source);
+ }
+
+ ///
+ /// Copies all properties from a source definition to the current definition.
+ ///
+ /// The source definition to copy from.
+ protected virtual void CopyPropertiesFrom(S1ItemFramework.StorableItemDefinition source)
+ {
+ if (source == null) return;
+
+ // Basic ItemDefinition properties
+ Definition.Name = source.Name;
+ Definition.Description = source.Description;
+ Definition.Category = source.Category;
+ Definition.StackLimit = source.StackLimit;
+ Definition.AvailableInDemo = source.AvailableInDemo;
+ Definition.UsableInFilters = source.UsableInFilters;
+ Definition.Icon = source.Icon;
+ Definition.legalStatus = source.legalStatus;
+ Definition.PickpocketDifficultyMultiplier = source.PickpocketDifficultyMultiplier;
+ Definition.CombatUtility = source.CombatUtility;
+
+ // StorableItemDefinition properties
+ Definition.BasePurchasePrice = source.BasePurchasePrice;
+ Definition.ResellMultiplier = source.ResellMultiplier;
+ Definition.ShopCategories = source.ShopCategories;
+ Definition.RequiresLevelToPurchase = source.RequiresLevelToPurchase;
+ Definition.RequiredRank = source.RequiredRank;
+ Definition.StoredItem = source.StoredItem != null ? source.StoredItem : Definition.StoredItem;
+ Definition.StationItem = source.StationItem;
+ Definition.Equippable = source.Equippable;
+ Definition.CustomItemUI = source.CustomItemUI;
+ }
+
+ private void ApplyDefaults()
+ {
+ Definition.StackLimit = 10;
+ Definition.BasePurchasePrice = 10f;
+ Definition.ResellMultiplier = 0.5f;
+ Definition.Category = S1CoreItemFramework.EItemCategory.Tools;
+ Definition.legalStatus = S1CoreItemFramework.ELegalStatus.Legal;
+ Definition.AvailableInDemo = true;
+ Definition.UsableInFilters = true;
+ Definition.RequiresLevelToPurchase = false;
+ Definition.RequiredRank = new S1Levelling.FullRank(S1Levelling.ERank.Street_Rat, 1);
+ }
+
+ private GameObject CreateStoredItemPlaceholder()
+ {
+ var storedItemPlaceholder = new GameObject("S1API_DefaultStoredItem");
+ storedItemPlaceholder.SetActive(false);
+ storedItemPlaceholder.hideFlags = HideFlags.HideAndDontSave;
+ Object.DontDestroyOnLoad(storedItemPlaceholder);
+ var storedItemComponent = storedItemPlaceholder.AddComponent();
+ Definition.StoredItem = storedItemComponent;
+ return storedItemPlaceholder;
+ }
+
+ ///
+ /// Sets the basic information for the item.
+ ///
+ /// Unique identifier for the item (e.g., "my_custom_tool").
+ /// Display name shown in UI.
+ /// Item description shown in tooltips.
+ /// Item category for inventory organization.
+ /// The builder instance for fluent chaining.
+ public TSelf WithBasicInfo(string id, string name, string description, ItemCategory category)
+ {
+ Definition.ID = id;
+ Definition.Name = name;
+ Definition.Description = description;
+ Definition.Category = (S1CoreItemFramework.EItemCategory)category;
+
+ // Update the underlying ScriptableObject name for clarity in inspectors/debuggers.
+ var displayName = string.IsNullOrEmpty(name) ? id : name;
+ if (!string.IsNullOrEmpty(displayName))
+ {
+ Definition.name = displayName;
+ if (_storedItemPlaceholder != null && !_hasCustomStoredItem)
+ {
+ _storedItemPlaceholder.name = $"{displayName}_StoredItem";
+ }
+ }
+
+ return Self;
+ }
+
+ ///
+ /// Sets the maximum stack size for this item.
+ ///
+ /// Maximum quantity per inventory slot (1-999).
+ /// The builder instance for fluent chaining.
+ public TSelf WithStackLimit(int limit)
+ {
+ Definition.StackLimit = Mathf.Clamp(limit, 1, 999);
+ return Self;
+ }
+
+ ///
+ /// Sets the icon sprite displayed for this item in UI.
+ ///
+ /// The sprite to use as the item icon.
+ /// The builder instance for fluent chaining.
+ public TSelf WithIcon(Sprite icon)
+ {
+ Definition.Icon = icon;
+ return Self;
+ }
+
+ ///
+ /// Configures the economic properties of the item.
+ ///
+ /// Base price when buying from shops.
+ /// Fraction of purchase price recovered when selling (0.0 to 1.0).
+ /// The builder instance for fluent chaining.
+ public TSelf WithPricing(float basePurchasePrice, float resellMultiplier = 0.5f)
+ {
+ Definition.BasePurchasePrice = Mathf.Max(0f, basePurchasePrice);
+ Definition.ResellMultiplier = Mathf.Clamp01(resellMultiplier);
+ return Self;
+ }
+
+ ///
+ /// Sets the legal status of the item.
+ ///
+ /// Whether the item is legal or illegal.
+ /// The builder instance for fluent chaining.
+ public TSelf WithLegalStatus(LegalStatus status)
+ {
+ Definition.legalStatus = (S1CoreItemFramework.ELegalStatus)status;
+ return Self;
+ }
+
+ ///
+ /// Attaches an equippable component to this item, allowing it to be equipped by the player.
+ ///
+ /// The equippable wrapper to attach.
+ /// The builder instance for fluent chaining.
+ public TSelf WithEquippable(Equippable equippable)
+ {
+ if (equippable != null)
+ {
+ Definition.Equippable = equippable.S1Equippable;
+ }
+
+ return Self;
+ }
+
+ ///
+ /// Assigns a custom StoredItem prefab for this definition.
+ ///
+ /// Prefab containing a StoredItem component.
+ /// The builder instance for fluent chaining.
+ public TSelf WithStoredItem(GameObject storedItemPrefab)
+ {
+ if (storedItemPrefab == null)
+ return Self;
+
+ var storedItem = storedItemPrefab.GetComponent() ??
+ storedItemPrefab.AddComponent();
+ Definition.StoredItem = storedItem;
+ _hasCustomStoredItem = true;
+ return Self;
+ }
+
+ ///
+ /// Assigns a StationItem prefab to this item definition so it can be used as a station/minigame ingredient
+ /// (e.g., Chemistry Station).
+ ///
+ ///
+ /// S1API clones and caches the prefab under a hidden DontDestroyOnLoad root by default.
+ /// This avoids mutating shared prefabs and helps keep the reference stable across scene loads.
+ ///
+ /// A prefab GameObject that has a StationItem component.
+ /// The builder instance for fluent chaining.
+ /// Thrown if is null.
+ /// Thrown if does not have a StationItem component.
+ public TSelf WithStationItem(GameObject stationItemPrefab)
+ {
+ if (stationItemPrefab == null)
+ throw new ArgumentNullException(nameof(stationItemPrefab));
+
+ var stationItem = stationItemPrefab.GetComponent();
+ if (stationItem == null)
+ throw new ArgumentException("Station item prefab must have a StationItem component.",
+ nameof(stationItemPrefab));
+
+ var cached = GetOrCreateStationItemPrefab(stationItem);
+ Definition.StationItem = cached;
+
+ WarnIfStationItemMissingChemistryModules(cached);
+ return Self;
+ }
+
+ ///
+ /// Clears the StationItem reference for this definition.
+ ///
+ public TSelf WithoutStationItem()
+ {
+ Definition.StationItem = null;
+ return Self;
+ }
+
+ ///
+ /// Sets whether this item is available in the demo version of the game.
+ ///
+ /// True if available in demo, false otherwise.
+ /// The builder instance for fluent chaining.
+ public TSelf WithDemoAvailability(bool available)
+ {
+ Definition.AvailableInDemo = available;
+ return Self;
+ }
+
+ ///
+ /// Assigns a level requirement for purchasing this item in shops.
+ ///
+ /// The required rank to purchase this item, or null to remove level requirement.
+ /// >The builder instance for fluent chaining.
+ public TSelf WithRequiredRank(Leveling.FullRank? rank)
+ {
+ if (rank == null)
+ {
+ Definition.RequiresLevelToPurchase = false;
+ return Self;
+ }
+
+ Definition.RequiredRank = rank.Value.ToNative();
+ Definition.RequiresLevelToPurchase = true;
+ return Self;
+ }
+
+ ///
+ /// Builds the item definition, registers it with the game's registry, and returns a wrapper.
+ ///
+ /// A wrapper around the created storable item definition.
+ ///
+ /// Designated virtual, usually shadowed in subclasses due to different return type.
+ ///
+ protected virtual Storable.StorableItemDefinition Build()
+ {
+ if (string.IsNullOrWhiteSpace(Definition.ID))
+ {
+ Logger.Error("Cannot build item definition: ID is required. Use WithBasicInfo(...) to set the ID.");
+ throw new InvalidOperationException("Cannot build item definition: ID is required.");
+ }
+ if (!_hasCustomStoredItem && Definition.StoredItem != null)
+ {
+ // Ensure placeholder naming stays in sync after late changes.
+ if (!string.IsNullOrEmpty(Definition.Name) && _storedItemPlaceholder != null)
+ {
+ _storedItemPlaceholder.name = $"{Definition.Name}_StoredItem";
+ }
+ }
+
+ // Register with the game's registry
+ S1Registry.Instance.AddToRegistry(Definition);
+
+ // Return wrapper
+ return CreateWrapper(Definition);
+ }
+
+ ///
+ /// INTERNAL: Builds and returns the raw game item definition without registering.
+ /// Used internally by S1API. Modders should use instead.
+ ///
+ ///
+ /// Designated virtual, usually shadowed in subclasses due to different return type.
+ ///
+ internal virtual S1ItemFramework.StorableItemDefinition BuildInternal()
+ {
+ return Definition;
+ }
+
+ ///
+ /// Creates a wrapper around the given item definition.
+ /// Subclasses can override this to return a more specific wrapper type.
+ ///
+ /// The item definition to wrap.
+ /// >A wrapper around the given item definition.
+ protected virtual StorableItemDefinition CreateWrapper(
+ S1ItemFramework.StorableItemDefinition definition)
+ {
+ return new StorableItemDefinition(definition);
+ }
+
+ private static S1StationFramework.StationItem GetOrCreateStationItemPrefab(
+ S1StationFramework.StationItem stationItemPrefab)
+ {
+ var id = stationItemPrefab.GetInstanceID();
+
+ lock (StationItemGate)
+ {
+ if (StationItemCache.TryGetValue(id, out var cached) && cached != null)
+ return cached;
+
+ var root = GetStationItemRoot();
+
+ // Clone + cache (final decision): keep a stable hidden prefab reference across scene loads.
+ var clone = Object.Instantiate(stationItemPrefab, root.transform);
+ clone.gameObject.hideFlags = HideFlags.HideAndDontSave;
+ clone.name = $"{stationItemPrefab.name}_S1API_StationItem";
+
+ // Keep the cache far away from gameplay so it doesn't interfere with scenes.
+ clone.transform.position = root.transform.position;
+
+ StationItemCache[id] = clone;
+ return clone;
+ }
+ }
+
+ private static GameObject GetStationItemRoot()
+ {
+ if (StationItemRoot != null)
+ return StationItemRoot;
+
+ lock (StationItemGate)
+ {
+ if (StationItemRoot != null)
+ return StationItemRoot;
+
+ var root = new GameObject("S1API_StationItemCache");
+ root.hideFlags = HideFlags.HideAndDontSave;
+ Object.DontDestroyOnLoad(root);
+
+ // Place it far below the world; keep it active so instantiated prefabs remain active by default.
+ root.transform.position = new Vector3(0f, -10000f, 0f);
+
+ StationItemRoot = root;
+ return root;
+ }
+ }
+
+ private static void WarnIfStationItemMissingChemistryModules(S1StationFramework.StationItem stationItemPrefab)
+ {
+ if (stationItemPrefab == null)
+ return;
+
+ var id = stationItemPrefab.GetInstanceID();
+
+ lock (StationItemGate)
+ {
+ if (!WarnedStationItemModuleMissing.Add(id))
+ return;
+ }
+
+ try
+ {
+ var hasIngredientModule =
+ stationItemPrefab.GetComponentInChildren(true) != null;
+ var hasPourableModule =
+ stationItemPrefab.GetComponentInChildren(true) != null;
+
+ if (hasIngredientModule || hasPourableModule)
+ return;
+
+ Logger.Warning(
+ $"[S1API] StationItem prefab '{stationItemPrefab.name}' does not contain an IngredientModule or PourableModule. " +
+ "Chemistry station tasks may log errors or skip this ingredient at runtime.");
+ }
+ catch
+ {
+ // best-effort warning only
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/S1API/Items/StorableItemDefinition.cs b/S1API/Items/StorableItemDefinition.cs
index 0e3701c..38f5d8e 100644
--- a/S1API/Items/StorableItemDefinition.cs
+++ b/S1API/Items/StorableItemDefinition.cs
@@ -4,6 +4,7 @@
using S1ItemFramework = ScheduleOne.ItemFramework;
#endif
+using System;
using S1API.Leveling;
using UnityEngine;
@@ -17,6 +18,7 @@ namespace S1API.Items
/// In Schedule One, all items are StorableItemDefinition or subclasses thereof.
/// The base ItemDefinition class is abstract and not used directly in gameplay.
///
+ [Obsolete("Use S1API.Items.Storable.StorableItemDefinition instead")]
public class StorableItemDefinition : ItemDefinition
{
///
diff --git a/S1API/Items/StorableItemDefinitionBuilder.cs b/S1API/Items/StorableItemDefinitionBuilder.cs
index 91c66a1..cf9716c 100644
--- a/S1API/Items/StorableItemDefinitionBuilder.cs
+++ b/S1API/Items/StorableItemDefinitionBuilder.cs
@@ -1,4 +1,4 @@
-#if (IL2CPPMELON)
+#if (IL2CPPMELON)
using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
using S1CoreItemFramework = Il2CppScheduleOne.Core.Items.Framework;
using S1Levelling = Il2CppScheduleOne.Levelling;
@@ -30,6 +30,7 @@ namespace S1API.Items
/// All items in Schedule One are StorableItemDefinition (or subclasses thereof).
/// The base ItemDefinition class is never used directly in the game.
///
+ [Obsolete("Use S1API.Items.Storable.StorableItemDefinitionBuilder instead")]
public sealed class StorableItemDefinitionBuilder
{
private static readonly Log Logger = new Log("StorableItemDefinitionBuilder");
@@ -266,6 +267,8 @@ public StorableItemDefinitionBuilder WithRequiredRank(Leveling.FullRank? rank)
/// A wrapper around the created storable item definition.
public StorableItemDefinition Build()
{
+ if (string.IsNullOrWhiteSpace(_definition.ID))
+ throw new ArgumentException("Item ID cannot be null, empty, or whitespace.", nameof(_definition.ID));
if (!_hasCustomStoredItem && _definition.StoredItem != null)
{
// Ensure placeholder naming stays in sync after late changes.