growing: add GrowContainer allowed additives registry + injection patch#41
Merged
ifBars merged 1 commit intoifBars:stablefrom Jan 28, 2026
Conversation
Author
|
RuntimeGrowContainerAdditivesSmokeTestCommand.cs using System;
using System.Collections;
using System.Collections.Generic;
using MelonLoader;
using S1API.Console;
using S1API.Growing;
using S1API.Internal.Utils;
using S1API.Internal.Patches;
using S1API.Items;
using S1API.Logging;
using UnityEngine;
using UnityEngine.SceneManagement;
#if (IL2CPPMELON)
using S1Growing = Il2CppScheduleOne.Growing;
using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
using S1Registry = Il2CppScheduleOne.Registry;
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using S1Growing = ScheduleOne.Growing;
using S1ItemFramework = ScheduleOne.ItemFramework;
using S1Registry = ScheduleOne.Registry;
#endif
namespace S1API.Internal.DevLocal
{
/// <summary>
/// DEV-LOCAL: Console command to smoke-test GrowContainer allowed additive injection.
/// This file is intended to stay out of PR diffs.
/// </summary>
internal sealed class RuntimeGrowContainerAdditivesSmokeTestCommand : BaseConsoleCommand
{
private static readonly Log Logger = new Log("S1API.DevGrowContainerAdditivesSmokeTest");
private static bool _armed;
private static bool _running;
public override string CommandWord => "s1api_dev_growcontainer_additives_smoke_test";
public override string CommandDescription => "Runs a smoke test for GrowContainerAdditives. Requires an explicit mode: 'runtime' or 'basegame <additiveId>'. Use 'arm' to run once after the Main scene loads (waits for a GrowContainer).";
public override string ExampleUsage => "s1api_dev_growcontainer_additives_smoke_test [arm] (runtime | basegame <additiveId>)";
public override void ExecuteCommand(List<string> args)
{
var parsed = ParseArgs(args);
if (parsed == null)
{
Logger.Warning("[GrowContainerAdditivesSmokeTest] Missing required args.");
Logger.Warning("[GrowContainerAdditivesSmokeTest] Examples:");
Logger.Warning("[GrowContainerAdditivesSmokeTest] - s1api_dev_growcontainer_additives_smoke_test runtime");
Logger.Warning("[GrowContainerAdditivesSmokeTest] - s1api_dev_growcontainer_additives_smoke_test basegame pgr");
Logger.Warning("[GrowContainerAdditivesSmokeTest] - s1api_dev_growcontainer_additives_smoke_test arm runtime");
Logger.Warning("[GrowContainerAdditivesSmokeTest] - s1api_dev_growcontainer_additives_smoke_test arm basegame pgr");
return;
}
if (parsed.Value.Arm)
{
Arm(parsed.Value.Mode, parsed.Value.AdditiveId);
return;
}
RunSmokeTest("Manual", parsed.Value.Mode, parsed.Value.AdditiveId);
}
private static void Arm(Mode mode, string additiveId)
{
if (_armed)
{
Logger.Msg("[GrowContainerAdditivesSmokeTest] Already armed. Trigger a scene load to run once after Main is loaded.");
return;
}
_armed = true;
if (_running)
return;
_running = true;
MelonCoroutines.Start(WaitForMainAndGrowContainerThenRun(mode, additiveId));
Logger.Msg("[GrowContainerAdditivesSmokeTest] Armed. Smoke test will run once after Main is active and a GrowContainer exists.");
}
private static IEnumerator WaitForMainAndGrowContainerThenRun(Mode mode, string additiveId)
{
try
{
// One-shot: reset arm state as soon as the coroutine starts.
_armed = false;
var start = Time.realtimeSinceStartup;
const float timeoutSeconds = 30f;
// Wait until Main is the active scene (some mods/loader setups can call this command earlier).
while (Time.realtimeSinceStartup - start < timeoutSeconds)
{
try
{
if (string.Equals(SceneManager.GetActiveScene().name, "Main", StringComparison.OrdinalIgnoreCase))
break;
}
catch
{
// ignore
}
yield return null;
}
// Give the scene a moment to instantiate objects.
yield return null;
yield return null;
S1Growing.GrowContainer container = null;
while (Time.realtimeSinceStartup - start < timeoutSeconds)
{
container = UnityEngine.Object.FindObjectOfType<S1Growing.GrowContainer>();
if (container != null)
break;
yield return null;
}
if (container == null)
{
Logger.Warning("[GrowContainerAdditivesSmokeTest] SKIP (Armed): No GrowContainer found in Main within timeout.");
yield break;
}
RunSmokeTest("Armed", mode, additiveId, containerOverride: container);
}
finally
{
_running = false;
}
}
private static void RunSmokeTest(string tag, Mode mode, string additiveId, S1Growing.GrowContainer containerOverride = null)
{
Logger.Msg($"[GrowContainerAdditivesSmokeTest] START ({tag})");
try
{
var container = containerOverride ?? UnityEngine.Object.FindObjectOfType<S1Growing.GrowContainer>();
if (container == null)
{
Logger.Warning("[GrowContainerAdditivesSmokeTest] SKIP: No GrowContainer found in scene to validate AllowedAdditives.");
return;
}
var resolvedId = ResolveAdditiveIdOrThrow(mode, additiveId);
GrowContainerAdditives.AllowAdditive(resolvedId);
// Call the internal injector method from the patch to validate the core logic without relying on timing.
GrowContainerPatches.ApplyRegisteredAllowedAdditives(container);
if (!ContainsAllowed(container, resolvedId))
throw new Exception($"GrowContainer.AllowedAdditives did not contain '{resolvedId}' after injection.");
var countBefore = GetAllowedCount(container);
GrowContainerPatches.ApplyRegisteredAllowedAdditives(container);
var countAfter = GetAllowedCount(container);
if (countAfter != countBefore)
throw new Exception($"Duplicate injection detected. Count changed from {countBefore} to {countAfter}.");
Logger.Msg($"[GrowContainerAdditivesSmokeTest] OK: AllowedAdditives contains '{resolvedId}' (count={countAfter}).");
Logger.Msg($"[GrowContainerAdditivesSmokeTest] PASS ({tag})");
}
catch (Exception ex)
{
Logger.Error($"[GrowContainerAdditivesSmokeTest] FAIL ({tag}): {ex.Message}\n{ex.StackTrace}");
}
}
private static string ResolveAdditiveIdOrThrow(Mode mode, string additiveId)
{
switch (mode)
{
case Mode.Runtime:
{
var suffix = DateTime.UtcNow.Ticks.ToString("x");
var id = $"s1api_dev_test_allowed_additive_{suffix}";
AdditiveItemCreator.CreateBuilder()
.WithBasicInfo(
id: id,
name: $"S1API Dev Allowed Additive {suffix}",
description: "DEV-LOCAL additive for GrowContainerAdditives smoke test.",
category: ItemCategory.Growing
)
.WithEffects(1.0f, 0.0f, 0.0f)
.Build();
return id;
}
case Mode.BaseGame:
{
var id = (additiveId ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(id))
throw new ArgumentException("Base-game additive ID is required for 'basegame' mode.", nameof(additiveId));
S1ItemFramework.ItemDefinition def;
try { def = S1Registry.GetItem(id); }
catch (Exception ex)
{
throw new InvalidOperationException(
"ScheduleOne.Registry is not available yet. Run this command after Main is loaded (or use 'arm').",
ex);
}
if (def == null)
throw new InvalidOperationException($"Base-game additive '{id}' was not found in the registry.");
if (!CrossType.Is(def, out S1ItemFramework.AdditiveDefinition _))
throw new InvalidOperationException($"Item '{id}' is not an AdditiveDefinition.");
return id;
}
default:
throw new InvalidOperationException($"Unknown mode '{mode}'.");
}
}
private static ParsedArgs? ParseArgs(List<string> args)
{
if (args == null || args.Count == 0)
return null;
var a0 = (args[0] ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(a0))
return null;
var arm = false;
var idx = 0;
if (string.Equals(a0, "arm", StringComparison.OrdinalIgnoreCase))
{
arm = true;
idx = 1;
}
if (idx >= args.Count)
return null;
var modeRaw = (args[idx] ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(modeRaw))
return null;
if (string.Equals(modeRaw, "runtime", StringComparison.OrdinalIgnoreCase))
{
return new ParsedArgs(arm, Mode.Runtime, string.Empty);
}
if (string.Equals(modeRaw, "basegame", StringComparison.OrdinalIgnoreCase))
{
if (idx + 1 >= args.Count)
return null;
var id = (args[idx + 1] ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(id))
return null;
return new ParsedArgs(arm, Mode.BaseGame, id);
}
return null;
}
private static bool ContainsAllowed(S1Growing.GrowContainer container, string additiveId)
{
try
{
var arr = container.AllowedAdditives;
if (arr == null)
return false;
var len = GetAllowedCount(container);
for (int i = 0; i < len; i++)
{
var def = arr[i];
if (def != null && string.Equals(def.ID, additiveId, StringComparison.OrdinalIgnoreCase))
return true;
}
}
catch
{
// ignore
}
return false;
}
private static int GetAllowedCount(S1Growing.GrowContainer container)
{
try
{
var arr = container.AllowedAdditives;
return arr?.Length ?? 0;
}
catch
{
return 0;
}
}
private enum Mode
{
Runtime,
BaseGame
}
private readonly struct ParsedArgs
{
public ParsedArgs(bool arm, Mode mode, string additiveId)
{
Arm = arm;
Mode = mode;
AdditiveId = additiveId;
}
public bool Arm { get; }
public Mode Mode { get; }
public string AdditiveId { get; }
}
}
} |
ifBars
approved these changes
Jan 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds S1API support for allowing additional additives on grow containers at runtime, without each mod needing to patch
GrowContainer.InitializeGridItem.It introduces a small global registry of additive item IDs and a centralized Harmony patch that injects the corresponding
AdditiveDefinitionentries intoGrowContainer.AllowedAdditivesduring container initialization.What’s included
S1API.Growing.GrowContainerAdditivesAllowAdditive(string additiveItemId)(idempotent)GetAllowedAdditiveIds()ScheduleOne.Growing.GrowContainer.InitializeGridItem(ItemInstance, Grid, Vector2, int, string)AdditiveDefinitionentries intoGrowContainer.AllowedAdditivesIl2CppReferenceArray<AdditiveDefinition>)S1API/docs/items.mdUsage (recommended)
Register allowed additives during
GameLifecycle.OnPreLoad:Implementation
GrowContainerinstances).Testing
Find the smoke-test console command source file in the first comment under this PR!
s1api_dev_growcontainer_additives_smoke_test runtimes1api_dev_growcontainer_additives_smoke_test basegame <additiveId>(example:pgr)s1api_dev_growcontainer_additives_smoke_test arm runtimes1api_dev_growcontainer_additives_smoke_test arm basegame <additiveId>GrowContainer.AllowedAdditives, and validates idempotency.Registry, allows it, injects it intoGrowContainer.AllowedAdditives, and validates idempotency.GrowContainer(skips if none appear) then runs the same validations once.Notes / rationale
InitializeGridItem) and fighting over ordering.Registryon scene changes; therefore missing additive IDs are handled defensively (warn+skip) rather than breaking container initialization.