Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions S1API/Internal/Utils/AutoPropertySetter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Reflection;

namespace S1API.Internal.Utils
{
/// <summary>
/// INTERNAL: Helper for setting auto-properties that may have non-public setters.
/// Tries the property setter first, then falls back to the compiler backing field (e.g., "&lt;PropName&gt;k__BackingField").
/// </summary>
internal static class AutoPropertySetter
{
private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

internal static bool TrySet(object target, string propertyName, object value)
{
if (target == null)
return false;
if (string.IsNullOrWhiteSpace(propertyName))
return false;

var type = target.GetType();

try
{
var property = type.GetProperty(propertyName, InstanceFlags);
var setter = property?.GetSetMethod(nonPublic: true);
if (setter != null)
{
try
{
setter.Invoke(target, new[] { value });
return true;
}
catch
{
// Fallback below
}
}
}
catch
{
// ignored; fallback below
}

try
{
return ReflectionUtils.TrySetFieldOrProperty(target, $"<{propertyName}>k__BackingField", value);
}
catch
{
return false;
}
}
}
}

57 changes: 57 additions & 0 deletions S1API/Items/AdditiveDefinition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#if (IL2CPPMELON)
using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using S1ItemFramework = ScheduleOne.ItemFramework;
#endif

using UnityEngine;

namespace S1API.Items
{
/// <summary>
/// Represents an additive item definition.
/// Extends <see cref="StorableItemDefinition"/> with additive-specific properties.
/// </summary>
/// <remarks>
/// Builder-only: these properties are intentionally read-only to avoid runtime surprises from mutating
/// globally-registered ScriptableObject definitions mid-session. Use <see cref="AdditiveItemCreator"/> to create
/// additives with configured effects.
/// </remarks>
public sealed class AdditiveDefinition : StorableItemDefinition
{
/// <summary>
/// INTERNAL: Wraps an existing native additive definition.
/// </summary>
internal AdditiveDefinition(S1ItemFramework.AdditiveDefinition definition)
: base(definition)
{
S1AdditiveDefinition = definition;
}

/// <summary>
/// INTERNAL: A reference to the native game additive definition.
/// </summary>
internal S1ItemFramework.AdditiveDefinition S1AdditiveDefinition { get; }

/// <summary>
/// Display material used for the additive (if applicable).
/// </summary>
public Material DisplayMaterial => S1AdditiveDefinition.DisplayMaterial;

/// <summary>
/// Quality modifier applied by this additive.
/// </summary>
public float QualityChange => S1AdditiveDefinition.QualityChange;

/// <summary>
/// Yield multiplier applied by this additive.
/// </summary>
public float YieldMultiplier => S1AdditiveDefinition.YieldMultiplier;

/// <summary>
/// Instant growth fraction applied by this additive (0..1).
/// </summary>
public float InstantGrowth => S1AdditiveDefinition.InstantGrowth;
}
}

276 changes: 276 additions & 0 deletions S1API/Items/AdditiveDefinitionBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
#if (IL2CPPMELON)
using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
using S1Registry = Il2CppScheduleOne.Registry;
using S1Storage = Il2CppScheduleOne.Storage;
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using S1ItemFramework = ScheduleOne.ItemFramework;
using S1Registry = ScheduleOne.Registry;
using S1Storage = ScheduleOne.Storage;
#endif

using System;
using S1API.Internal.Utils;
using S1API.Logging;
using UnityEngine;
using Object = UnityEngine.Object;

namespace S1API.Items
{
/// <summary>
/// Builder for composing additive definitions at runtime.
/// Use fluent methods to configure additive properties before calling <see cref="Build"/>.
/// </summary>
public sealed class AdditiveDefinitionBuilder
{
private static readonly Log Logger = new Log("AdditiveDefinitionBuilder");

private readonly S1ItemFramework.AdditiveDefinition _definition;
private readonly GameObject _storedItemPlaceholder;

/// <summary>
/// INTERNAL: Creates a new builder instance with a fresh AdditiveDefinition.
/// Only <see cref="AdditiveItemCreator"/> can instantiate this.
/// </summary>
internal AdditiveDefinitionBuilder()
{
_definition = ScriptableObject.CreateInstance<S1ItemFramework.AdditiveDefinition>();

// Defaults align with other builders; users are expected to call WithBasicInfo before Build().
_definition.StackLimit = 10;
_definition.BasePurchasePrice = 10f;
_definition.ResellMultiplier = 0.5f;
_definition.Category = S1ItemFramework.EItemCategory.Agriculture;
_definition.legalStatus = S1ItemFramework.ELegalStatus.Legal;
_definition.AvailableInDemo = true;
_definition.UsableInFilters = true;
_definition.LabelDisplayColor = Color.white;

// 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<S1Storage.StoredItem>();
_definition.StoredItem = storedItemComponent;
}

/// <summary>
/// INTERNAL: Creates a builder instance initialized by cloning an existing additive.
/// </summary>
internal AdditiveDefinitionBuilder(S1ItemFramework.AdditiveDefinition source)
{
_definition = ScriptableObject.CreateInstance<S1ItemFramework.AdditiveDefinition>();

// Placeholder to keep parity with other builders; overridden by copy if source has StoredItem.
_storedItemPlaceholder = new GameObject("S1API_DefaultStoredItem");
_storedItemPlaceholder.SetActive(false);
_storedItemPlaceholder.hideFlags = HideFlags.HideAndDontSave;
Object.DontDestroyOnLoad(_storedItemPlaceholder);
var storedItemComponent = _storedItemPlaceholder.AddComponent<S1Storage.StoredItem>();
_definition.StoredItem = storedItemComponent;

CopyPropertiesFrom(source);
}

private void CopyPropertiesFrom(S1ItemFramework.AdditiveDefinition 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.Keywords = source.Keywords;
_definition.AvailableInDemo = source.AvailableInDemo;
_definition.UsableInFilters = source.UsableInFilters;
_definition.LabelDisplayColor = source.LabelDisplayColor;
_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;

// AdditiveDefinition properties (auto-properties with private set in Mono)
AutoPropertySetter.TrySet(_definition, nameof(S1ItemFramework.AdditiveDefinition.DisplayMaterial), source.DisplayMaterial);
AutoPropertySetter.TrySet(_definition, nameof(S1ItemFramework.AdditiveDefinition.QualityChange), source.QualityChange);
AutoPropertySetter.TrySet(_definition, nameof(S1ItemFramework.AdditiveDefinition.YieldMultiplier), source.YieldMultiplier);
AutoPropertySetter.TrySet(_definition, nameof(S1ItemFramework.AdditiveDefinition.InstantGrowth), source.InstantGrowth);
}

/// <summary>
/// Sets the basic information for the additive.
/// </summary>
public AdditiveDefinitionBuilder WithBasicInfo(string id, string name, string description, ItemCategory category)
{
_definition.ID = id;
_definition.Name = name;
_definition.Description = description;
_definition.Category = (S1ItemFramework.EItemCategory)category;

var displayName = string.IsNullOrEmpty(name) ? id : name;
if (!string.IsNullOrEmpty(displayName))
{
_definition.name = displayName;
if (_storedItemPlaceholder != null)
{
_storedItemPlaceholder.name = $"{displayName}_StoredItem";
}
}

return this;
}

/// <summary>
/// Sets the maximum stack size for this additive.
/// </summary>
public AdditiveDefinitionBuilder WithStackLimit(int limit)
{
_definition.StackLimit = Mathf.Clamp(limit, 1, 999);
return this;
}

/// <summary>
/// Sets the icon sprite displayed for this additive in UI.
/// </summary>
public AdditiveDefinitionBuilder WithIcon(Sprite icon)
{
_definition.Icon = icon;
return this;
}

/// <summary>
/// Configures the economic properties of the additive.
/// </summary>
public AdditiveDefinitionBuilder WithPricing(float basePurchasePrice, float resellMultiplier = 0.5f)
{
_definition.BasePurchasePrice = Mathf.Max(0f, basePurchasePrice);
_definition.ResellMultiplier = Mathf.Clamp01(resellMultiplier);
return this;
}

/// <summary>
/// Sets the legal status of the additive.
/// </summary>
public AdditiveDefinitionBuilder WithLegalStatus(LegalStatus status)
{
_definition.legalStatus = (S1ItemFramework.ELegalStatus)status;
return this;
}

/// <summary>
/// Sets the color of the label displayed in UI.
/// </summary>
public AdditiveDefinitionBuilder WithLabelColor(Color color)
{
_definition.LabelDisplayColor = color;
return this;
}

/// <summary>
/// Sets keywords used for filtering and searching this additive.
/// </summary>
public AdditiveDefinitionBuilder WithKeywords(params string[] keywords)
{
_definition.Keywords = keywords;
return this;
}

/// <summary>
/// Sets whether this additive is available in the demo version of the game.
/// </summary>
public AdditiveDefinitionBuilder WithDemoAvailability(bool available)
{
_definition.AvailableInDemo = available;
return this;
}

/// <summary>
/// Sets the display material for this additive.
/// </summary>
public AdditiveDefinitionBuilder WithDisplayMaterial(Material material)
{
if (!AutoPropertySetter.TrySet(_definition, nameof(S1ItemFramework.AdditiveDefinition.DisplayMaterial), material))
{
Logger.Warning($"Failed to set DisplayMaterial on AdditiveDefinition '{_definition.ID ?? "<no id>"}'.");
}
return this;
}

/// <summary>
/// Sets the effect values for this additive.
/// </summary>
public AdditiveDefinitionBuilder WithEffects(float yieldMultiplier, float instantGrowth, float qualityChange)
{
if (!AutoPropertySetter.TrySet(_definition, nameof(S1ItemFramework.AdditiveDefinition.YieldMultiplier), yieldMultiplier))
{
Logger.Warning($"Failed to set YieldMultiplier on AdditiveDefinition '{_definition.ID ?? "<no id>"}'.");
}
if (!AutoPropertySetter.TrySet(_definition, nameof(S1ItemFramework.AdditiveDefinition.InstantGrowth), instantGrowth))
{
Logger.Warning($"Failed to set InstantGrowth on AdditiveDefinition '{_definition.ID ?? "<no id>"}'.");
}
if (!AutoPropertySetter.TrySet(_definition, nameof(S1ItemFramework.AdditiveDefinition.QualityChange), qualityChange))
{
Logger.Warning($"Failed to set QualityChange on AdditiveDefinition '{_definition.ID ?? "<no id>"}'.");
}
return this;
}

/// <summary>
/// Builds the additive definition, registers it with the game's registry, and returns a wrapper.
/// </summary>
public AdditiveDefinition Build()
{
if (string.IsNullOrWhiteSpace(_definition.ID))
{
throw new InvalidOperationException("AdditiveDefinitionBuilder requires WithBasicInfo(...) to be called before Build().");
}

if (!string.IsNullOrEmpty(_definition.Name) && _storedItemPlaceholder != null)
{
_storedItemPlaceholder.name = $"{_definition.Name}_StoredItem";
}

S1Registry registry;
try
{
registry = S1Registry.Instance;
}
catch (Exception ex)
{
throw new InvalidOperationException(
"ScheduleOne.Registry is not available yet. Register additives during S1API.Lifecycle.GameLifecycle.OnPreLoad.",
ex);
}

if (registry == null)
{
throw new InvalidOperationException(
"ScheduleOne.Registry is not available yet. Register additives during S1API.Lifecycle.GameLifecycle.OnPreLoad.");
}

registry.AddToRegistry(_definition);
return new AdditiveDefinition(_definition);
}

/// <summary>
/// INTERNAL: Builds and returns the raw game additive definition without registering.
/// Used internally by S1API. Modders should use <see cref="Build"/> instead.
/// </summary>
internal S1ItemFramework.AdditiveDefinition BuildInternal()
{
return _definition;
}
}
}
Loading
Loading