Files
AbsorbentSoil/AbsorbentSoilMod.cs
T
2026-05-18 18:43:51 -04:00

336 lines
11 KiB
C#

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<string, HashSet<string>> 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<string> set))
{
set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
AdditivesByPotKey[key] = set;
}
if (set.Add(additiveId) && AbsorbentSoilMod.VerboseLogging)
MelonLogger.Msg($"Remembered additive '{additiveId}' for pot '{key}'.");
}
public static IReadOnlyList<string> Get(Pot pot)
{
if (pot == null)
return Array.Empty<string>();
string key = PotKeyHelper.GetPotKey(pot);
if (string.IsNullOrWhiteSpace(key))
return Array.Empty<string>();
return AdditivesByPotKey.TryGetValue(key, out HashSet<string> set)
? set.ToArray()
: Array.Empty<string>();
}
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<string> 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 { }
}
}
}