Skip to content

Commit 1289eaf

Browse files
authored
Merge pull request KaBooMa#42 from Khundiann/items/add-StationItem-support-for-station/minigame-ingredients
adds support for assigning a StationItem prefab to runtime/custom StorableItemDefinition items
2 parents a718c40 + 8de7f85 commit 1289eaf

3 files changed

Lines changed: 162 additions & 2 deletions

File tree

S1API/Items/StorableItemDefinition.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using S1ItemFramework = ScheduleOne.ItemFramework;
55
#endif
66

7+
using UnityEngine;
8+
79
namespace S1API.Items
810
{
911
/// <summary>
@@ -54,6 +56,21 @@ public float ResellMultiplier
5456
/// </summary>
5557
public bool IsUnlocked =>
5658
S1StorableItemDefinition.IsUnlocked;
59+
60+
/// <summary>
61+
/// Gets whether this item has a StationItem assigned (used by station/minigame tasks, e.g., Chemistry Station).
62+
/// </summary>
63+
public bool HasStationItem =>
64+
S1StorableItemDefinition.StationItem != null;
65+
66+
/// <summary>
67+
/// Gets the StationItem prefab GameObject for this item, if any.
68+
/// </summary>
69+
/// <remarks>
70+
/// This is primarily used for debugging and tooling. Prefer configuring StationItem via
71+
/// <see cref="StorableItemDefinitionBuilder.WithStationItem"/> during build/registration.
72+
/// </remarks>
73+
public GameObject? StationItemPrefab =>
74+
S1StorableItemDefinition.StationItem?.gameObject;
5775
}
5876
}
59-

S1API/Items/StorableItemDefinitionBuilder.cs

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
#if (IL2CPPMELON)
22
using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
33
using S1Registry = Il2CppScheduleOne.Registry;
4+
using S1StationFramework = Il2CppScheduleOne.StationFramework;
45
using S1Storage = Il2CppScheduleOne.Storage;
56
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
67
using S1ItemFramework = ScheduleOne.ItemFramework;
78
using S1Registry = ScheduleOne.Registry;
9+
using S1StationFramework = ScheduleOne.StationFramework;
810
using S1Storage = ScheduleOne.Storage;
911
#endif
1012

13+
using System;
14+
using System.Collections.Generic;
15+
using S1API.Logging;
1116
using UnityEngine;
1217
using Object = UnityEngine.Object;
1318

@@ -23,6 +28,12 @@ namespace S1API.Items
2328
/// </remarks>
2429
public sealed class StorableItemDefinitionBuilder
2530
{
31+
private static readonly Log Logger = new Log("StorableItemDefinitionBuilder");
32+
private static readonly object StationItemGate = new object();
33+
private static readonly Dictionary<int, S1StationFramework.StationItem> StationItemCache = new Dictionary<int, S1StationFramework.StationItem>();
34+
private static readonly HashSet<int> WarnedStationItemModuleMissing = new HashSet<int>();
35+
private static GameObject _stationItemRoot;
36+
2637
private readonly S1ItemFramework.StorableItemDefinition _definition;
2738
private readonly GameObject _storedItemPlaceholder;
2839
private bool _hasCustomStoredItem;
@@ -180,6 +191,43 @@ public StorableItemDefinitionBuilder WithStoredItem(GameObject storedItemPrefab)
180191
return this;
181192
}
182193

194+
/// <summary>
195+
/// Assigns a StationItem prefab to this item definition so it can be used as a station/minigame ingredient
196+
/// (e.g., Chemistry Station).
197+
/// </summary>
198+
/// <remarks>
199+
/// S1API clones and caches the prefab under a hidden <c>DontDestroyOnLoad</c> root by default.
200+
/// This avoids mutating shared prefabs and helps keep the reference stable across scene loads.
201+
/// </remarks>
202+
/// <param name="stationItemPrefab">A prefab GameObject that has a StationItem component.</param>
203+
/// <returns>The builder instance for fluent chaining.</returns>
204+
/// <exception cref="ArgumentNullException">Thrown if <paramref name="stationItemPrefab"/> is null.</exception>
205+
/// <exception cref="ArgumentException">Thrown if <paramref name="stationItemPrefab"/> does not have a StationItem component.</exception>
206+
public StorableItemDefinitionBuilder WithStationItem(GameObject stationItemPrefab)
207+
{
208+
if (stationItemPrefab == null)
209+
throw new ArgumentNullException(nameof(stationItemPrefab));
210+
211+
var stationItem = stationItemPrefab.GetComponent<S1StationFramework.StationItem>();
212+
if (stationItem == null)
213+
throw new ArgumentException("Station item prefab must have a StationItem component.", nameof(stationItemPrefab));
214+
215+
var cached = GetOrCreateStationItemPrefab(stationItem);
216+
_definition.StationItem = cached;
217+
218+
WarnIfStationItemMissingChemistryModules(cached);
219+
return this;
220+
}
221+
222+
/// <summary>
223+
/// Clears the StationItem reference for this definition.
224+
/// </summary>
225+
public StorableItemDefinitionBuilder WithoutStationItem()
226+
{
227+
_definition.StationItem = null;
228+
return this;
229+
}
230+
183231
/// <summary>
184232
/// Sets whether this item is available in the demo version of the game.
185233
/// </summary>
@@ -221,6 +269,82 @@ internal S1ItemFramework.StorableItemDefinition BuildInternal()
221269
{
222270
return _definition;
223271
}
272+
273+
private static S1StationFramework.StationItem GetOrCreateStationItemPrefab(S1StationFramework.StationItem stationItemPrefab)
274+
{
275+
var id = stationItemPrefab.GetInstanceID();
276+
277+
lock (StationItemGate)
278+
{
279+
if (StationItemCache.TryGetValue(id, out var cached) && cached != null)
280+
return cached;
281+
282+
var root = GetStationItemRoot();
283+
284+
// Clone + cache (final decision): keep a stable hidden prefab reference across scene loads.
285+
var clone = Object.Instantiate(stationItemPrefab, root.transform);
286+
clone.gameObject.hideFlags = HideFlags.HideAndDontSave;
287+
clone.name = $"{stationItemPrefab.name}_S1API_StationItem";
288+
289+
// Keep the cache far away from gameplay so it doesn't interfere with scenes.
290+
clone.transform.position = root.transform.position;
291+
292+
StationItemCache[id] = clone;
293+
return clone;
294+
}
295+
}
296+
297+
private static GameObject GetStationItemRoot()
298+
{
299+
if (_stationItemRoot != null)
300+
return _stationItemRoot;
301+
302+
lock (StationItemGate)
303+
{
304+
if (_stationItemRoot != null)
305+
return _stationItemRoot;
306+
307+
var root = new GameObject("S1API_StationItemCache");
308+
root.hideFlags = HideFlags.HideAndDontSave;
309+
Object.DontDestroyOnLoad(root);
310+
311+
// Place it far below the world; keep it active so instantiated prefabs remain active by default.
312+
root.transform.position = new Vector3(0f, -10000f, 0f);
313+
314+
_stationItemRoot = root;
315+
return root;
316+
}
317+
}
318+
319+
private static void WarnIfStationItemMissingChemistryModules(S1StationFramework.StationItem stationItemPrefab)
320+
{
321+
if (stationItemPrefab == null)
322+
return;
323+
324+
var id = stationItemPrefab.GetInstanceID();
325+
326+
lock (StationItemGate)
327+
{
328+
if (!WarnedStationItemModuleMissing.Add(id))
329+
return;
330+
}
331+
332+
try
333+
{
334+
var hasIngredientModule = stationItemPrefab.GetComponentInChildren<S1StationFramework.IngredientModule>(true) != null;
335+
var hasPourableModule = stationItemPrefab.GetComponentInChildren<S1StationFramework.PourableModule>(true) != null;
336+
337+
if (hasIngredientModule || hasPourableModule)
338+
return;
339+
340+
Logger.Warning(
341+
$"[S1API] StationItem prefab '{stationItemPrefab.name}' does not contain an IngredientModule or PourableModule. " +
342+
"Chemistry station tasks may log errors or skip this ingredient at runtime.");
343+
}
344+
catch
345+
{
346+
// best-effort warning only
347+
}
348+
}
224349
}
225350
}
226-

S1API/docs/stations.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,25 @@ public class MyMod : MelonMod
4545
}
4646
```
4747

48+
## Station Items for Custom Ingredients
49+
50+
Some station/minigame tasks (like Chemistry) spawn ingredient props by instantiating `StorableItemDefinition.StationItem`.
51+
If an ingredient item has no StationItem, the game will log errors and may skip that ingredient.
52+
53+
For runtime/custom items, set a StationItem prefab when you build the item:
54+
55+
```csharp
56+
using S1API.Items;
57+
58+
// A prefab GameObject that has a StationItem component (typically loaded from an AssetBundle)
59+
GameObject myIngredientStationItemPrefab = ...;
60+
61+
var ingredient = ItemCreator.CreateBuilder()
62+
.WithBasicInfo("mymod_custom_ingredient", "Custom Ingredient", "Used in stations.", ItemCategory.Consumable)
63+
.WithStationItem(myIngredientStationItemPrefab)
64+
.Build();
65+
```
66+
4867
## See Also
4968

5069
- <xref:S1API.Stations.ChemistryStationRecipeBuilder> - Chemistry Station recipe builder API

0 commit comments

Comments
 (0)