Compare commits

...

4 Commits

21 changed files with 801 additions and 365 deletions
+10 -362
View File
@@ -1,28 +1,21 @@
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 Il2CppScheduleOne.PlayerTasks.Tasks;
using Il2CppScheduleOne.ObjectScripts.Soil;
using UnityEngine;
[assembly: MelonInfo(typeof(AbsorbentSoil.AbsorbentSoilMod), "Absorbent Soil", "0.1.0", "AttilaG")]
[assembly: MelonGame(null, "Schedule I")]
[assembly: MelonInfo(typeof(AbsorbentSoil.AbsorbentSoilMod), "Absorbent Soil", "1.9.5", "9ate7six")]
[assembly: MelonGame("TVGS", "Schedule I")]
namespace AbsorbentSoil
{
public sealed class AbsorbentSoilMod : MelonMod
{
internal static bool VerboseLogging = true;
internal static bool VerboseLogging =>
Config.AbsorbentSoilConfig.VerboseLogging;
public override void OnInitializeMelon()
{
AbsorbentSoil.Config.AbsorbentSoilConfig.Init();
PersistenceStore.EnsureLoaded();
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))
{
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);
}
}
}
+47
View File
@@ -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;
}
}
+37
View File
@@ -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}");
}
}
}
}
+104
View File
@@ -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();
}
}
+200
View File
@@ -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();
}
}
}
+102
View File
@@ -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.AssemblyConfigurationAttribute("Release")]
[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.AssemblyTitleAttribute("AbsorbentSoil")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
@@ -1 +1 @@
205b227cc6bf36a9df78289d110ec64054a771277aee03d496358bdc65fdfb7b
5b3843bf5d032d4965db2bf686646296b18d42f85ea867f9b81bf1117453f45b
@@ -1 +1 @@
ddb36bf3fc2e77af19df7cc3d5e652bab5098b8451766fb4c9483694133ef288
c4aee16fa9119b298402ca973e24c641ed042847f3a55a64c8b8a5107dac9e48
Binary file not shown.
Binary file not shown.