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 { } } } }