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/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);
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..db7fd0f
--- /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
+ ///
+ public sealed class QualityItemDefinitionBuilder
+ {
+ private static readonly Log Logger = new Log("QualityItemDefinitionBuilder");
+ private static readonly object StationItemGate = new object();
+ private static readonly Dictionary 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
+ }
+ }
+ }
+}
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