From b7c2203f594071ff49cf6b244678cb6c8f9e7072 Mon Sep 17 00:00:00 2001 From: k0 <21180271+k073l@users.noreply.github.com> Date: Fri, 29 May 2026 02:06:39 +0200 Subject: [PATCH 1/4] feat(items): add quality item wrappers and builders --- S1API/Items/ItemCreator.cs | 53 +++ S1API/Items/QualityItemCreator.cs | 67 +++ S1API/Items/QualityItemDefinition.cs | 67 +++ S1API/Items/QualityItemDefinitionBuilder.cs | 412 +++++++++++++++++++ S1API/Items/QualityItemInstance.cs | 45 ++ S1API/Items/StorableItemDefinition.cs | 19 + S1API/Items/StorableItemDefinitionBuilder.cs | 80 +++- 7 files changed, 740 insertions(+), 3 deletions(-) create mode 100644 S1API/Items/QualityItemCreator.cs create mode 100644 S1API/Items/QualityItemDefinition.cs create mode 100644 S1API/Items/QualityItemDefinitionBuilder.cs create mode 100644 S1API/Items/QualityItemInstance.cs diff --git a/S1API/Items/ItemCreator.cs b/S1API/Items/ItemCreator.cs index 7d0a070..b447be6 100644 --- a/S1API/Items/ItemCreator.cs +++ b/S1API/Items/ItemCreator.cs @@ -1,4 +1,14 @@ +using System; +using S1API.Internal.Utils; +using S1API.Leveling; using UnityEngine; +#if (IL2CPPMELON) +using S1ItemFramework = Il2CppScheduleOne.ItemFramework; +using S1Registry = Il2CppScheduleOne.Registry; +#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) +using S1ItemFramework = ScheduleOne.ItemFramework; +using S1Registry = ScheduleOne.Registry; +#endif namespace S1API.Items { @@ -30,6 +40,44 @@ 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) + { + 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. @@ -42,6 +90,8 @@ public static StorableItemDefinitionBuilder CreateBuilder() /// 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). /// Optional sprite to use as the item icon. /// Optional equippable component to attach. /// A wrapper around the created item definition. @@ -66,6 +116,8 @@ 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) { @@ -73,6 +125,7 @@ public static StorableItemDefinition CreateItem( .WithBasicInfo(id, name, description, category) .WithStackLimit(stackLimit) .WithPricing(basePurchasePrice, resellMultiplier) + .WithRequiredRank(requiredRank) .WithLegalStatus(legalStatus); if (icon != null) diff --git a/S1API/Items/QualityItemCreator.cs b/S1API/Items/QualityItemCreator.cs new file mode 100644 index 0000000..3255b15 --- /dev/null +++ b/S1API/Items/QualityItemCreator.cs @@ -0,0 +1,67 @@ +#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 +{ + /// + /// 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) + { + 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/QualityItemDefinition.cs b/S1API/Items/QualityItemDefinition.cs new file mode 100644 index 0000000..576474c --- /dev/null +++ b/S1API/Items/QualityItemDefinition.cs @@ -0,0 +1,67 @@ +#if (IL2CPPMELON) +using S1ItemFramework = Il2CppScheduleOne.ItemFramework; +#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) +using S1ItemFramework = ScheduleOne.ItemFramework; +#endif +using S1API.Products; + +namespace S1API.Items +{ + /// + /// Represents a quality item definition that can be consumed or used in recipes + /// Extends with quality-specific properties. + /// + /// + /// Use + /// + public class QualityItemDefinition : 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(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, Quality quality) => + new QualityItemInstance(new S1ItemFramework.QualityItemInstance( + S1QualityDefinition, + quantity, + (S1ItemFramework.EQuality)quality)); + + /// + /// The default quality for this item. + /// + public Quality DefaultQuality + { + get => (Quality)S1QualityDefinition.DefaultQuality; + set => S1QualityDefinition.DefaultQuality = (S1ItemFramework.EQuality)value; + } + } +} \ No newline at end of file diff --git a/S1API/Items/QualityItemDefinitionBuilder.cs b/S1API/Items/QualityItemDefinitionBuilder.cs new file mode 100644 index 0000000..0db8058 --- /dev/null +++ b/S1API/Items/QualityItemDefinitionBuilder.cs @@ -0,0 +1,412 @@ +#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 S1API.Products; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace S1API.Items +{ + /// + /// Builder for composing quality item definitions at runtime. + /// Use fluent methods to configure item properties before calling + /// StationItemCache = new Dictionary(); + private static readonly HashSet WarnedStationItemModuleMissing = new HashSet(); + private static GameObject _stationItemRoot; + + private readonly S1ItemFramework.QualityItemDefinition _definition; + private readonly GameObject _storedItemPlaceholder; + private bool _hasCustomStoredItem; + + /// + /// INTERNAL: Creates a new builder instance with a fresh QualityItemDefinition. + /// Only QualityItemCreator can instantiate this. + /// + internal QualityItemDefinitionBuilder() + { + _definition = ScriptableObject.CreateInstance(); + + // Set defaults + _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); + _definition.DefaultQuality = S1ItemFramework.EQuality.Standard; + + // Provide a minimal StoredItem placeholder so the field is never null in tooling/inspectors. + _storedItemPlaceholder = new GameObject("S1API_DefaultStoredItem"); + _storedItemPlaceholder.SetActive(false); + _storedItemPlaceholder.hideFlags = HideFlags.HideAndDontSave; + Object.DontDestroyOnLoad(_storedItemPlaceholder); + var storedItemComponent = _storedItemPlaceholder.AddComponent(); + _definition.StoredItem = storedItemComponent; + } + + /// + /// INTERNAL: Creates a builder instance initialized by cloning an existing quality item definition. + /// Only QualityItemCreator can instantiate this. + /// + /// + internal QualityItemDefinitionBuilder(S1ItemFramework.QualityItemDefinition source) + { + _definition = ScriptableObject.CreateInstance(); + + // Provide a minimal StoredItem placeholder so the field is never null in tooling/inspectors. + _storedItemPlaceholder = new GameObject("S1API_DefaultStoredItem"); + _storedItemPlaceholder.SetActive(false); + _storedItemPlaceholder.hideFlags = HideFlags.HideAndDontSave; + Object.DontDestroyOnLoad(_storedItemPlaceholder); + var storedItemComponent = _storedItemPlaceholder.AddComponent(); + _definition.StoredItem = storedItemComponent; + + CopyPropertiesFrom(source); + } + + /// + /// Sets the basic information for the item. + /// + /// Unique identifier for the item (e.g., "my_custom_item"). + /// Display name shown in UI. + /// Item description shown in tooltips. + /// Item category for inventory organization. + /// The builder instance for fluent chaining. + public QualityItemDefinitionBuilder 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 this; + } + + /// + /// Sets the maximum stack size for this item. + /// + /// Maximum quantity per inventory slot (1-999). + /// The builder instance for fluent chaining. + public QualityItemDefinitionBuilder WithStackLimit(int limit) + { + _definition.StackLimit = Mathf.Clamp(limit, 1, 999); + return this; + } + + /// + /// 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 QualityItemDefinitionBuilder WithIcon(Sprite icon) + { + _definition.Icon = icon; + return this; + } + + /// + /// 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 QualityItemDefinitionBuilder WithPricing(float basePurchasePrice, float resellMultiplier = 0.5f) + { + _definition.BasePurchasePrice = Mathf.Max(0f, basePurchasePrice); + _definition.ResellMultiplier = Mathf.Clamp01(resellMultiplier); + return this; + } + + /// + /// Sets the legal status of the item. + /// + /// Whether the item is legal or illegal. + /// The builder instance for fluent chaining. + public QualityItemDefinitionBuilder WithLegalStatus(LegalStatus status) + { + _definition.legalStatus = (S1CoreItemFramework.ELegalStatus)status; + return this; + } + + /// + /// 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 QualityItemDefinitionBuilder WithEquippable(Equippable equippable) + { + if (equippable != null) + { + _definition.Equippable = equippable.S1Equippable; + } + return this; + } + + /// + /// Assigns a custom StoredItem prefab for this definition. + /// + /// Prefab containing a StoredItem component. + /// The builder instance for fluent chaining. + public QualityItemDefinitionBuilder WithStoredItem(GameObject storedItemPrefab) + { + if (storedItemPrefab == null) + return this; + + var storedItem = storedItemPrefab.GetComponent() ?? storedItemPrefab.AddComponent(); + _definition.StoredItem = storedItem; + _hasCustomStoredItem = true; + return this; + } + + /// + /// 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 QualityItemDefinitionBuilder 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 this; + } + + /// + /// Clears the StationItem reference for this definition. + /// + public QualityItemDefinitionBuilder WithoutStationItem() + { + _definition.StationItem = null; + return this; + } + + /// + /// 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 QualityItemDefinitionBuilder WithDemoAvailability(bool available) + { + _definition.AvailableInDemo = available; + return this; + } + + /// + /// 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 QualityItemDefinitionBuilder WithRequiredRank(Leveling.FullRank? rank) + { + if (rank == null) + { + _definition.RequiresLevelToPurchase = false; + return this; + } + _definition.RequiredRank = rank.Value.ToNative(); + _definition.RequiresLevelToPurchase = true; + return this; + } + + /// + /// 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(Quality quality) + { + _definition.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 storable item definition. + public QualityItemDefinition Build() + { + 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 new QualityItemDefinition(_definition); + } + + /// + /// INTERNAL: Builds and returns the raw game item definition without registering. + /// Used internally by S1API. Modders should use instead. + /// + internal S1ItemFramework.QualityItemDefinition BuildInternal() + { + return _definition; + } + + /// + /// Copies all properties from a source StorableItemDefinition to the current definition. + /// + /// The source definition to copy properties from. + private void CopyPropertiesFrom(S1ItemFramework.QualityItemDefinition 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.DefaultQuality = source.DefaultQuality; + _definition.CustomItemUI = source.CustomItemUI; + } + + 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/QualityItemInstance.cs b/S1API/Items/QualityItemInstance.cs new file mode 100644 index 0000000..b78ec11 --- /dev/null +++ b/S1API/Items/QualityItemInstance.cs @@ -0,0 +1,45 @@ +#if (IL2CPPMELON) +using S1ItemFramework = Il2CppScheduleOne.ItemFramework; +#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) +using S1ItemFramework = ScheduleOne.ItemFramework; +#endif +using S1API.Products; + +namespace S1API.Items +{ + /// + /// 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 Quality Quality + { + get => (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((S1ItemFramework.QualityItemDefinition)S1QualityInstance.Definition); + } +} \ No newline at end of file diff --git a/S1API/Items/StorableItemDefinition.cs b/S1API/Items/StorableItemDefinition.cs index 5760758..0e3701c 100644 --- a/S1API/Items/StorableItemDefinition.cs +++ b/S1API/Items/StorableItemDefinition.cs @@ -4,6 +4,7 @@ using S1ItemFramework = ScheduleOne.ItemFramework; #endif +using S1API.Leveling; using UnityEngine; namespace S1API.Items @@ -57,6 +58,24 @@ public float ResellMultiplier 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). /// diff --git a/S1API/Items/StorableItemDefinitionBuilder.cs b/S1API/Items/StorableItemDefinitionBuilder.cs index 4f36e79..91c66a1 100644 --- a/S1API/Items/StorableItemDefinitionBuilder.cs +++ b/S1API/Items/StorableItemDefinitionBuilder.cs @@ -1,12 +1,14 @@ #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; @@ -47,7 +49,7 @@ public sealed class StorableItemDefinitionBuilder internal StorableItemDefinitionBuilder() { _definition = ScriptableObject.CreateInstance(); - + // Set defaults _definition.StackLimit = 10; _definition.BasePurchasePrice = 10f; @@ -56,6 +58,26 @@ internal StorableItemDefinitionBuilder() _definition.legalStatus = S1CoreItemFramework.ELegalStatus.Legal; _definition.AvailableInDemo = true; _definition.UsableInFilters = true; + _definition.RequiresLevelToPurchase = false; + _definition.RequiredRank = new S1Levelling.FullRank(S1Levelling.ERank.Street_Rat, 1); + + // Provide a minimal StoredItem placeholder so the field is never null in tooling/inspectors. + _storedItemPlaceholder = new GameObject("S1API_DefaultStoredItem"); + _storedItemPlaceholder.SetActive(false); + _storedItemPlaceholder.hideFlags = HideFlags.HideAndDontSave; + Object.DontDestroyOnLoad(_storedItemPlaceholder); + var storedItemComponent = _storedItemPlaceholder.AddComponent(); + _definition.StoredItem = storedItemComponent; + } + + /// + /// INTERNAL: Creates a builder instance initialized by cloning an existing storable item definition. + /// Only ItemCreator can instantiate this. + /// + /// The source definition to clone properties from. + internal StorableItemDefinitionBuilder(S1ItemFramework.StorableItemDefinition source) + { + _definition = ScriptableObject.CreateInstance(); // Provide a minimal StoredItem placeholder so the field is never null in tooling/inspectors. _storedItemPlaceholder = new GameObject("S1API_DefaultStoredItem"); @@ -64,6 +86,8 @@ internal StorableItemDefinitionBuilder() Object.DontDestroyOnLoad(_storedItemPlaceholder); var storedItemComponent = _storedItemPlaceholder.AddComponent(); _definition.StoredItem = storedItemComponent; + + CopyPropertiesFrom(source); } /// @@ -218,6 +242,24 @@ public StorableItemDefinitionBuilder WithDemoAvailability(bool available) return this; } + /// + /// 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 StorableItemDefinitionBuilder WithRequiredRank(Leveling.FullRank? rank) + { + if (rank == null) + { + _definition.RequiresLevelToPurchase = false; + return this; + } + + _definition.RequiredRank = rank.Value.ToNative(); + _definition.RequiresLevelToPurchase = true; + return this; + } + /// /// Builds the item definition, registers it with the game's registry, and returns a wrapper. /// @@ -249,7 +291,39 @@ internal S1ItemFramework.StorableItemDefinition BuildInternal() return _definition; } - private static S1StationFramework.StationItem GetOrCreateStationItemPrefab(S1StationFramework.StationItem stationItemPrefab) + /// + /// Copies all properties from a source StorableItemDefinition to the current definition. + /// + /// The source definition to copy properties from. + private 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; + } + + private static S1StationFramework.StationItem GetOrCreateStationItemPrefab( + S1StationFramework.StationItem stationItemPrefab) { var id = stationItemPrefab.GetInstanceID(); @@ -326,4 +400,4 @@ private static void WarnIfStationItemMissingChemistryModules(S1StationFramework. } } } -} +} \ No newline at end of file From e7415fdee19276f0184279e9325296a3b9280bc1 Mon Sep 17 00:00:00 2001 From: k0 <21180271+k073l@users.noreply.github.com> Date: Fri, 29 May 2026 19:05:53 +0200 Subject: [PATCH 2/4] fix(items): add quality definition to itemmanager --- S1API/Items/ItemManager.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/S1API/Items/ItemManager.cs b/S1API/Items/ItemManager.cs index b384dfc..d28260b 100644 --- a/S1API/Items/ItemManager.cs +++ b/S1API/Items/ItemManager.cs @@ -63,6 +63,10 @@ public static ItemDefinition GetItemDefinition(string itemID) out S1ItemFramework.AdditiveDefinition additiveDefinition)) return new AdditiveDefinition(additiveDefinition); + if (CrossType.Is(itemDefinition, + out S1ItemFramework.QualityItemDefinition qualityItemDefinition)) + return new QualityItemDefinition(qualityItemDefinition); + if (CrossType.Is(itemDefinition, out S1ItemFramework.StorableItemDefinition storableItemDefinition)) return new StorableItemDefinition(storableItemDefinition); From e5f165b843036430b5c380acff756a2711744b9e Mon Sep 17 00:00:00 2001 From: k073l <21180271+k073l@users.noreply.github.com> Date: Fri, 29 May 2026 21:04:42 +0200 Subject: [PATCH 3/4] fix(items): fix XML comment tag --- S1API/Items/QualityItemDefinitionBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/S1API/Items/QualityItemDefinitionBuilder.cs b/S1API/Items/QualityItemDefinitionBuilder.cs index 0db8058..db7fd0f 100644 --- a/S1API/Items/QualityItemDefinitionBuilder.cs +++ b/S1API/Items/QualityItemDefinitionBuilder.cs @@ -26,7 +26,7 @@ namespace S1API.Items /// /// Builder for composing quality item definitions at runtime. /// Use fluent methods to configure item properties before calling - /// public sealed class QualityItemDefinitionBuilder { private static readonly Log Logger = new Log("QualityItemDefinitionBuilder"); @@ -409,4 +409,4 @@ private static void WarnIfStationItemMissingChemistryModules(S1StationFramework. } } } -} \ No newline at end of file +} From 19b5a73b149c88689d67de144f792cb777b51863 Mon Sep 17 00:00:00 2001 From: ifBars Date: Wed, 3 Jun 2026 17:47:39 -0700 Subject: [PATCH 4/4] fix(items): use CrossType for derived item definitions Route derived item definition access through CrossType.As so IL2CPP wrappers use the runtime-safe cast path consistently. This covers quality, clothing, product, and shroom item instances, including the pre-existing clothing pattern that the PR copied. --- .gitignore | 1 + S1API/Items/ClothingItemInstance.cs | 4 +++- S1API/Items/QualityItemInstance.cs | 6 ++++-- S1API/Products/ProductInstance.cs | 3 ++- S1API/Products/ShroomInstance.cs | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index f27e327..c10425a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ obj/ bin/ *.user .vs/ +smoke # Build props local.* diff --git a/S1API/Items/ClothingItemInstance.cs b/S1API/Items/ClothingItemInstance.cs index f545156..71567be 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 S1API.Internal.Utils; namespace S1API.Items { @@ -40,7 +41,8 @@ public ClothingColor Color /// The clothing definition (template) this instance was created from. /// public new ClothingItemDefinition Definition => - new ClothingItemDefinition((S1Clothing.ClothingDefinition)S1ClothingInstance.Definition); + new ClothingItemDefinition( + CrossType.As(S1ClothingInstance.Definition)); } } diff --git a/S1API/Items/QualityItemInstance.cs b/S1API/Items/QualityItemInstance.cs index b78ec11..121ec8e 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 S1API.Internal.Utils; using S1API.Products; namespace S1API.Items @@ -40,6 +41,7 @@ public Quality Quality /// The quality item definition (template) this instance was created from. /// public new QualityItemDefinition Definition => - new QualityItemDefinition((S1ItemFramework.QualityItemDefinition)S1QualityInstance.Definition); + new QualityItemDefinition( + CrossType.As(S1QualityInstance.Definition)); } -} \ No newline at end of file +} diff --git a/S1API/Products/ProductInstance.cs b/S1API/Products/ProductInstance.cs index 229a29e..f9b1f94 100644 --- a/S1API/Products/ProductInstance.cs +++ b/S1API/Products/ProductInstance.cs @@ -58,7 +58,8 @@ internal ProductInstance(S1Product.ProductItemInstance productInstance) /// /// Gets the definition of the product associated with this instance. /// - public ProductDefinition Definition => new ProductDefinition(S1ProductInstance.Definition); + public ProductDefinition Definition => + new ProductDefinition(CrossType.As(S1ProductInstance.Definition)); /// /// Gets the list of properties associated with the product definition. diff --git a/S1API/Products/ShroomInstance.cs b/S1API/Products/ShroomInstance.cs index 1f68a3b..c0cdebe 100644 --- a/S1API/Products/ShroomInstance.cs +++ b/S1API/Products/ShroomInstance.cs @@ -37,7 +37,7 @@ internal ShroomInstance(S1Product.ShroomInstance shroomInstance) /// Gets the shroom-specific definition for this instance. /// public new ShroomDefinition Definition => - new ShroomDefinition(S1ShroomInstance.Definition as S1Product.ShroomDefinition); + new ShroomDefinition(CrossType.As(S1ShroomInstance.Definition)); /// /// Gets the display name of the shroom instance.