This commit is contained in:
2026-05-18 20:11:39 -04:00
parent 1374b2e929
commit 57f6a7917f
8 changed files with 85 additions and 113 deletions
@@ -4,12 +4,11 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using HarmonyLib; using HarmonyLib;
using MelonLoader; using MelonLoader;
using ScheduleOne.Growing; using Il2CppScheduleOne.Growing;
using ScheduleOne.ItemFramework; using Il2CppScheduleOne.ItemFramework;
using ScheduleOne.ObjectScripts; using Il2CppScheduleOne.ObjectScripts;
using UnityEngine;
[assembly: MelonInfo(typeof(AbsorbentSoil.AbsorbentSoilMod), "Absorbent Soil", "0.1.0", "AttilaG")] [assembly: MelonInfo(typeof(AbsorbentSoil.AbsorbentSoilMod), "Absorbent Soil", "0.1.1", "AttilaG")]
[assembly: MelonGame(null, "Schedule I")] [assembly: MelonGame(null, "Schedule I")]
namespace AbsorbentSoil namespace AbsorbentSoil
@@ -23,16 +22,15 @@ namespace AbsorbentSoil
MelonLogger.Msg("Absorbent Soil loaded. Additive retention is active for long-life soils."); 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) public override void OnSceneWasLoaded(int buildIndex, string sceneName)
{ {
// Prevent save-slot/session bleed if the player returns to menu and loads another save.
if (!string.IsNullOrWhiteSpace(sceneName) && if (!string.IsNullOrWhiteSpace(sceneName) &&
(sceneName.IndexOf("menu", StringComparison.OrdinalIgnoreCase) >= 0 || (sceneName.IndexOf("menu", StringComparison.OrdinalIgnoreCase) >= 0 ||
sceneName.IndexOf("main", StringComparison.OrdinalIgnoreCase) >= 0 || sceneName.IndexOf("main", StringComparison.OrdinalIgnoreCase) >= 0 ||
sceneName.IndexOf("load", StringComparison.OrdinalIgnoreCase) >= 0)) sceneName.IndexOf("load", StringComparison.OrdinalIgnoreCase) >= 0))
{ {
AdditiveMemory.Clear(); AdditiveMemory.ClearAll();
MelonLogger.Msg($"Cleared additive memory on scene load: {sceneName}"); MelonLogger.Msg($"Cleared additive memory on scene load: {sceneName}");
} }
} }
@@ -80,12 +78,22 @@ namespace AbsorbentSoil
if (pot == null) if (pot == null)
return; return;
string key = PotKeyHelper.GetPotKey(pot); Forget(PotKeyHelper.GetPotKey(pot));
if (!string.IsNullOrWhiteSpace(key))
AdditivesByPotKey.Remove(key);
} }
public static void Clear() => AdditivesByPotKey.Clear(); public static void Forget(string potKey)
{
if (string.IsNullOrWhiteSpace(potKey))
return;
if (AdditivesByPotKey.Remove(potKey) && AbsorbentSoilMod.VerboseLogging)
MelonLogger.Msg($"Cleared retained additives for pot '{potKey}'.");
}
public static void ClearAll()
{
AdditivesByPotKey.Clear();
}
} }
internal static class PotKeyHelper internal static class PotKeyHelper
@@ -95,13 +103,10 @@ namespace AbsorbentSoil
if (pot == null) if (pot == null)
return string.Empty; 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"); object guid = ReadMember(pot, "GUID") ?? ReadMember(pot, "Guid") ?? ReadMember(pot, "guid");
if (guid != null && !string.IsNullOrWhiteSpace(guid.ToString())) if (guid != null && !string.IsNullOrWhiteSpace(guid.ToString()))
return guid.ToString(); return guid.ToString();
// Fallback for testing only. Not stable across reloads, but better than hard failing.
return $"scene:{pot.GetInstanceID()}"; return $"scene:{pot.GetInstanceID()}";
} }
@@ -111,92 +116,6 @@ namespace AbsorbentSoil
return null; return null;
Type type = instance.GetType(); 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) while (type != null)
{ {
PropertyInfo prop = AccessTools.Property(type, name); PropertyInfo prop = AccessTools.Property(type, name);
@@ -216,22 +135,61 @@ namespace AbsorbentSoil
return null; return null;
} }
}
private static bool ContainsAnyToken(string text) internal static class SoilHelper
{
public static bool IsRetainingSoil(Pot pot)
{ {
if (string.IsNullOrWhiteSpace(text)) string soilId = GetSoilId(pot);
if (string.IsNullOrWhiteSpace(soilId))
return false; return false;
return RetainingSoilTokens.Any(token => text.Contains(token)); soilId = soilId.ToLowerInvariant();
return soilId.Contains("longlifesoil") || soilId.Contains("extralonglifesoil");
} }
private static string SafeLower(string value) => value == null ? string.Empty : value.ToLowerInvariant(); public static string GetSoilId(Pot pot)
{
if (pot == null)
return string.Empty;
object soilId = ReadMember(pot, "SoilID") ?? ReadMember(pot, "soilID") ?? ReadMember(pot, "SoilId") ?? ReadMember(pot, "soilId");
return soilId?.ToString() ?? string.Empty;
}
private static object ReadMember(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;
}
} }
[HarmonyPatch(typeof(Pot), "ApplyAdditive")] [HarmonyPatch(typeof(Pot), "ApplyAdditive")]
internal static class Pot_ApplyAdditive_Patch internal static class Pot_ApplyAdditive_Patch
{ {
private static bool _suppressCapture; private static bool _suppressCapture;
private static readonly MethodInfo ApplyAdditiveMethod = AccessTools.Method(typeof(Pot), "ApplyAdditive");
public static void Postfix(Pot __instance, AdditiveDefinition __result, string additiveID, bool isInitialApplication) public static void Postfix(Pot __instance, AdditiveDefinition __result, string additiveID, bool isInitialApplication)
{ {
@@ -259,8 +217,7 @@ namespace AbsorbentSoil
if (pot == null || string.IsNullOrWhiteSpace(additiveID)) if (pot == null || string.IsNullOrWhiteSpace(additiveID))
return; return;
MethodInfo applyAdditive = AccessTools.Method(typeof(Pot), "ApplyAdditive"); if (ApplyAdditiveMethod == null)
if (applyAdditive == null)
{ {
MelonLogger.Warning("Could not find Pot.ApplyAdditive via reflection."); MelonLogger.Warning("Could not find Pot.ApplyAdditive via reflection.");
return; return;
@@ -269,7 +226,7 @@ namespace AbsorbentSoil
try try
{ {
_suppressCapture = true; _suppressCapture = true;
applyAdditive.Invoke(pot, new object[] { additiveID, true }); ApplyAdditiveMethod.Invoke(pot, new object[] { additiveID, true });
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -293,8 +250,12 @@ namespace AbsorbentSoil
return; return;
Pot pot = __instance.Pot; Pot pot = __instance.Pot;
if (!SoilHelper.IsRetainingSoil(pot)) if (!SoilHelper.IsRetainingSoil(pot))
{
AdditiveMemory.Forget(pot);
return; return;
}
IReadOnlyList<string> additiveIds = AdditiveMemory.Get(pot); IReadOnlyList<string> additiveIds = AdditiveMemory.Get(pot);
if (additiveIds.Count == 0) if (additiveIds.Count == 0)
@@ -317,8 +278,20 @@ namespace AbsorbentSoil
{ {
public static void Postfix(Pot __instance) public static void Postfix(Pot __instance)
{ {
// Intentionally do nothing. This patch exists as a reminder that retained additives should survive harvest. try
// Do NOT clear AdditiveMemory here, because long-life/extra-long-life soil should carry additives forward. {
if (__instance == null)
return;
// After the game's harvest logic runs, if soil was consumed/removed or is no longer retaining soil,
// clear the retained additives so freshly re-poured soil starts clean.
if (!SoilHelper.IsRetainingSoil(__instance))
AdditiveMemory.Forget(__instance);
}
catch (Exception ex)
{
MelonLogger.Warning($"OnPlantFullyHarvested postfix failed: {ex}");
}
} }
} }
@@ -327,7 +300,6 @@ namespace AbsorbentSoil
{ {
public static void Prefix(Pot __instance) public static void Prefix(Pot __instance)
{ {
// If the pot itself is destroyed/sold, stop tracking its additives.
try { AdditiveMemory.Forget(__instance); } try { AdditiveMemory.Forget(__instance); }
catch { } catch { }
} }
Binary file not shown.
@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("AbsorbentSoil")] [assembly: System.Reflection.AssemblyCompanyAttribute("AbsorbentSoil")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+1374b2e929263849b9362bc2032e73ac19079563")]
[assembly: System.Reflection.AssemblyProductAttribute("AbsorbentSoil")] [assembly: System.Reflection.AssemblyProductAttribute("AbsorbentSoil")]
[assembly: System.Reflection.AssemblyTitleAttribute("AbsorbentSoil")] [assembly: System.Reflection.AssemblyTitleAttribute("AbsorbentSoil")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
@@ -1 +1 @@
54ce364b284ea9f7c7e326b3938578be61e76fb507bfb64c59c1c67e28888f74 c2488a0a4e0149be33c85611dadce8edca6655bd5fdedcb3c238565f85b2d0ec
Binary file not shown.
Binary file not shown.