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
252 changes: 252 additions & 0 deletions S1API/Internal/Patches/ChemistryStationPatches.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
#if (IL2CPPMELON)
using S1ObjectScripts = Il2CppScheduleOne.ObjectScripts;
using S1StationFramework = Il2CppScheduleOne.StationFramework;
using S1UIStations = Il2CppScheduleOne.UI.Stations;
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using S1ObjectScripts = ScheduleOne.ObjectScripts;
using S1StationFramework = ScheduleOne.StationFramework;
using S1UIStations = ScheduleOne.UI.Stations;
#endif

using System;
using System.Collections.Generic;
using HarmonyLib;
using S1API.Internal.Utils;
using S1API.Logging;
using S1API.Stations;
using UnityEngine;

namespace S1API.Internal.Patches
{
/// <summary>
/// INTERNAL: Harmony patches to inject S1API-registered Chemistry Station recipes into the UI.
/// </summary>
[HarmonyPatch]
internal static class ChemistryStationPatches
{
private static readonly Log Logger = new Log("ChemistryStationPatches");
private static readonly HashSet<string> LoggedCanvasConflicts = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private static readonly HashSet<string> LoggedEntryConflicts = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

private static bool _loggedRecipeEntriesMissing;

private static bool TryGetRecipeEntriesList(
S1UIStations.ChemistryStationCanvas canvas,
#if (IL2CPPMELON || IL2CPPBEPINEX)
out Il2CppSystem.Collections.Generic.List<S1UIStations.StationRecipeEntry>? entries
#else
out List<S1UIStations.StationRecipeEntry>? entries
#endif
)
{
entries = null;
if (canvas == null)
return false;

try
{
var value = ReflectionUtils.TryGetFieldOrProperty(canvas, "recipeEntries");
entries =
#if (IL2CPPMELON || IL2CPPBEPINEX)
value as Il2CppSystem.Collections.Generic.List<S1UIStations.StationRecipeEntry>;
#else
value as List<S1UIStations.StationRecipeEntry>;
#endif
}
catch
{
entries = null;
}

return entries != null;
}

[HarmonyPatch(typeof(S1UIStations.ChemistryStationCanvas), "Awake")]
[HarmonyPrefix]
private static void AwakePrefix(S1UIStations.ChemistryStationCanvas __instance)
{
try
{
InjectRegisteredRecipes(__instance);
}
catch (Exception ex)
{
Logger.Warning($"[S1API] ChemistryStationCanvas.Awake inject failed: {ex.Message}");
}
}

[HarmonyPatch(typeof(S1UIStations.ChemistryStationCanvas), "Open")]
[HarmonyPrefix]
private static void OpenPrefix(S1UIStations.ChemistryStationCanvas __instance, S1ObjectScripts.ChemistryStation __0)
{
try
{
// Late registrations: ensure Recipes and recipeEntries are in sync before StationSlotsChanged runs.
InjectRegisteredRecipes(__instance);
EnsureRecipeEntries(__instance);
}
catch (Exception ex)
{
Logger.Warning($"[S1API] ChemistryStationCanvas.Open sync failed: {ex.Message}");
}
}

private static void InjectRegisteredRecipes(S1UIStations.ChemistryStationCanvas canvas)
{
if (canvas == null)
return;

var registered = ChemistryStationRecipes.GetAllNative();
if (registered == null || registered.Count == 0)
return;

var recipes = canvas.Recipes;
if (recipes == null)
return;

for (int i = 0; i < registered.Count; i++)
{
var custom = registered[i];
if (custom == null)
continue;

string? id;
try { id = custom.RecipeID; } catch { id = null; }
if (string.IsNullOrWhiteSpace(id))
continue;

if (ContainsRecipeId(recipes, id))
{
if (LoggedCanvasConflicts.Add(id))
Logger.Warning($"[S1API] Chemistry Station already has a recipe with ID '{id}'. Skipping S1API injection for this ID.");
continue;
}

recipes.Add(custom);
}
}

private static void EnsureRecipeEntries(S1UIStations.ChemistryStationCanvas canvas)
{
if (canvas == null)
return;

if (!TryGetRecipeEntriesList(canvas, out var entries) || entries == null)
{
if (!_loggedRecipeEntriesMissing)
{
_loggedRecipeEntriesMissing = true;
Logger.Warning("[S1API] ChemistryStationCanvas recipeEntries could not be resolved. Late recipe UI sync will be skipped.");
}

return;
}

var registered = ChemistryStationRecipes.GetAllNative();
if (registered == null || registered.Count == 0)
return;

for (int i = 0; i < registered.Count; i++)
{
var recipe = registered[i];
if (recipe == null)
continue;

string? id;
try { id = recipe.RecipeID; } catch { id = null; }
if (string.IsNullOrWhiteSpace(id))
continue;

if (ContainsEntryForRecipeId(entries, id))
{
if (LoggedEntryConflicts.Add(id))
Logger.Warning($"[S1API] Chemistry Station UI already has an entry for recipe ID '{id}'. Skipping entry injection for this ID.");
continue;
}

try
{
if (canvas.RecipeEntryPrefab == null || canvas.RecipeContainer == null)
return;

var entry = UnityEngine.Object.Instantiate(canvas.RecipeEntryPrefab, canvas.RecipeContainer);
if (entry == null)
continue;

entry.AssignRecipe(recipe);
entries.Add(entry);
}
catch (Exception ex)
{
Logger.Warning($"[S1API] Failed to create StationRecipeEntry for '{id}': {ex.Message}");
}
}
}

private static bool ContainsRecipeId(
#if (IL2CPPMELON || IL2CPPBEPINEX)
Il2CppSystem.Collections.Generic.List<S1StationFramework.StationRecipe> recipes,
#else
List<S1StationFramework.StationRecipe> recipes,
#endif
string recipeId)
{
if (recipes == null || string.IsNullOrWhiteSpace(recipeId))
return false;

for (int i = 0; i < recipes.Count; i++)
{
var r = recipes[i];
if (r == null)
continue;

try
{
if (string.Equals(r.RecipeID, recipeId, StringComparison.OrdinalIgnoreCase))
return true;
}
catch
{
// ignore and continue
}
}

return false;
}

private static bool ContainsEntryForRecipeId(
#if (IL2CPPMELON || IL2CPPBEPINEX)
Il2CppSystem.Collections.Generic.List<S1UIStations.StationRecipeEntry> entries,
#else
List<S1UIStations.StationRecipeEntry> entries,
#endif
string recipeId)
{
if (entries == null || string.IsNullOrWhiteSpace(recipeId))
return false;

for (int i = 0; i < entries.Count; i++)
{
var e = entries[i];
if (e == null)
continue;

try
{
var r = e.Recipe;
if (r == null)
continue;

if (string.Equals(r.RecipeID, recipeId, StringComparison.OrdinalIgnoreCase))
return true;
}
catch
{
// ignore and continue
}
}

return false;
}
}
}
119 changes: 119 additions & 0 deletions S1API/Stations/ChemistryStationRecipe.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#if (IL2CPPMELON)
using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
using S1StationFramework = Il2CppScheduleOne.StationFramework;
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using S1ItemFramework = ScheduleOne.ItemFramework;
using S1StationFramework = ScheduleOne.StationFramework;
#endif

using System.Collections.Generic;
using UnityEngine;

namespace S1API.Stations
{
/// <summary>
/// Read-only wrapper for a Chemistry Station recipe (<c>StationRecipe</c>).
/// </summary>
public sealed class ChemistryStationRecipe
{
internal S1StationFramework.StationRecipe S1StationRecipe { get; }

internal ChemistryStationRecipe(
S1StationFramework.StationRecipe stationRecipe,
string recipeId,
string title,
int cookTimeMinutes,
Color finalLiquidColor,
ChemistryStationRecipeProduct product,
IReadOnlyList<ChemistryStationRecipeIngredient> ingredients)
{
S1StationRecipe = stationRecipe;
RecipeID = recipeId;
Title = title;
CookTimeMinutes = cookTimeMinutes;
FinalLiquidColor = finalLiquidColor;
Product = product;
Ingredients = ingredients;
}

/// <summary>
/// Game-defined recipe identifier (<c>"{qty}x{productId}"</c>).
/// </summary>
public string RecipeID { get; }

/// <summary>
/// Display title shown in the Chemistry Station UI.
/// </summary>
public string Title { get; }

/// <summary>
/// Cook time in minutes.
/// </summary>
public int CookTimeMinutes { get; }

/// <summary>
/// UI liquid color for the final product.
/// </summary>
public Color FinalLiquidColor { get; }

/// <summary>
/// The product produced by this recipe.
/// </summary>
public ChemistryStationRecipeProduct Product { get; }

/// <summary>
/// Ingredient groups required by this recipe.
/// Each group can have multiple acceptable item IDs (variants).
/// </summary>
public IReadOnlyList<ChemistryStationRecipeIngredient> Ingredients { get; }

/// <summary>
/// Returns the native product item definition.
/// </summary>
public S1ItemFramework.ItemDefinition S1ProductItem => S1StationRecipe.Product?.Item;
}

/// <summary>
/// Product specification for a Chemistry Station recipe.
/// </summary>
public sealed class ChemistryStationRecipeProduct
{
internal ChemistryStationRecipeProduct(string itemId, int quantity)
{
ItemId = itemId;
Quantity = quantity;
}

/// <summary>
/// Product item ID.
/// </summary>
public string ItemId { get; }

/// <summary>
/// Product quantity.
/// </summary>
public int Quantity { get; }
}

/// <summary>
/// Ingredient group specification for a Chemistry Station recipe.
/// </summary>
public sealed class ChemistryStationRecipeIngredient
{
internal ChemistryStationRecipeIngredient(IReadOnlyList<string> itemIds, int quantity)
{
ItemIds = itemIds;
Quantity = quantity;
}

/// <summary>
/// Acceptable ingredient item IDs (variants).
/// </summary>
public IReadOnlyList<string> ItemIds { get; }

/// <summary>
/// Required quantity for this ingredient group.
/// </summary>
public int Quantity { get; }
}
}
Loading
Loading