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.