From 29748689bc553980b9a6af8ceeb0c0e066bec1c8 Mon Sep 17 00:00:00 2001 From: k0 <21180271+k073l@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:13:10 +0200 Subject: [PATCH 1/2] feat(recipe): add temperature and custom quality calculation method --- .../Patches/ChemistryStationPatches.cs | 29 +++++++++++ S1API/Stations/ChemistryStationRecipe.cs | 50 ++++++++++++++++++- .../Stations/ChemistryStationRecipeBuilder.cs | 34 ++++++++++++- S1API/Stations/QualityCalculationMethod.cs | 21 ++++++++ 4 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 S1API/Stations/QualityCalculationMethod.cs diff --git a/S1API/Internal/Patches/ChemistryStationPatches.cs b/S1API/Internal/Patches/ChemistryStationPatches.cs index 6fefbdf..935c192 100644 --- a/S1API/Internal/Patches/ChemistryStationPatches.cs +++ b/S1API/Internal/Patches/ChemistryStationPatches.cs @@ -1,8 +1,10 @@ #if (IL2CPPMELON) +using S1ItemFramework = Il2CppScheduleOne.ItemFramework; using S1ObjectScripts = Il2CppScheduleOne.ObjectScripts; using S1StationFramework = Il2CppScheduleOne.StationFramework; using S1UIStations = Il2CppScheduleOne.UI.Stations; #elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) +using S1ItemFramework = ScheduleOne.ItemFramework; using S1ObjectScripts = ScheduleOne.ObjectScripts; using S1StationFramework = ScheduleOne.StationFramework; using S1UIStations = ScheduleOne.UI.Stations; @@ -10,9 +12,11 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using HarmonyLib; using S1API.Internal.Utils; +using S1API.Items; using S1API.Logging; using S1API.Stations; using UnityEngine; @@ -269,5 +273,30 @@ private static void EnsureRecipeEntries(S1UIStations.ChemistryStationCanvas canv return null; } + + [HarmonyPatch(typeof(S1StationFramework.StationRecipe), "CalculateQuality")] + [HarmonyPrefix] + private static bool UseCustomCalcMethods(S1StationFramework.StationRecipe __instance, + ref S1ItemFramework.EQuality __result) + { + // Exit early out of patch if instance is null + if (__instance == null) return true; + var currentAddedRecipe = + ChemistryStationRecipes.GetAll().FirstOrDefault(r => r.RecipeID == __instance.RecipeID); + // If this recipe is not one of ours, exit early from patch + if (currentAddedRecipe == null) return true; + // Use default quality calculation for non-absolute methods + if (currentAddedRecipe.QualityCalculationMethod != QualityCalculationMethod.Absolute) return true; + var product = currentAddedRecipe.Product.ItemId; + var itemDefinition = ItemManager.GetItemDefinition(product); + if (itemDefinition is not QualityItemDefinition qualityItemDefinition) + { + Logger.Warning($"[S1API] Absolute quality calculation method specified for recipe '{currentAddedRecipe.RecipeID}' but product '{product}' is not a quality item. Falling back to default calculation."); + return true; + } + + __result = (S1ItemFramework.EQuality)qualityItemDefinition.DefaultQuality; + return false; + } } } diff --git a/S1API/Stations/ChemistryStationRecipe.cs b/S1API/Stations/ChemistryStationRecipe.cs index 3ab7e9c..7940055 100644 --- a/S1API/Stations/ChemistryStationRecipe.cs +++ b/S1API/Stations/ChemistryStationRecipe.cs @@ -23,17 +23,21 @@ internal ChemistryStationRecipe( string recipeId, string title, int cookTimeMinutes, + ChemistryStationRecipeTemperature temperature, Color finalLiquidColor, ChemistryStationRecipeProduct product, - IReadOnlyList ingredients) + IReadOnlyList ingredients, + QualityCalculationMethod qualityCalculationMethod) { S1StationRecipe = stationRecipe; RecipeID = recipeId; Title = title; CookTimeMinutes = cookTimeMinutes; + Temperature = temperature; FinalLiquidColor = finalLiquidColor; Product = product; Ingredients = ingredients; + QualityCalculationMethod = qualityCalculationMethod; } /// @@ -50,6 +54,11 @@ internal ChemistryStationRecipe( /// Cook time in minutes. /// public int CookTimeMinutes { get; } + + /// + /// Temperatures required by the recipe. + /// + public ChemistryStationRecipeTemperature Temperature { get; } /// /// UI liquid color for the final product. @@ -66,6 +75,11 @@ internal ChemistryStationRecipe( /// Each group can have multiple acceptable item IDs (variants). /// public IReadOnlyList Ingredients { get; } + + /// + /// Calculation method used when determining resulting product's quality. + /// + public QualityCalculationMethod QualityCalculationMethod { get; } /// /// Returns the native product item definition. @@ -116,4 +130,38 @@ internal ChemistryStationRecipeIngredient(IReadOnlyList itemIds, int qua /// public int Quantity { get; } } + + /// + /// Temperature ranges for the recipe, used mainly in the cooking minigame. + /// + public sealed class ChemistryStationRecipeTemperature + { + internal ChemistryStationRecipeTemperature(float cookTemperature, float tolerance) + { + CookTemperature = cookTemperature; + CookTemperatureTolerance = tolerance; + } + + /// + /// Target cook temperature. + /// + public float CookTemperature { get; } + + /// + /// Tolerance for cook temperature. + /// + public float CookTemperatureTolerance { get; } + + /// + /// Lower bound of the acceptable cook temperature range (inclusive). + /// Used in the minigame - lower values will not start the recipe. + /// + public float CookTemperatureLowerBound => CookTemperature - CookTemperatureTolerance; + + /// + /// Upper bound of the acceptable cook temperature range (inclusive). + /// Used in the minigame - higher values will cause the recipe to fail. + /// + public float CookTemperatureUpperBound => CookTemperature + CookTemperatureTolerance; + } } diff --git a/S1API/Stations/ChemistryStationRecipeBuilder.cs b/S1API/Stations/ChemistryStationRecipeBuilder.cs index 45d1619..b15ec86 100644 --- a/S1API/Stations/ChemistryStationRecipeBuilder.cs +++ b/S1API/Stations/ChemistryStationRecipeBuilder.cs @@ -22,11 +22,14 @@ public sealed class ChemistryStationRecipeBuilder { private string? _title; private int _cookTimeMinutes = 180; + private float _cookTemperature = 250f; + private float _cookTemperatureTolerance = 25f; private Color _finalLiquidColor = Color.white; private string? _productItemId; private int _productQuantity = 1; private S1ItemFramework.ItemDefinition? _productItem; + private QualityCalculationMethod _method = QualityCalculationMethod.Additive; private readonly List _ingredients = new List(); @@ -127,6 +130,30 @@ public ChemistryStationRecipeBuilder WithProduct(string itemId, int quantity) return this; } + /// + /// Sets the quality calculation method. + /// + public ChemistryStationRecipeBuilder WithCalculationMethod(QualityCalculationMethod method) + { + _method = method; + return this; + } + + /// + /// Sets the cook temperature and tolerance. + /// + public ChemistryStationRecipeBuilder WithTemperature(float cookTemperature, float tolerance) + { + if (cookTemperature <= 0) + throw new ArgumentOutOfRangeException(nameof(cookTemperature), "Cook temperature must be > 0."); + if (tolerance < 0) + throw new ArgumentOutOfRangeException(nameof(tolerance), "Cook temperature tolerance cannot be negative."); + + _cookTemperature = cookTemperature; + _cookTemperatureTolerance = tolerance; + return this; + } + /// /// Builds and auto-registers the recipe with S1API. /// @@ -147,9 +174,11 @@ public ChemistryStationRecipe Build() recipeId: recipeId, title: title, cookTimeMinutes: _cookTimeMinutes, + temperature: new ChemistryStationRecipeTemperature(_cookTemperature, _cookTemperatureTolerance), finalLiquidColor: _finalLiquidColor, product: new ChemistryStationRecipeProduct(_productItemId!, _productQuantity), - ingredients: BuildIngredientWrappers()); + ingredients: BuildIngredientWrappers(), + qualityCalculationMethod: _method); return ChemistryStationRecipes.Register(wrapper); } @@ -167,7 +196,10 @@ internal S1StationFramework.StationRecipe BuildInternal() recipe.Unlocked = true; recipe.RecipeTitle = string.IsNullOrWhiteSpace(_title) ? _productItemId! : _title!; recipe.CookTime_Mins = _cookTimeMinutes; + recipe.CookTemperature = _cookTemperature; + recipe.CookTemperatureTolerance = _cookTemperatureTolerance; recipe.FinalLiquidColor = _finalLiquidColor; + // Other calculation methods do not exist in base game, so we implement them ourselves later recipe.QualityCalculationMethod = S1StationFramework.StationRecipe.EQualityCalculationMethod.Additive; recipe.Product = new S1StationFramework.StationRecipe.ItemQuantity diff --git a/S1API/Stations/QualityCalculationMethod.cs b/S1API/Stations/QualityCalculationMethod.cs new file mode 100644 index 0000000..c207025 --- /dev/null +++ b/S1API/Stations/QualityCalculationMethod.cs @@ -0,0 +1,21 @@ +namespace S1API.Stations +{ + /// + /// Calculation method to use when determining quality of + /// the result. + /// + public enum QualityCalculationMethod + { + /// + /// Quality is calculated by adding the quality contributions of each ingredient together. + /// + Additive, + /// + /// Quality is determined by the product's default quality. + /// + /// + /// Requires the result to be of type QualityItemInstance or its inheritor. + /// + Absolute + } +} \ No newline at end of file From c0c7bb2e951277e1c27c0adc6dd77b202cd8b9fc Mon Sep 17 00:00:00 2001 From: k0 <21180271+k073l@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:41:04 +0200 Subject: [PATCH 2/2] fix(recipe): safely access RecipeID in patch --- S1API/Internal/Patches/ChemistryStationPatches.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/S1API/Internal/Patches/ChemistryStationPatches.cs b/S1API/Internal/Patches/ChemistryStationPatches.cs index 935c192..773f9be 100644 --- a/S1API/Internal/Patches/ChemistryStationPatches.cs +++ b/S1API/Internal/Patches/ChemistryStationPatches.cs @@ -279,10 +279,13 @@ private static void EnsureRecipeEntries(S1UIStations.ChemistryStationCanvas canv private static bool UseCustomCalcMethods(S1StationFramework.StationRecipe __instance, ref S1ItemFramework.EQuality __result) { - // Exit early out of patch if instance is null + // Exit early out of patch if instance or recipeID is null if (__instance == null) return true; + string? instanceRecipeId; + try { instanceRecipeId = __instance.RecipeID; } catch { instanceRecipeId = null; } + if (string.IsNullOrWhiteSpace(instanceRecipeId)) return true; var currentAddedRecipe = - ChemistryStationRecipes.GetAll().FirstOrDefault(r => r.RecipeID == __instance.RecipeID); + ChemistryStationRecipes.GetAll().FirstOrDefault(r => instanceRecipeId == __instance.RecipeID); // If this recipe is not one of ours, exit early from patch if (currentAddedRecipe == null) return true; // Use default quality calculation for non-absolute methods