commit 1374b2e929263849b9362bc2032e73ac19079563 Author: thehun927 Date: Mon May 18 18:43:51 2026 -0400 d diff --git a/AbsorbentSoil/AbsorbentSoil.csproj b/AbsorbentSoil/AbsorbentSoil.csproj new file mode 100644 index 0000000..6a380d0 --- /dev/null +++ b/AbsorbentSoil/AbsorbentSoil.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + AbsorbentSoil + AbsorbentSoil + latest + disable + /home/attila/.local/share/Steam/steamapps/common/Schedule I + + + + + + + + + + \ No newline at end of file diff --git a/AbsorbentSoil/bin/Release/net6.0/AbsorbentSoil.deps.json b/AbsorbentSoil/bin/Release/net6.0/AbsorbentSoil.deps.json new file mode 100644 index 0000000..074dff0 --- /dev/null +++ b/AbsorbentSoil/bin/Release/net6.0/AbsorbentSoil.deps.json @@ -0,0 +1,23 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v6.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v6.0": { + "AbsorbentSoil/1.0.0": { + "runtime": { + "AbsorbentSoil.dll": {} + } + } + } + }, + "libraries": { + "AbsorbentSoil/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/AbsorbentSoil/bin/Release/net6.0/AbsorbentSoil.pdb b/AbsorbentSoil/bin/Release/net6.0/AbsorbentSoil.pdb new file mode 100644 index 0000000..d181b8d Binary files /dev/null and b/AbsorbentSoil/bin/Release/net6.0/AbsorbentSoil.pdb differ diff --git a/AbsorbentSoil/obj/AbsorbentSoil.csproj.nuget.dgspec.json b/AbsorbentSoil/obj/AbsorbentSoil.csproj.nuget.dgspec.json new file mode 100644 index 0000000..870c537 --- /dev/null +++ b/AbsorbentSoil/obj/AbsorbentSoil.csproj.nuget.dgspec.json @@ -0,0 +1,77 @@ +{ + "format": 1, + "restore": { + "/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/AbsorbentSoil.csproj": {} + }, + "projects": { + "/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/AbsorbentSoil.csproj": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/AbsorbentSoil.csproj", + "projectName": "AbsorbentSoil", + "projectPath": "/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/AbsorbentSoil.csproj", + "packagesPath": "/home/attila/.nuget/packages/", + "outputPath": "/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/", + "projectStyle": "PackageReference", + "configFilePaths": [ + "/home/attila/.nuget/NuGet/NuGet.Config" + ], + "originalTargetFrameworks": [ + "net6.0" + ], + "sources": { + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net6.0": { + "targetAlias": "net6.0", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + }, + "SdkAnalysisLevel": "10.0.100" + }, + "frameworks": { + "net6.0": { + "targetAlias": "net6.0", + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "downloadDependencies": [ + { + "name": "Microsoft.AspNetCore.App.Ref", + "version": "[6.0.36, 6.0.36]" + }, + { + "name": "Microsoft.NETCore.App.Ref", + "version": "[6.0.36, 6.0.36]" + } + ], + "frameworkReferences": { + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "/usr/share/dotnet/sdk/10.0.104/RuntimeIdentifierGraph.json" + } + } + } + } +} \ No newline at end of file diff --git a/AbsorbentSoil/obj/AbsorbentSoil.csproj.nuget.g.props b/AbsorbentSoil/obj/AbsorbentSoil.csproj.nuget.g.props new file mode 100644 index 0000000..6caae6e --- /dev/null +++ b/AbsorbentSoil/obj/AbsorbentSoil.csproj.nuget.g.props @@ -0,0 +1,15 @@ + + + + True + NuGet + $(MSBuildThisFileDirectory)project.assets.json + /home/attila/.nuget/packages/ + /home/attila/.nuget/packages/ + PackageReference + 7.0.0 + + + + + \ No newline at end of file diff --git a/AbsorbentSoil/obj/AbsorbentSoil.csproj.nuget.g.targets b/AbsorbentSoil/obj/AbsorbentSoil.csproj.nuget.g.targets new file mode 100644 index 0000000..3dc06ef --- /dev/null +++ b/AbsorbentSoil/obj/AbsorbentSoil.csproj.nuget.g.targets @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AbsorbentSoil/obj/Release/net6.0/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs b/AbsorbentSoil/obj/Release/net6.0/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs new file mode 100644 index 0000000..ed92695 --- /dev/null +++ b/AbsorbentSoil/obj/Release/net6.0/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] diff --git a/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.AssemblyInfo.cs b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.AssemblyInfo.cs new file mode 100644 index 0000000..972d1a6 --- /dev/null +++ b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.AssemblyInfo.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("AbsorbentSoil")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] +[assembly: System.Reflection.AssemblyProductAttribute("AbsorbentSoil")] +[assembly: System.Reflection.AssemblyTitleAttribute("AbsorbentSoil")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + +// Generated by the MSBuild WriteCodeFragment class. + diff --git a/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.AssemblyInfoInputs.cache b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.AssemblyInfoInputs.cache new file mode 100644 index 0000000..9e8ac38 --- /dev/null +++ b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +54ce364b284ea9f7c7e326b3938578be61e76fb507bfb64c59c1c67e28888f74 diff --git a/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.GeneratedMSBuildEditorConfig.editorconfig b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 0000000..2f7a197 --- /dev/null +++ b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,17 @@ +is_global = true +build_property.TargetFramework = net6.0 +build_property.TargetFrameworkIdentifier = .NETCoreApp +build_property.TargetFrameworkVersion = v6.0 +build_property.TargetPlatformMinVersion = +build_property.UsingMicrosoftNETSdkWeb = +build_property.ProjectTypeGuids = +build_property.InvariantGlobalization = +build_property.PlatformNeutralAssembly = +build_property.EnforceExtendedAnalyzerRules = +build_property._SupportedPlatformList = Linux,macOS,Windows +build_property.RootNamespace = AbsorbentSoil +build_property.ProjectDir = /home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/ +build_property.EnableComHosting = +build_property.EnableGeneratedComInterfaceComImportInterop = +build_property.EffectiveAnalysisLevelStyle = 6.0 +build_property.EnableCodeStyleSeverity = diff --git a/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.assets.cache b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.assets.cache new file mode 100644 index 0000000..aeb394c Binary files /dev/null and b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.assets.cache differ diff --git a/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.csproj.AssemblyReference.cache b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.csproj.AssemblyReference.cache new file mode 100644 index 0000000..823168b Binary files /dev/null and b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.csproj.AssemblyReference.cache differ diff --git a/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.csproj.CoreCompileInputs.cache b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..62a182f --- /dev/null +++ b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +ddb36bf3fc2e77af19df7cc3d5e652bab5098b8451766fb4c9483694133ef288 diff --git a/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.csproj.FileListAbsolute.txt b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..96cd45d --- /dev/null +++ b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.csproj.FileListAbsolute.txt @@ -0,0 +1,12 @@ +/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/bin/Release/net6.0/AbsorbentSoil.deps.json +/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/bin/Release/net6.0/AbsorbentSoil.dll +/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/bin/Release/net6.0/AbsorbentSoil.pdb +/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.csproj.AssemblyReference.cache +/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.GeneratedMSBuildEditorConfig.editorconfig +/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.AssemblyInfoInputs.cache +/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.AssemblyInfo.cs +/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.csproj.CoreCompileInputs.cache +/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.dll +/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/refint/AbsorbentSoil.dll +/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.pdb +/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/ref/AbsorbentSoil.dll diff --git a/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.dll b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.dll new file mode 100644 index 0000000..a199e3e Binary files /dev/null and b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.dll differ diff --git a/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.pdb b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.pdb new file mode 100644 index 0000000..d181b8d Binary files /dev/null and b/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.pdb differ diff --git a/AbsorbentSoil/obj/Release/net6.0/ref/AbsorbentSoil.dll b/AbsorbentSoil/obj/Release/net6.0/ref/AbsorbentSoil.dll new file mode 100644 index 0000000..bf946c7 Binary files /dev/null and b/AbsorbentSoil/obj/Release/net6.0/ref/AbsorbentSoil.dll differ diff --git a/AbsorbentSoil/obj/Release/net6.0/refint/AbsorbentSoil.dll b/AbsorbentSoil/obj/Release/net6.0/refint/AbsorbentSoil.dll new file mode 100644 index 0000000..bf946c7 Binary files /dev/null and b/AbsorbentSoil/obj/Release/net6.0/refint/AbsorbentSoil.dll differ diff --git a/AbsorbentSoil/obj/project.assets.json b/AbsorbentSoil/obj/project.assets.json new file mode 100644 index 0000000..fc3fd94 --- /dev/null +++ b/AbsorbentSoil/obj/project.assets.json @@ -0,0 +1,82 @@ +{ + "version": 3, + "targets": { + "net6.0": {} + }, + "libraries": {}, + "projectFileDependencyGroups": { + "net6.0": [] + }, + "packageFolders": { + "/home/attila/.nuget/packages/": {} + }, + "project": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/AbsorbentSoil.csproj", + "projectName": "AbsorbentSoil", + "projectPath": "/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/AbsorbentSoil.csproj", + "packagesPath": "/home/attila/.nuget/packages/", + "outputPath": "/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/", + "projectStyle": "PackageReference", + "configFilePaths": [ + "/home/attila/.nuget/NuGet/NuGet.Config" + ], + "originalTargetFrameworks": [ + "net6.0" + ], + "sources": { + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net6.0": { + "targetAlias": "net6.0", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + }, + "SdkAnalysisLevel": "10.0.100" + }, + "frameworks": { + "net6.0": { + "targetAlias": "net6.0", + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "downloadDependencies": [ + { + "name": "Microsoft.AspNetCore.App.Ref", + "version": "[6.0.36, 6.0.36]" + }, + { + "name": "Microsoft.NETCore.App.Ref", + "version": "[6.0.36, 6.0.36]" + } + ], + "frameworkReferences": { + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "/usr/share/dotnet/sdk/10.0.104/RuntimeIdentifierGraph.json" + } + } + } +} \ No newline at end of file diff --git a/AbsorbentSoil/obj/project.nuget.cache b/AbsorbentSoil/obj/project.nuget.cache new file mode 100644 index 0000000..acbe69d --- /dev/null +++ b/AbsorbentSoil/obj/project.nuget.cache @@ -0,0 +1,11 @@ +{ + "version": 2, + "dgSpecHash": "72I/i8FVP4Q=", + "success": true, + "projectFilePath": "/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/AbsorbentSoil.csproj", + "expectedPackageFiles": [ + "/home/attila/.nuget/packages/microsoft.netcore.app.ref/6.0.36/microsoft.netcore.app.ref.6.0.36.nupkg.sha512", + "/home/attila/.nuget/packages/microsoft.aspnetcore.app.ref/6.0.36/microsoft.aspnetcore.app.ref.6.0.36.nupkg.sha512" + ], + "logs": [] +} \ No newline at end of file diff --git a/AbsorbentSoilMod.cs b/AbsorbentSoilMod.cs new file mode 100644 index 0000000..4fb8f14 --- /dev/null +++ b/AbsorbentSoilMod.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using HarmonyLib; +using MelonLoader; +using ScheduleOne.Growing; +using ScheduleOne.ItemFramework; +using ScheduleOne.ObjectScripts; +using UnityEngine; + +[assembly: MelonInfo(typeof(AbsorbentSoil.AbsorbentSoilMod), "Absorbent Soil", "0.1.0", "AttilaG")] +[assembly: MelonGame(null, "Schedule I")] + +namespace AbsorbentSoil +{ + public sealed class AbsorbentSoilMod : MelonMod + { + internal static bool VerboseLogging = false; + + public override void OnInitializeMelon() + { + MelonLogger.Msg("Absorbent Soil loaded. Additive retention is active for long-life soils."); + } + + // This keeps slot/session bleed low if the player backs out and loads another save without restarting. + // It does NOT persist anything to disk. It only tracks pots currently seen in the running game session. + public override void OnSceneWasLoaded(int buildIndex, string sceneName) + { + if (!string.IsNullOrWhiteSpace(sceneName) && + (sceneName.IndexOf("menu", StringComparison.OrdinalIgnoreCase) >= 0 || + sceneName.IndexOf("main", StringComparison.OrdinalIgnoreCase) >= 0 || + sceneName.IndexOf("load", StringComparison.OrdinalIgnoreCase) >= 0)) + { + AdditiveMemory.Clear(); + MelonLogger.Msg($"Cleared additive memory on scene load: {sceneName}"); + } + } + } + + internal static class AdditiveMemory + { + private static readonly Dictionary> AdditivesByPotKey = new(); + + public static void Remember(Pot pot, string additiveId) + { + if (pot == null || string.IsNullOrWhiteSpace(additiveId)) + return; + + string key = PotKeyHelper.GetPotKey(pot); + if (string.IsNullOrWhiteSpace(key)) + return; + + if (!AdditivesByPotKey.TryGetValue(key, out HashSet set)) + { + set = new HashSet(StringComparer.OrdinalIgnoreCase); + AdditivesByPotKey[key] = set; + } + + if (set.Add(additiveId) && AbsorbentSoilMod.VerboseLogging) + MelonLogger.Msg($"Remembered additive '{additiveId}' for pot '{key}'."); + } + + public static IReadOnlyList Get(Pot pot) + { + if (pot == null) + return Array.Empty(); + + string key = PotKeyHelper.GetPotKey(pot); + if (string.IsNullOrWhiteSpace(key)) + return Array.Empty(); + + return AdditivesByPotKey.TryGetValue(key, out HashSet set) + ? set.ToArray() + : Array.Empty(); + } + + public static void Forget(Pot pot) + { + if (pot == null) + return; + + string key = PotKeyHelper.GetPotKey(pot); + if (!string.IsNullOrWhiteSpace(key)) + AdditivesByPotKey.Remove(key); + } + + public static void Clear() => AdditivesByPotKey.Clear(); + } + + internal static class PotKeyHelper + { + public static string GetPotKey(Pot pot) + { + if (pot == null) + return string.Empty; + + // Most Schedule I buildable/grid items have a GUID inherited from a base class. + // Use reflection so this survives minor EA API shifts. + object guid = ReadMember(pot, "GUID") ?? ReadMember(pot, "Guid") ?? ReadMember(pot, "guid"); + if (guid != null && !string.IsNullOrWhiteSpace(guid.ToString())) + return guid.ToString(); + + // Fallback for testing only. Not stable across reloads, but better than hard failing. + return $"scene:{pot.GetInstanceID()}"; + } + + private static object ReadMember(object instance, string name) + { + if (instance == null || string.IsNullOrWhiteSpace(name)) + return null; + + Type type = instance.GetType(); + + PropertyInfo prop = AccessTools.Property(type, name); + if (prop != null) + { + try { return prop.GetValue(instance); } catch { } + } + + FieldInfo field = AccessTools.Field(type, name); + if (field != null) + { + try { return field.GetValue(instance); } catch { } + } + + return null; + } + } + + internal static class SoilHelper + { + // Adjust these after checking the actual IDs/names from item definitions or Melon logs. + // The helper checks IDs/names case-insensitively and allows partial matches. + private static readonly string[] RetainingSoilTokens = + { + "longlife", + "long_life", + "long-life", + "long life", + "extralonglife", + "extra_long_life", + "extra-long-life", + "extra long life" + }; + + public static bool IsRetainingSoil(Pot pot) + { + if (pot == null) + return false; + + // In current Pot.cs, soil identity is probably on GrowContainer/PotData/base state rather than Pot itself. + // Try several likely member names so the mod remains resilient to EA renames. + foreach (string memberName in new[] + { + "SoilID", "soilID", "SoilId", "soilId", + "SoilDefinition", "soilDefinition", "SoilItem", "soilItem", + "Soil", "soil", "CurrentSoil", "currentSoil" + }) + { + object value = ReadMemberRecursive(pot, memberName); + if (MatchesRetainingSoil(value)) + return true; + } + + // Last resort: inspect chunks/transforms/names. This is intentionally broad but only affects whether + // additives are remembered/reapplied; if it is too broad, tighten RetainingSoilTokens above. + string potName = SafeLower(pot.name); + if (ContainsAnyToken(potName)) + return true; + + return false; + } + + private static bool MatchesRetainingSoil(object value) + { + if (value == null) + return false; + + if (ContainsAnyToken(SafeLower(value.ToString()))) + return true; + + foreach (string memberName in new[] { "ID", "Id", "Name", "name", "ItemID", "itemID", "Definition", "definition" }) + { + object nested = ReadMemberRecursive(value, memberName); + if (nested != null && ContainsAnyToken(SafeLower(nested.ToString()))) + return true; + } + + return false; + } + + private static object ReadMemberRecursive(object instance, string name) + { + if (instance == null || string.IsNullOrWhiteSpace(name)) + return null; + + Type type = instance.GetType(); + + while (type != null) + { + PropertyInfo prop = AccessTools.Property(type, name); + if (prop != null) + { + try { return prop.GetValue(instance); } catch { } + } + + FieldInfo field = AccessTools.Field(type, name); + if (field != null) + { + try { return field.GetValue(instance); } catch { } + } + + type = type.BaseType; + } + + return null; + } + + private static bool ContainsAnyToken(string text) + { + if (string.IsNullOrWhiteSpace(text)) + return false; + + return RetainingSoilTokens.Any(token => text.Contains(token)); + } + + private static string SafeLower(string value) => value == null ? string.Empty : value.ToLowerInvariant(); + } + + [HarmonyPatch(typeof(Pot), "ApplyAdditive")] + internal static class Pot_ApplyAdditive_Patch + { + private static bool _suppressCapture; + + public static void Postfix(Pot __instance, AdditiveDefinition __result, string additiveID, bool isInitialApplication) + { + try + { + if (_suppressCapture) + return; + + if (__instance == null || __result == null || string.IsNullOrWhiteSpace(additiveID)) + return; + + if (!SoilHelper.IsRetainingSoil(__instance)) + return; + + AdditiveMemory.Remember(__instance, additiveID); + } + catch (Exception ex) + { + MelonLogger.Warning($"ApplyAdditive postfix failed: {ex}"); + } + } + + public static void ReapplyWithoutRecapture(Pot pot, string additiveID) + { + if (pot == null || string.IsNullOrWhiteSpace(additiveID)) + return; + + MethodInfo applyAdditive = AccessTools.Method(typeof(Pot), "ApplyAdditive"); + if (applyAdditive == null) + { + MelonLogger.Warning("Could not find Pot.ApplyAdditive via reflection."); + return; + } + + try + { + _suppressCapture = true; + applyAdditive.Invoke(pot, new object[] { additiveID, true }); + } + catch (Exception ex) + { + MelonLogger.Warning($"Failed to reapply additive '{additiveID}': {ex}"); + } + finally + { + _suppressCapture = false; + } + } + } + + [HarmonyPatch(typeof(Plant), nameof(Plant.Initialize))] + internal static class Plant_Initialize_Patch + { + public static void Postfix(Plant __instance) + { + try + { + if (__instance == null || __instance.Pot == null) + return; + + Pot pot = __instance.Pot; + if (!SoilHelper.IsRetainingSoil(pot)) + return; + + IReadOnlyList additiveIds = AdditiveMemory.Get(pot); + if (additiveIds.Count == 0) + return; + + foreach (string additiveId in additiveIds) + Pot_ApplyAdditive_Patch.ReapplyWithoutRecapture(pot, additiveId); + + MelonLogger.Msg($"Reapplied {additiveIds.Count} retained additive(s) to new plant in pot '{PotKeyHelper.GetPotKey(pot)}'."); + } + catch (Exception ex) + { + MelonLogger.Warning($"Plant.Initialize postfix failed: {ex}"); + } + } + } + + [HarmonyPatch(typeof(Pot), "OnPlantFullyHarvested")] + internal static class Pot_OnPlantFullyHarvested_Patch + { + public static void Postfix(Pot __instance) + { + // Intentionally do nothing. This patch exists as a reminder that retained additives should survive harvest. + // Do NOT clear AdditiveMemory here, because long-life/extra-long-life soil should carry additives forward. + } + } + + [HarmonyPatch(typeof(Pot), "Destroy")] + internal static class Pot_Destroy_Patch + { + public static void Prefix(Pot __instance) + { + // If the pot itself is destroyed/sold, stop tracking its additives. + try { AdditiveMemory.Forget(__instance); } + catch { } + } + } +}