Compare commits
12 Commits
3c7ea09f73
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 795dbe9acc | |||
| 41aead008b | |||
| 75ee5ce621 | |||
| b21db135db | |||
| 162a1ce795 | |||
| df8a62295e | |||
| 37e8635b49 | |||
| c81eda518c | |||
| fdf41b2567 | |||
| edfdcd0f82 | |||
| 4905fd318f | |||
| 7eba0a03f0 |
@@ -7,6 +7,7 @@
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
<GameDir>/home/attila/.local/share/Steam/steamapps/common/Schedule I</GameDir>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,283 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using HarmonyLib;
|
||||
using MelonLoader;
|
||||
using Il2CppScheduleOne.Growing;
|
||||
using Il2CppScheduleOne.ItemFramework;
|
||||
using Il2CppScheduleOne.ObjectScripts;
|
||||
|
||||
[assembly: MelonInfo(typeof(AbsorbentSoil.AbsorbentSoilMod), "Absorbent Soil", "0.1.1", "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 = false;
|
||||
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.");
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
// Prevent save-slot/session bleed if the player returns to menu and loads another save.
|
||||
if (!string.IsNullOrWhiteSpace(sceneName) &&
|
||||
(sceneName.IndexOf("menu", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
sceneName.IndexOf("main", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
sceneName.IndexOf("load", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
AdditiveMemory.ClearAll();
|
||||
MelonLogger.Msg($"Cleared additive memory on scene load: {sceneName}");
|
||||
AdditiveMemory.Clear();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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.AssemblyConfigurationAttribute("Release")]
|
||||
[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.AssemblyTitleAttribute("AbsorbentSoil")]
|
||||
[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.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.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.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.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user