Compare commits

..

12 Commits

24 changed files with 835 additions and 261 deletions
+1
View File
@@ -7,6 +7,7 @@
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<Nullable>disable</Nullable> <Nullable>disable</Nullable>
<GameDir>/home/attila/.local/share/Steam/steamapps/common/Schedule I</GameDir> <GameDir>/home/attila/.local/share/Steam/steamapps/common/Schedule I</GameDir>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
+13 -258
View File
@@ -1,283 +1,38 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using MelonLoader; using MelonLoader;
using Il2CppScheduleOne.Growing;
using Il2CppScheduleOne.ItemFramework;
using Il2CppScheduleOne.ObjectScripts;
[assembly: MelonInfo(typeof(AbsorbentSoil.AbsorbentSoilMod), "Absorbent Soil", "0.1.1", "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 = false; 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.");
} }
// 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.ClearAll(); 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 void Forget(Pot pot)
{
if (pot == null)
return;
Forget(PotKeyHelper.GetPotKey(pot));
}
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
{
public static string GetPotKey(Pot pot)
{
if (pot == null)
return string.Empty;
object guid = ReadMember(pot, "GUID") ?? ReadMember(pot, "Guid") ?? ReadMember(pot, "guid");
if (guid != null && !string.IsNullOrWhiteSpace(guid.ToString()))
return guid.ToString();
return $"scene:{pot.GetInstanceID()}";
}
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;
}
}
internal static class SoilHelper
{
private static readonly FieldInfo RemainingSoilUsesField =
AccessTools.Field(typeof(GrowContainer), "_remainingSoilUses");
public static bool IsRetainingSoil(Pot pot)
{
if (pot == null || string.IsNullOrWhiteSpace(pot.SoilID))
return false;
string soilId = pot.SoilID.ToLowerInvariant();
return soilId.Contains("longlifesoil") ||
soilId.Contains("extralonglifesoil");
}
public static int GetRemainingSoilUses(Pot pot)
{
if (pot == null || RemainingSoilUsesField == null)
return 0;
return (int)RemainingSoilUsesField.GetValue(pot);
}
public static bool CanRetainAdditives(Pot pot)
{
return IsRetainingSoil(pot) && GetRemainingSoilUses(pot) > 0;
}
}
[HarmonyPatch(typeof(Pot), "ApplyAdditive")]
internal static class Pot_ApplyAdditive_Patch
{
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)
{
try
{
if (_suppressCapture)
return;
if (__instance == null || __result == null || string.IsNullOrWhiteSpace(additiveID))
return;
if (!SoilHelper.IsRetainingSoil(__instance))
return;
AdditiveMemory.Remember(__instance, additiveID);
}
catch (Exception ex)
{
MelonLogger.Warning($"ApplyAdditive postfix failed: {ex}");
}
}
public static void ReapplyWithoutRecapture(Pot pot, string additiveID)
{
if (pot == null || string.IsNullOrWhiteSpace(additiveID))
return;
if (ApplyAdditiveMethod == null)
{
MelonLogger.Warning("Could not find Pot.ApplyAdditive via reflection.");
return;
}
try
{
_suppressCapture = true;
ApplyAdditiveMethod.Invoke(pot, new object[] { additiveID, true });
}
catch (Exception ex)
{
MelonLogger.Warning($"Failed to reapply additive '{additiveID}': {ex}");
}
finally
{
_suppressCapture = false;
}
}
}
[HarmonyPatch(typeof(Plant), nameof(Plant.Initialize))]
internal static class Plant_Initialize_Patch
{
public static void Postfix(Plant __instance)
{
try
{
if (__instance == null || __instance.Pot == null)
return;
Pot pot = __instance.Pot;
if (!SoilHelper.CanRetainAdditives(pot))
{
AdditiveMemory.Forget(pot);
return;
}
IReadOnlyList<string> additiveIds = AdditiveMemory.Get(pot);
if (additiveIds.Count == 0)
return;
foreach (string additiveId in additiveIds)
Pot_ApplyAdditive_Patch.ReapplyWithoutRecapture(pot, additiveId);
MelonLogger.Msg($"Reapplied {additiveIds.Count} retained additive(s) to new plant in pot '{PotKeyHelper.GetPotKey(pot)}'.");
}
catch (Exception ex)
{
MelonLogger.Warning($"Plant.Initialize postfix failed: {ex}");
}
}
}
[HarmonyPatch(typeof(Pot), "OnPlantFullyHarvested")]
internal static class Pot_OnPlantFullyHarvested_Patch
{
private static void Postfix(Pot __instance)
{
if (__instance == null)
return;
if (!SoilHelper.CanRetainAdditives(__instance))
{
AdditiveMemory.Forget(__instance);
MelonLogger.Msg("[Absorbent Soil] Cleared retained additives because soil has no retaining uses left.");
}
}
}
[HarmonyPatch(typeof(Pot), "Destroy")]
internal static class Pot_Destroy_Patch
{
public static void Prefix(Pot __instance)
{
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();
}
}
@@ -0,0 +1,23 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v6.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v6.0": {
"AbsorbentSoil/1.0.0": {
"runtime": {
"AbsorbentSoil.dll": {}
}
}
}
},
"libraries": {
"AbsorbentSoil/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}
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+57f6a7917f7640bd65cefb3f66b24ea34b817e0c")] [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 @@
8a3855f139cd66387b0e747834f0fb38e6416f2dc381e733183dbdeaa1f21465 5b3843bf5d032d4965db2bf686646296b18d42f85ea867f9b81bf1117453f45b
@@ -1 +1 @@
ddb36bf3fc2e77af19df7cc3d5e652bab5098b8451766fb4c9483694133ef288 c4aee16fa9119b298402ca973e24c641ed042847f3a55a64c8b8a5107dac9e48
@@ -1,5 +1,12 @@
/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/bin/Release/net6.0/AbsorbentSoil.deps.json
/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/bin/Release/net6.0/AbsorbentSoil.dll
/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/bin/Release/net6.0/AbsorbentSoil.pdb
/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.csproj.AssemblyReference.cache /home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.csproj.AssemblyReference.cache
/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.GeneratedMSBuildEditorConfig.editorconfig /home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.GeneratedMSBuildEditorConfig.editorconfig
/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.AssemblyInfoInputs.cache /home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.AssemblyInfoInputs.cache
/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.AssemblyInfo.cs /home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.AssemblyInfo.cs
/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.csproj.CoreCompileInputs.cache /home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.csproj.CoreCompileInputs.cache
/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.dll
/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/refint/AbsorbentSoil.dll
/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/AbsorbentSoil.pdb
/home/attila/Projects/Schedule-1/AbsorbentSoilMod/AbsorbentSoil/obj/Release/net6.0/ref/AbsorbentSoil.dll
Binary file not shown.
Binary file not shown.