Compare commits
4 Commits
162a1ce795
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 795dbe9acc | |||
| 41aead008b | |||
| 75ee5ce621 | |||
| b21db135db |
@@ -1,28 +1,21 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using HarmonyLib;
|
|
||||||
using MelonLoader;
|
using MelonLoader;
|
||||||
using Il2CppInterop.Runtime;
|
|
||||||
using Il2CppScheduleOne.Growing;
|
|
||||||
using Il2CppScheduleOne.ItemFramework;
|
|
||||||
using Il2CppScheduleOne.ObjectScripts;
|
|
||||||
using Il2CppScheduleOne.PlayerTasks.Tasks;
|
|
||||||
using Il2CppScheduleOne.ObjectScripts.Soil;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
[assembly: MelonInfo(typeof(AbsorbentSoil.AbsorbentSoilMod), "Absorbent Soil", "0.1.0", "AttilaG")]
|
[assembly: MelonInfo(typeof(AbsorbentSoil.AbsorbentSoilMod), "Absorbent Soil", "1.9.5", "9ate7six")]
|
||||||
[assembly: MelonGame(null, "Schedule I")]
|
[assembly: MelonGame("TVGS", "Schedule I")]
|
||||||
|
|
||||||
namespace AbsorbentSoil
|
namespace AbsorbentSoil
|
||||||
{
|
{
|
||||||
public sealed class AbsorbentSoilMod : MelonMod
|
public sealed class AbsorbentSoilMod : MelonMod
|
||||||
{
|
{
|
||||||
internal static bool VerboseLogging = true;
|
internal static bool VerboseLogging =>
|
||||||
|
Config.AbsorbentSoilConfig.VerboseLogging;
|
||||||
|
|
||||||
public override void OnInitializeMelon()
|
public override void OnInitializeMelon()
|
||||||
{
|
{
|
||||||
|
AbsorbentSoil.Config.AbsorbentSoilConfig.Init();
|
||||||
|
PersistenceStore.EnsureLoaded();
|
||||||
|
|
||||||
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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,355 +29,10 @@ namespace AbsorbentSoil
|
|||||||
sceneName.IndexOf("load", StringComparison.OrdinalIgnoreCase) >= 0))
|
sceneName.IndexOf("load", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||||
{
|
{
|
||||||
AdditiveMemory.Clear();
|
AdditiveMemory.Clear();
|
||||||
MelonLogger.Msg($"Cleared additive memory on scene load: {sceneName}");
|
SoilHelper.Clear();
|
||||||
|
PersistenceStore.ReloadNow();
|
||||||
|
MelonLogger.Msg($"Reloaded persisted 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 int DecrementRemainingSoilUses(Pot pot)
|
|
||||||
{
|
|
||||||
if (pot == null)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
string key = PotKeyHelper.GetPotKey(pot);
|
|
||||||
if (string.IsNullOrWhiteSpace(key))
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
int current = RemainingUsesByPotKey.TryGetValue(key, out int uses) ? uses : 0;
|
|
||||||
int next = Math.Max(0, current - 1);
|
|
||||||
|
|
||||||
RemainingUsesByPotKey[key] = next;
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
private static void Postfix(Pot __instance)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int remaining = SoilHelper.DecrementRemainingSoilUses(__instance);
|
|
||||||
|
|
||||||
MelonLogger.Msg($"[Absorbent Soil] Harvest consumed soil use. Remaining uses={remaining}");
|
|
||||||
|
|
||||||
if (remaining <= 0)
|
|
||||||
{
|
|
||||||
AdditiveMemory.Forget(__instance);
|
|
||||||
SoilHelper.Forget(__instance);
|
|
||||||
MelonLogger.Msg($"[Absorbent Soil] Soil depleted. Cleared additives for pot '{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(PourSoilTask), "OnInitialPour")]
|
|
||||||
internal static class PourSoilTask_OnInitialPour_Patch
|
|
||||||
{
|
|
||||||
private static unsafe void Postfix(PourSoilTask __instance)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
nint basePtr = (nint)IL2CPP.Il2CppObjectBaseToPtrNotNull(__instance);
|
|
||||||
|
|
||||||
nint soilDefPtr = *(nint*)(basePtr + 0xD0);
|
|
||||||
nint growContainerPtr = *(nint*)(basePtr + 0xE8);
|
|
||||||
|
|
||||||
SoilDefinition soilDef = soilDefPtr != 0
|
|
||||||
? new SoilDefinition(soilDefPtr)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
GrowContainer growContainer = growContainerPtr != 0
|
|
||||||
? new GrowContainer(growContainerPtr)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
Pot pot = growContainer?.TryCast<Pot>();
|
|
||||||
|
|
||||||
MelonLogger.Msg($"PourSoilTask.OnInitialPour offset-read fired. Soil={soilDef?.ID}, Uses={soilDef?.Uses}, Pot={pot?.name}");
|
|
||||||
|
|
||||||
if (pot == null || soilDef == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
SoilHelper.SetRemainingSoilUses(pot, soilDef.Uses);
|
|
||||||
AdditiveMemory.Forget(pot);
|
|
||||||
|
|
||||||
MelonLogger.Msg($"[Absorbent Soil] Fresh soil added. Pot='{PotKeyHelper.GetPotKey(pot)}', Soil='{soilDef.ID}', Uses={soilDef.Uses}. Cleared old additives.");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
MelonLogger.Warning($"PourSoilTask.OnInitialPour 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 { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using MelonLoader;
|
||||||
|
|
||||||
|
namespace AbsorbentSoil.Config
|
||||||
|
{
|
||||||
|
internal static class AbsorbentSoilConfig
|
||||||
|
{
|
||||||
|
private static MelonPreferences_Category Category;
|
||||||
|
private static MelonPreferences_Entry<bool> VerboseLoggingEntry;
|
||||||
|
|
||||||
|
public static bool VerboseLogging => VerboseLoggingEntry?.Value ?? false;
|
||||||
|
|
||||||
|
public static void Init()
|
||||||
|
{
|
||||||
|
Category = MelonPreferences.CreateCategory("AbsorbentSoil");
|
||||||
|
|
||||||
|
VerboseLoggingEntry = Category.CreateEntry(
|
||||||
|
"VerboseLogging",
|
||||||
|
false,
|
||||||
|
"Verbose Logging",
|
||||||
|
"Enable detailed troubleshooting logs for Absorbent Soil.");
|
||||||
|
|
||||||
|
Category.SaveToFile(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using HarmonyLib;
|
||||||
|
using Il2CppScheduleOne.ObjectScripts;
|
||||||
|
|
||||||
|
namespace AbsorbentSoil
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Il2CppScheduleOne.Persistence;
|
||||||
|
using MelonLoader;
|
||||||
|
|
||||||
|
internal static class RuntimeSafety
|
||||||
|
{
|
||||||
|
public static bool CanMutateState()
|
||||||
|
{
|
||||||
|
// Single-player usually passes.
|
||||||
|
// In multiplayer, this should ideally be host/server only.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using Il2CppScheduleOne.Persistence;
|
||||||
|
using MelonLoader;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
internal static class SaveSlotHelper
|
||||||
|
{
|
||||||
|
public static string GetCurrentSaveKey()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string path = LoadManager.Instance?.LoadedGameFolderPath;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(path))
|
||||||
|
return HashPath(path);
|
||||||
|
|
||||||
|
string saveName = SaveManager.Instance?.SaveName;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(saveName))
|
||||||
|
return SaveManager.SanitizeFileName(saveName);
|
||||||
|
|
||||||
|
return "global";
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return "global";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string HashPath(string path)
|
||||||
|
{
|
||||||
|
using var sha1 = SHA1.Create();
|
||||||
|
byte[] bytes = sha1.ComputeHash(Encoding.UTF8.GetBytes(path.ToLowerInvariant()));
|
||||||
|
return System.BitConverter.ToString(bytes).Replace("-", "").Substring(0, 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using HarmonyLib;
|
||||||
|
using Il2CppScheduleOne.Growing;
|
||||||
|
using Il2CppScheduleOne.ItemFramework;
|
||||||
|
using Il2CppScheduleOne.ObjectScripts;
|
||||||
|
using MelonLoader;
|
||||||
|
|
||||||
|
namespace AbsorbentSoil
|
||||||
|
{
|
||||||
|
[HarmonyPatch(typeof(Plant), nameof(Plant.AdditiveApplied))]
|
||||||
|
public static class PlantAdditiveAppliedPatch
|
||||||
|
{
|
||||||
|
public static void Postfix(Plant __instance, AdditiveDefinition additive, bool isInitialApplication)
|
||||||
|
{
|
||||||
|
if (!RuntimeSafety.CanMutateState())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (__instance == null || additive == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Important cleanup:
|
||||||
|
// Initial applications are caused by load/init/reapply behavior.
|
||||||
|
// Do NOT treat them as a fresh additive pour.
|
||||||
|
if (isInitialApplication)
|
||||||
|
{
|
||||||
|
if (AbsorbentSoilMod.VerboseLogging)
|
||||||
|
MelonLogger.Msg($"[Absorbent Soil] Ignored initial additive fire: {additive.ID}");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Pot pot = __instance.Pot;
|
||||||
|
if (pot == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string potKey = PotKeyHelper.GetPotKey(pot);
|
||||||
|
if (string.IsNullOrWhiteSpace(potKey))
|
||||||
|
return;
|
||||||
|
|
||||||
|
AdditiveMemory.Remember(pot, additive.ID);
|
||||||
|
|
||||||
|
if (AbsorbentSoilMod.VerboseLogging)
|
||||||
|
MelonLogger.Msg($"[Absorbent Soil] Remembered player additive '{additive.ID}' for pot '{potKey}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using HarmonyLib;
|
||||||
|
using Il2CppScheduleOne.Growing;
|
||||||
|
using Il2CppScheduleOne.ObjectScripts;
|
||||||
|
using MelonLoader;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace AbsorbentSoil
|
||||||
|
{
|
||||||
|
[HarmonyPatch(typeof(Plant), nameof(Plant.Initialize))]
|
||||||
|
public static class PlantInitializePatch
|
||||||
|
{
|
||||||
|
private static readonly HashSet<int> ReappliedPlants = new HashSet<int>();
|
||||||
|
|
||||||
|
public static void Postfix(Plant __instance)
|
||||||
|
{
|
||||||
|
if (__instance == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int plantId = __instance.GetInstanceID();
|
||||||
|
|
||||||
|
if (ReappliedPlants.Contains(plantId))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ReappliedPlants.Add(plantId);
|
||||||
|
|
||||||
|
Pot pot = __instance.Pot;
|
||||||
|
if (pot == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string potKey = PotKeyHelper.GetPotKey(pot);
|
||||||
|
if (string.IsNullOrWhiteSpace(potKey))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var additives = AdditiveMemory.Get(pot);
|
||||||
|
|
||||||
|
if (additives == null || additives.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (AbsorbentSoilMod.VerboseLogging)
|
||||||
|
MelonLogger.Msg($"[Absorbent Soil] Reapplying {additives.Count} remembered additive(s) to plant in pot '{potKey}'");
|
||||||
|
|
||||||
|
foreach (string additiveId in additives)
|
||||||
|
{
|
||||||
|
pot.ApplyAdditive(additiveId, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using HarmonyLib;
|
||||||
|
using Il2CppScheduleOne.ItemFramework;
|
||||||
|
using Il2CppScheduleOne.ObjectScripts;
|
||||||
|
using MelonLoader;
|
||||||
|
|
||||||
|
namespace AbsorbentSoil
|
||||||
|
{
|
||||||
|
[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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using HarmonyLib;
|
||||||
|
using Il2CppScheduleOne.ObjectScripts;
|
||||||
|
using MelonLoader;
|
||||||
|
|
||||||
|
namespace AbsorbentSoil
|
||||||
|
{
|
||||||
|
[HarmonyPatch(typeof(Pot), "OnPlantFullyHarvested")]
|
||||||
|
internal static class Pot_OnPlantFullyHarvested_Patch
|
||||||
|
{
|
||||||
|
private static void Postfix(Pot __instance)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int remaining = SoilHelper.DecrementRemainingSoilUses(__instance);
|
||||||
|
|
||||||
|
MelonLogger.Msg($"[Absorbent Soil] Harvest consumed soil use. Remaining uses={remaining}");
|
||||||
|
|
||||||
|
if (remaining <= 0)
|
||||||
|
{
|
||||||
|
AdditiveMemory.Forget(__instance);
|
||||||
|
SoilHelper.Forget(__instance);
|
||||||
|
MelonLogger.Msg($"[Absorbent Soil] Soil depleted. Cleared additives for pot '{PotKeyHelper.GetPotKey(__instance)}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MelonLogger.Warning($"OnPlantFullyHarvested postfix failed: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Pot), "Destroy")]
|
||||||
|
internal static class Pot_Destroy_Patch
|
||||||
|
{
|
||||||
|
public static void Prefix(Pot __instance)
|
||||||
|
{
|
||||||
|
if (!RuntimeSafety.CanMutateState())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// If the pot itself is destroyed/sold, stop tracking its additives.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AdditiveMemory.Forget(__instance);
|
||||||
|
SoilHelper.Forget(__instance);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using HarmonyLib;
|
||||||
|
using Il2CppInterop.Runtime;
|
||||||
|
using Il2CppScheduleOne.Growing;
|
||||||
|
using Il2CppScheduleOne.ItemFramework;
|
||||||
|
using Il2CppScheduleOne.ObjectScripts;
|
||||||
|
using Il2CppScheduleOne.PlayerTasks.Tasks;
|
||||||
|
using MelonLoader;
|
||||||
|
|
||||||
|
namespace AbsorbentSoil
|
||||||
|
{
|
||||||
|
[HarmonyPatch(typeof(PourSoilTask), "OnInitialPour")]
|
||||||
|
internal static class PourSoilTask_OnInitialPour_Patch
|
||||||
|
{
|
||||||
|
private static unsafe void Postfix(PourSoilTask __instance)
|
||||||
|
{
|
||||||
|
if (!RuntimeSafety.CanMutateState())
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
nint basePtr = (nint)IL2CPP.Il2CppObjectBaseToPtrNotNull(__instance);
|
||||||
|
|
||||||
|
nint soilDefPtr = *(nint*)(basePtr + 0xD0);
|
||||||
|
nint growContainerPtr = *(nint*)(basePtr + 0xE8);
|
||||||
|
|
||||||
|
SoilDefinition soilDef = soilDefPtr != 0
|
||||||
|
? new SoilDefinition(soilDefPtr)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
GrowContainer growContainer = growContainerPtr != 0
|
||||||
|
? new GrowContainer(growContainerPtr)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
Pot pot = growContainer?.TryCast<Pot>();
|
||||||
|
|
||||||
|
MelonLogger.Msg($"PourSoilTask.OnInitialPour offset-read fired. Soil={soilDef?.ID}, Uses={soilDef?.Uses}, Pot={pot?.name}");
|
||||||
|
|
||||||
|
if (pot == null || soilDef == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SoilHelper.SetRemainingSoilUses(pot, soilDef.Uses);
|
||||||
|
AdditiveMemory.Forget(pot);
|
||||||
|
|
||||||
|
MelonLogger.Msg($"[Absorbent Soil] Fresh soil added. Pot='{PotKeyHelper.GetPotKey(pot)}', Soil='{soilDef.ID}', Uses={soilDef.Uses}. Cleared old additives.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MelonLogger.Warning($"PourSoilTask.OnInitialPour postfix failed: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Il2CppScheduleOne.ObjectScripts;
|
||||||
|
using MelonLoader;
|
||||||
|
|
||||||
|
namespace AbsorbentSoil
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
if (AbsorbentSoilMod.VerboseLogging)
|
||||||
|
MelonLogger.Msg($"Remembered additive '{additiveId}' for pot '{key}'.");
|
||||||
|
|
||||||
|
PersistenceStore.SaveNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
PersistenceStore.SaveNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<string, List<string>> Export()
|
||||||
|
{
|
||||||
|
return AdditivesByPotKey.ToDictionary(
|
||||||
|
kvp => kvp.Key,
|
||||||
|
kvp => kvp.Value.ToList(),
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Import(Dictionary<string, IEnumerable<string>> additivesByPotKey)
|
||||||
|
{
|
||||||
|
AdditivesByPotKey.Clear();
|
||||||
|
|
||||||
|
if (additivesByPotKey == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var kvp in additivesByPotKey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(kvp.Key) || kvp.Value == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var set = new HashSet<string>(
|
||||||
|
kvp.Value.Where(id => !string.IsNullOrWhiteSpace(id)),
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (set.Count > 0)
|
||||||
|
AdditivesByPotKey[kvp.Key] = set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory-only clear for changing scenes/saves. Do not call SaveNow here or you can wipe the JSON.
|
||||||
|
public static void Clear() => AdditivesByPotKey.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using MelonLoader;
|
||||||
|
using MelonLoader.Preferences;
|
||||||
|
using MelonLoader.Utils;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Il2CppScheduleOne.Persistence;
|
||||||
|
|
||||||
|
namespace AbsorbentSoil
|
||||||
|
{
|
||||||
|
internal static class PersistenceStore
|
||||||
|
{
|
||||||
|
private const int CurrentVersion = 1;
|
||||||
|
private static bool _loaded;
|
||||||
|
private static bool _isLoading;
|
||||||
|
|
||||||
|
private static string DirectoryPath => Path.Combine(
|
||||||
|
MelonEnvironment.UserDataDirectory,
|
||||||
|
"AbsorbentSoil",
|
||||||
|
GetCurrentSaveKey());
|
||||||
|
|
||||||
|
private static string FilePath => Path.Combine(DirectoryPath, "state.json");
|
||||||
|
|
||||||
|
private static string GetCurrentSaveKey()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string loadedPath = LoadManager.Instance?.LoadedGameFolderPath;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(loadedPath))
|
||||||
|
return HashPath(loadedPath);
|
||||||
|
|
||||||
|
string saveName = SaveManager.Instance?.SaveName;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(saveName))
|
||||||
|
return SaveManager.SanitizeFileName(saveName);
|
||||||
|
|
||||||
|
return "global";
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return "global";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string HashPath(string path)
|
||||||
|
{
|
||||||
|
using SHA1 sha1 = SHA1.Create();
|
||||||
|
|
||||||
|
byte[] bytes = sha1.ComputeHash(
|
||||||
|
Encoding.UTF8.GetBytes(path.ToLowerInvariant()));
|
||||||
|
|
||||||
|
return BitConverter
|
||||||
|
.ToString(bytes)
|
||||||
|
.Replace("-", "")
|
||||||
|
.Substring(0, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void EnsureLoaded()
|
||||||
|
{
|
||||||
|
if (_loaded)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LoadNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ReloadNow()
|
||||||
|
{
|
||||||
|
_loaded = false;
|
||||||
|
LoadNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void LoadNow()
|
||||||
|
{
|
||||||
|
if (_isLoading)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isLoading = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_loaded = true;
|
||||||
|
if (AbsorbentSoilMod.VerboseLogging)
|
||||||
|
{
|
||||||
|
MelonLogger.Msg($"[Absorbent Soil] LoadedGameFolderPath = {LoadManager.Instance?.LoadedGameFolderPath}");
|
||||||
|
MelonLogger.Msg($"[Absorbent Soil] SaveName = {SaveManager.Instance?.SaveName}");
|
||||||
|
MelonLogger.Msg($"[Absorbent Soil] SaveKey = {GetCurrentSaveKey()}");
|
||||||
|
MelonLogger.Msg($"[Absorbent Soil] Persistence file = {FilePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(FilePath))
|
||||||
|
{
|
||||||
|
if (AbsorbentSoilMod.VerboseLogging)
|
||||||
|
MelonLogger.Msg("[Absorbent Soil] No persistence file found yet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string json = File.ReadAllText(FilePath);
|
||||||
|
PersistedState state = JsonSerializer.Deserialize<PersistedState>(json);
|
||||||
|
if (state == null || state.Pots == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AdditiveMemory.Import(state.Pots.ToDictionary(
|
||||||
|
kvp => kvp.Key,
|
||||||
|
kvp => (IEnumerable<string>)(kvp.Value?.Additives ?? new List<string>()),
|
||||||
|
StringComparer.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
SoilHelper.Import(state.Pots.ToDictionary(
|
||||||
|
kvp => kvp.Key,
|
||||||
|
kvp => kvp.Value?.RemainingUses ?? 0,
|
||||||
|
StringComparer.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
MelonLogger.Msg($"[Absorbent Soil] Loaded persisted state for {state.Pots.Count} pot(s).");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MelonLogger.Warning($"[Absorbent Soil] Failed to load persisted state: {ex}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SaveNow()
|
||||||
|
{
|
||||||
|
if (_isLoading)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(DirectoryPath);
|
||||||
|
|
||||||
|
Dictionary<string, List<string>> additiveSnapshot = AdditiveMemory.Export();
|
||||||
|
Dictionary<string, int> soilSnapshot = SoilHelper.Export();
|
||||||
|
|
||||||
|
var keys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (string key in additiveSnapshot.Keys)
|
||||||
|
keys.Add(key);
|
||||||
|
foreach (string key in soilSnapshot.Keys)
|
||||||
|
keys.Add(key);
|
||||||
|
|
||||||
|
var state = new PersistedState
|
||||||
|
{
|
||||||
|
Version = CurrentVersion,
|
||||||
|
UpdatedUtc = DateTime.UtcNow,
|
||||||
|
Pots = new Dictionary<string, PersistedPotState>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (string key in keys)
|
||||||
|
{
|
||||||
|
additiveSnapshot.TryGetValue(key, out List<string> additives);
|
||||||
|
soilSnapshot.TryGetValue(key, out int remainingUses);
|
||||||
|
|
||||||
|
additives ??= new List<string>();
|
||||||
|
|
||||||
|
// Avoid keeping dead/empty records forever.
|
||||||
|
if (remainingUses <= 0 && additives.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
state.Pots[key] = new PersistedPotState
|
||||||
|
{
|
||||||
|
RemainingUses = remainingUses,
|
||||||
|
Additives = additives
|
||||||
|
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(id => id, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions { WriteIndented = true };
|
||||||
|
File.WriteAllText(FilePath, JsonSerializer.Serialize(state, options));
|
||||||
|
|
||||||
|
if (AbsorbentSoilMod.VerboseLogging)
|
||||||
|
MelonLogger.Msg($"[Absorbent Soil] Saved persisted state for {state.Pots.Count} pot(s).");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MelonLogger.Warning($"[Absorbent Soil] Failed to save persisted state: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PersistedState
|
||||||
|
{
|
||||||
|
public int Version { get; set; }
|
||||||
|
public DateTime UpdatedUtc { get; set; }
|
||||||
|
public Dictionary<string, PersistedPotState> Pots { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PersistedPotState
|
||||||
|
{
|
||||||
|
public int RemainingUses { get; set; }
|
||||||
|
public List<string> Additives { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Il2CppScheduleOne.ObjectScripts;
|
||||||
|
using MelonLoader;
|
||||||
|
|
||||||
|
namespace AbsorbentSoil
|
||||||
|
{
|
||||||
|
internal static class SoilHelper
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, int> RemainingUsesByPotKey = new();
|
||||||
|
|
||||||
|
public static int DecrementRemainingSoilUses(Pot pot)
|
||||||
|
{
|
||||||
|
if (!RuntimeSafety.CanMutateState())
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (pot == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
string key = PotKeyHelper.GetPotKey(pot);
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
int current = RemainingUsesByPotKey.TryGetValue(key, out int uses) ? uses : 0;
|
||||||
|
int next = Math.Max(0, current - 1);
|
||||||
|
|
||||||
|
RemainingUsesByPotKey[key] = next;
|
||||||
|
PersistenceStore.SaveNow();
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetRemainingSoilUses(Pot pot, int uses)
|
||||||
|
{
|
||||||
|
if (!RuntimeSafety.CanMutateState())
|
||||||
|
return;
|
||||||
|
|
||||||
|
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}");
|
||||||
|
|
||||||
|
PersistenceStore.SaveNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
PersistenceStore.SaveNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<string, int> Export()
|
||||||
|
{
|
||||||
|
return new Dictionary<string, int>(RemainingUsesByPotKey, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Import(Dictionary<string, int> remainingUsesByPotKey)
|
||||||
|
{
|
||||||
|
RemainingUsesByPotKey.Clear();
|
||||||
|
|
||||||
|
if (remainingUsesByPotKey == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var kvp in remainingUsesByPotKey)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(kvp.Key))
|
||||||
|
RemainingUsesByPotKey[kvp.Key] = Math.Max(0, kvp.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Clear() => RemainingUsesByPotKey.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
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+df8a62295eea38ad16e750280eea878da48af3ae")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+41aead008b6360d5bcd9ec7582d4f5227870e88e")]
|
||||||
[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 @@
|
|||||||
205b227cc6bf36a9df78289d110ec64054a771277aee03d496358bdc65fdfb7b
|
5b3843bf5d032d4965db2bf686646296b18d42f85ea867f9b81bf1117453f45b
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
ddb36bf3fc2e77af19df7cc3d5e652bab5098b8451766fb4c9483694133ef288
|
c4aee16fa9119b298402ca973e24c641ed042847f3a55a64c8b8a5107dac9e48
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user