347 lines
11 KiB
C#
347 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using HarmonyLib;
|
|
using MelonLoader;
|
|
using Il2CppInterop.Runtime;
|
|
using Il2CppScheduleOne.Growing;
|
|
using Il2CppScheduleOne.ItemFramework;
|
|
using Il2CppScheduleOne.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 = true;
|
|
|
|
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 bool HasStored(Pot pot)
|
|
{
|
|
if (pot == null)
|
|
return false;
|
|
|
|
string key = PotKeyHelper.GetPotKey(pot);
|
|
return !string.IsNullOrWhiteSpace(key) &&
|
|
AdditivesByPotKey.TryGetValue(key, out HashSet<string> set) &&
|
|
set.Count > 0;
|
|
}
|
|
|
|
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
|
|
{
|
|
private static readonly Dictionary<string, int> RemainingUsesByPotKey = new();
|
|
|
|
public static void SetRemainingSoilUses(Pot pot, int uses)
|
|
{
|
|
if (pot == null)
|
|
return;
|
|
|
|
string key = PotKeyHelper.GetPotKey(pot);
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
return;
|
|
|
|
RemainingUsesByPotKey[key] = uses;
|
|
|
|
if (AbsorbentSoilMod.VerboseLogging)
|
|
MelonLogger.Msg($"Tracked soil uses for pot '{key}': {uses}");
|
|
}
|
|
|
|
public static int GetRemainingSoilUses(Pot pot)
|
|
{
|
|
if (pot == null)
|
|
return 0;
|
|
|
|
string key = PotKeyHelper.GetPotKey(pot);
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
return 0;
|
|
|
|
return RemainingUsesByPotKey.TryGetValue(key, out int uses) ? uses : 0;
|
|
}
|
|
|
|
public static bool CanCaptureNewAdditives(Pot pot)
|
|
{
|
|
return GetRemainingSoilUses(pot) > 1;
|
|
}
|
|
|
|
public static bool CanReapplyRetainedAdditives(Pot pot)
|
|
{
|
|
return GetRemainingSoilUses(pot) > 0;
|
|
}
|
|
|
|
public static void Forget(Pot pot)
|
|
{
|
|
string key = PotKeyHelper.GetPotKey(pot);
|
|
if (!string.IsNullOrWhiteSpace(key))
|
|
RemainingUsesByPotKey.Remove(key);
|
|
}
|
|
}
|
|
|
|
[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.CanCaptureNewAdditives(__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
|
|
{
|
|
MelonLogger.Msg($"Plant.Initialize fired. Plant={__instance?.name}, Pot={__instance?.Pot?.name}");
|
|
|
|
if (__instance == null || __instance.Pot == null)
|
|
return;
|
|
|
|
Pot actualPot = __instance.Pot;
|
|
var additiveIds = AdditiveMemory.Get(actualPot);
|
|
|
|
MelonLogger.Msg($"Plant.Initialize pot key={PotKeyHelper.GetPotKey(actualPot)}, remembered additives={additiveIds.Count}");
|
|
|
|
foreach (string additiveId in additiveIds)
|
|
Pot_ApplyAdditive_Patch.ReapplyWithoutRecapture(actualPot, additiveId);
|
|
}
|
|
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), "OnPlantFullyHarvested")]
|
|
internal static class Pot_OnPlantFullyHarvested_Patch
|
|
{
|
|
private static void Prefix(Pot __instance)
|
|
{
|
|
try
|
|
{
|
|
MelonLogger.Msg($"Pot.OnPlantFullyHarvested fired. Pot={__instance?.name}, key={PotKeyHelper.GetPotKey(__instance)}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MelonLogger.Warning($"OnPlantFullyHarvested prefix failed: {ex}");
|
|
}
|
|
}
|
|
|
|
private static void Postfix(Pot __instance)
|
|
{
|
|
try
|
|
{
|
|
MelonLogger.Msg($"Pot.OnPlantFullyHarvested postfix. Pot={__instance?.name}, key={PotKeyHelper.GetPotKey(__instance)}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MelonLogger.Warning($"OnPlantFullyHarvested postfix failed: {ex}");
|
|
}
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch(typeof(Plant), nameof(Plant.AdditiveApplied))]
|
|
internal static class Plant_AdditiveApplied_Patch
|
|
{
|
|
public static void Postfix(Plant __instance, AdditiveDefinition additive, bool isInitialApplication)
|
|
{
|
|
try
|
|
{
|
|
MelonLogger.Msg($"Plant.AdditiveApplied fired. Plant={__instance?.name}, Pot={__instance?.Pot?.name}, Additive={additive?.ID}, Initial={isInitialApplication}");
|
|
|
|
if (__instance == null || __instance.Pot == null || additive == null)
|
|
return;
|
|
|
|
string additiveId = additive.ID;
|
|
if (string.IsNullOrWhiteSpace(additiveId))
|
|
return;
|
|
|
|
AdditiveMemory.Remember(__instance.Pot, additiveId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MelonLogger.Warning($"Plant.AdditiveApplied postfix failed: {ex}");
|
|
}
|
|
}
|
|
}
|
|
|
|
[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 { }
|
|
}
|
|
}
|
|
}
|